From 505e6ed694d019d3361132a027bd65b73f818a02 Mon Sep 17 00:00:00 2001 From: Ahmet Alp Balkan Date: Wed, 29 Nov 2017 13:12:36 -0800 Subject: [PATCH 001/395] =?UTF-8?q?Container=20Engine=20=E2=86=92=20Kubern?= =?UTF-8?q?etes=20Engine=20(#150)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Ahmet Alp Balkan --- .../.dockerignore | 0 .../Dockerfile | 0 .../Makefile | 0 .../README.md | 8 ++++---- .../bookshelf-frontend.yaml | 2 +- .../bookshelf-service.yaml | 6 +++--- .../bookshelf-worker.yaml | 2 +- .../bookshelf/__init__.py | 0 .../bookshelf/crud.py | 0 .../bookshelf/model_cloudsql.py | 0 .../bookshelf/model_datastore.py | 0 .../bookshelf/model_mongodb.py | 0 .../bookshelf/storage.py | 0 .../bookshelf/tasks.py | 0 .../bookshelf/templates/base.html | 0 .../bookshelf/templates/form.html | 0 .../bookshelf/templates/list.html | 0 .../bookshelf/templates/view.html | 0 .../config.py | 0 .../main.py | 0 .../procfile | 0 .../requirements-dev.txt | 0 .../requirements.txt | 0 .../tests/conftest.py | 0 .../tests/test_auth.py | 0 .../tests/test_crud.py | 0 .../tests/test_end_to_end.py | 0 .../tests/test_storage.py | 0 .../tox.ini | 0 29 files changed, 9 insertions(+), 9 deletions(-) rename {optional-container-engine => optional-kubernetes-engine}/.dockerignore (100%) rename {optional-container-engine => optional-kubernetes-engine}/Dockerfile (100%) rename {optional-container-engine => optional-kubernetes-engine}/Makefile (100%) rename {optional-container-engine => optional-kubernetes-engine}/README.md (87%) rename {optional-container-engine => optional-kubernetes-engine}/bookshelf-frontend.yaml (96%) rename {optional-container-engine => optional-kubernetes-engine}/bookshelf-service.yaml (83%) rename {optional-container-engine => optional-kubernetes-engine}/bookshelf-worker.yaml (96%) rename {optional-container-engine => optional-kubernetes-engine}/bookshelf/__init__.py (100%) rename {optional-container-engine => optional-kubernetes-engine}/bookshelf/crud.py (100%) rename {optional-container-engine => optional-kubernetes-engine}/bookshelf/model_cloudsql.py (100%) rename {optional-container-engine => optional-kubernetes-engine}/bookshelf/model_datastore.py (100%) rename {optional-container-engine => optional-kubernetes-engine}/bookshelf/model_mongodb.py (100%) rename {optional-container-engine => optional-kubernetes-engine}/bookshelf/storage.py (100%) rename {optional-container-engine => optional-kubernetes-engine}/bookshelf/tasks.py (100%) rename {optional-container-engine => optional-kubernetes-engine}/bookshelf/templates/base.html (100%) rename {optional-container-engine => optional-kubernetes-engine}/bookshelf/templates/form.html (100%) rename {optional-container-engine => optional-kubernetes-engine}/bookshelf/templates/list.html (100%) rename {optional-container-engine => optional-kubernetes-engine}/bookshelf/templates/view.html (100%) rename {optional-container-engine => optional-kubernetes-engine}/config.py (100%) rename {optional-container-engine => optional-kubernetes-engine}/main.py (100%) rename {optional-container-engine => optional-kubernetes-engine}/procfile (100%) rename {optional-container-engine => optional-kubernetes-engine}/requirements-dev.txt (100%) rename {optional-container-engine => optional-kubernetes-engine}/requirements.txt (100%) rename {optional-container-engine => optional-kubernetes-engine}/tests/conftest.py (100%) rename {optional-container-engine => optional-kubernetes-engine}/tests/test_auth.py (100%) rename {optional-container-engine => optional-kubernetes-engine}/tests/test_crud.py (100%) rename {optional-container-engine => optional-kubernetes-engine}/tests/test_end_to_end.py (100%) rename {optional-container-engine => optional-kubernetes-engine}/tests/test_storage.py (100%) rename {optional-container-engine => optional-kubernetes-engine}/tox.ini (100%) diff --git a/optional-container-engine/.dockerignore b/optional-kubernetes-engine/.dockerignore similarity index 100% rename from optional-container-engine/.dockerignore rename to optional-kubernetes-engine/.dockerignore diff --git a/optional-container-engine/Dockerfile b/optional-kubernetes-engine/Dockerfile similarity index 100% rename from optional-container-engine/Dockerfile rename to optional-kubernetes-engine/Dockerfile diff --git a/optional-container-engine/Makefile b/optional-kubernetes-engine/Makefile similarity index 100% rename from optional-container-engine/Makefile rename to optional-kubernetes-engine/Makefile diff --git a/optional-container-engine/README.md b/optional-kubernetes-engine/README.md similarity index 87% rename from optional-container-engine/README.md rename to optional-kubernetes-engine/README.md index 7d499b0e..e3c17236 100644 --- a/optional-container-engine/README.md +++ b/optional-kubernetes-engine/README.md @@ -1,6 +1,6 @@ -# Deploy Bookshelf to Container Engine/Kubernetes +# Deploy Bookshelf to Google Kubernetes Engine -This optional tutorial will walk you through how to deploy the Bookshelf sample application to [Google Container Engine](https://cloud.google.com/container-engine/). This tutorial is also applicable to [Kubernetes](http://kubernetes.io/) outside of Container Engine, but may require additional steps for external load balancing. +This optional tutorial will walk you through how to deploy the Bookshelf sample application to [Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/). This tutorial is also applicable to [Kubernetes](http://kubernetes.io/) outside of Google Kubernetes Engine, but may require additional steps for external load balancing. ## Pre-requisites @@ -12,7 +12,7 @@ This optional tutorial will walk you through how to deploy the Bookshelf sample 4. Install the [Google Cloud SDK](https://cloud.google.com/sdk) - $ curl https://sdk.cloud.google.com | bash + $ curl https://sdk.cloud.google.com | bash $ gcloud init 5. Install [Docker](https://www.docker.com/). @@ -49,7 +49,7 @@ Modify config.py and enter your Cloud Project ID into the `PROJECT_ID` and `CLOU ## Build the bookshelf container -Before the application can be deployed to Container Engine, you will need build and push the image to [Google Container Registry](https://cloud.google.com/container-registry/). +Before the application can be deployed to Kubernetes Engine, you will need build and push the image to [Google Container Registry](https://cloud.google.com/container-registry/). docker build -t gcr.io//bookshelf . gcloud docker push gcr.io//bookshelf diff --git a/optional-container-engine/bookshelf-frontend.yaml b/optional-kubernetes-engine/bookshelf-frontend.yaml similarity index 96% rename from optional-container-engine/bookshelf-frontend.yaml rename to optional-kubernetes-engine/bookshelf-frontend.yaml index 1a6f56f0..265c8e35 100644 --- a/optional-container-engine/bookshelf-frontend.yaml +++ b/optional-kubernetes-engine/bookshelf-frontend.yaml @@ -24,7 +24,7 @@ metadata: # The bookshelf frontend replica set ensures that at least 3 # instances of the bookshelf app are running on the cluster. # For more info about Pods see: -# https://cloud.google.com/container-engine/docs/pods/ +# https://cloud.google.com/kubernetes-engine/docs/pods/ spec: replicas: 3 template: diff --git a/optional-container-engine/bookshelf-service.yaml b/optional-kubernetes-engine/bookshelf-service.yaml similarity index 83% rename from optional-container-engine/bookshelf-service.yaml rename to optional-kubernetes-engine/bookshelf-service.yaml index d6df8138..db8f1fe7 100644 --- a/optional-container-engine/bookshelf-service.yaml +++ b/optional-kubernetes-engine/bookshelf-service.yaml @@ -13,12 +13,12 @@ # limitations under the License # The bookshelf service provides a load-balancing proxy over the bookshelf -# frontend pods. By specifying the type as a 'LoadBalancer', Container Engine +# frontend pods. By specifying the type as a 'LoadBalancer', Kubernetes Engine # will create an external HTTP load balancer. # For more information about Services see: -# https://cloud.google.com/container-engine/docs/services/ +# https://cloud.google.com/kubernetes-engine/docs/services/ # For more information about external HTTP load balancing see: -# https://cloud.google.com/container-engine/docs/load-balancer +# https://cloud.google.com/kubernetes-engine/docs/load-balancer apiVersion: v1 kind: Service metadata: diff --git a/optional-container-engine/bookshelf-worker.yaml b/optional-kubernetes-engine/bookshelf-worker.yaml similarity index 96% rename from optional-container-engine/bookshelf-worker.yaml rename to optional-kubernetes-engine/bookshelf-worker.yaml index 1c30d650..c30df89b 100644 --- a/optional-container-engine/bookshelf-worker.yaml +++ b/optional-kubernetes-engine/bookshelf-worker.yaml @@ -24,7 +24,7 @@ metadata: # The bookshelf worker replica set ensures that at least 2 instances of the # bookshelf worker pod are running on the cluster. # For more info about Pods see: -# https://cloud.google.com/container-engine/docs/pods/ +# https://cloud.google.com/kubernetes-engine/docs/pods/ spec: replicas: 2 template: diff --git a/optional-container-engine/bookshelf/__init__.py b/optional-kubernetes-engine/bookshelf/__init__.py similarity index 100% rename from optional-container-engine/bookshelf/__init__.py rename to optional-kubernetes-engine/bookshelf/__init__.py diff --git a/optional-container-engine/bookshelf/crud.py b/optional-kubernetes-engine/bookshelf/crud.py similarity index 100% rename from optional-container-engine/bookshelf/crud.py rename to optional-kubernetes-engine/bookshelf/crud.py diff --git a/optional-container-engine/bookshelf/model_cloudsql.py b/optional-kubernetes-engine/bookshelf/model_cloudsql.py similarity index 100% rename from optional-container-engine/bookshelf/model_cloudsql.py rename to optional-kubernetes-engine/bookshelf/model_cloudsql.py diff --git a/optional-container-engine/bookshelf/model_datastore.py b/optional-kubernetes-engine/bookshelf/model_datastore.py similarity index 100% rename from optional-container-engine/bookshelf/model_datastore.py rename to optional-kubernetes-engine/bookshelf/model_datastore.py diff --git a/optional-container-engine/bookshelf/model_mongodb.py b/optional-kubernetes-engine/bookshelf/model_mongodb.py similarity index 100% rename from optional-container-engine/bookshelf/model_mongodb.py rename to optional-kubernetes-engine/bookshelf/model_mongodb.py diff --git a/optional-container-engine/bookshelf/storage.py b/optional-kubernetes-engine/bookshelf/storage.py similarity index 100% rename from optional-container-engine/bookshelf/storage.py rename to optional-kubernetes-engine/bookshelf/storage.py diff --git a/optional-container-engine/bookshelf/tasks.py b/optional-kubernetes-engine/bookshelf/tasks.py similarity index 100% rename from optional-container-engine/bookshelf/tasks.py rename to optional-kubernetes-engine/bookshelf/tasks.py diff --git a/optional-container-engine/bookshelf/templates/base.html b/optional-kubernetes-engine/bookshelf/templates/base.html similarity index 100% rename from optional-container-engine/bookshelf/templates/base.html rename to optional-kubernetes-engine/bookshelf/templates/base.html diff --git a/optional-container-engine/bookshelf/templates/form.html b/optional-kubernetes-engine/bookshelf/templates/form.html similarity index 100% rename from optional-container-engine/bookshelf/templates/form.html rename to optional-kubernetes-engine/bookshelf/templates/form.html diff --git a/optional-container-engine/bookshelf/templates/list.html b/optional-kubernetes-engine/bookshelf/templates/list.html similarity index 100% rename from optional-container-engine/bookshelf/templates/list.html rename to optional-kubernetes-engine/bookshelf/templates/list.html diff --git a/optional-container-engine/bookshelf/templates/view.html b/optional-kubernetes-engine/bookshelf/templates/view.html similarity index 100% rename from optional-container-engine/bookshelf/templates/view.html rename to optional-kubernetes-engine/bookshelf/templates/view.html diff --git a/optional-container-engine/config.py b/optional-kubernetes-engine/config.py similarity index 100% rename from optional-container-engine/config.py rename to optional-kubernetes-engine/config.py diff --git a/optional-container-engine/main.py b/optional-kubernetes-engine/main.py similarity index 100% rename from optional-container-engine/main.py rename to optional-kubernetes-engine/main.py diff --git a/optional-container-engine/procfile b/optional-kubernetes-engine/procfile similarity index 100% rename from optional-container-engine/procfile rename to optional-kubernetes-engine/procfile diff --git a/optional-container-engine/requirements-dev.txt b/optional-kubernetes-engine/requirements-dev.txt similarity index 100% rename from optional-container-engine/requirements-dev.txt rename to optional-kubernetes-engine/requirements-dev.txt diff --git a/optional-container-engine/requirements.txt b/optional-kubernetes-engine/requirements.txt similarity index 100% rename from optional-container-engine/requirements.txt rename to optional-kubernetes-engine/requirements.txt diff --git a/optional-container-engine/tests/conftest.py b/optional-kubernetes-engine/tests/conftest.py similarity index 100% rename from optional-container-engine/tests/conftest.py rename to optional-kubernetes-engine/tests/conftest.py diff --git a/optional-container-engine/tests/test_auth.py b/optional-kubernetes-engine/tests/test_auth.py similarity index 100% rename from optional-container-engine/tests/test_auth.py rename to optional-kubernetes-engine/tests/test_auth.py diff --git a/optional-container-engine/tests/test_crud.py b/optional-kubernetes-engine/tests/test_crud.py similarity index 100% rename from optional-container-engine/tests/test_crud.py rename to optional-kubernetes-engine/tests/test_crud.py diff --git a/optional-container-engine/tests/test_end_to_end.py b/optional-kubernetes-engine/tests/test_end_to_end.py similarity index 100% rename from optional-container-engine/tests/test_end_to_end.py rename to optional-kubernetes-engine/tests/test_end_to_end.py diff --git a/optional-container-engine/tests/test_storage.py b/optional-kubernetes-engine/tests/test_storage.py similarity index 100% rename from optional-container-engine/tests/test_storage.py rename to optional-kubernetes-engine/tests/test_storage.py diff --git a/optional-container-engine/tox.ini b/optional-kubernetes-engine/tox.ini similarity index 100% rename from optional-container-engine/tox.ini rename to optional-kubernetes-engine/tox.ini From 4b62f2f9dece68ea7da1e8d2cfc7d3baa6e29678 Mon Sep 17 00:00:00 2001 From: Jon Wayne Parrott Date: Thu, 7 Dec 2017 13:09:02 -0800 Subject: [PATCH 002/395] Don't use broad google-cloud package; update tests to use Python 3.6 --- 2-structured-data/requirements-dev.txt | 2 +- 2-structured-data/requirements.txt | 5 ++--- 2-structured-data/tox.ini | 6 +----- 3-binary-data/requirements-dev.txt | 2 +- 3-binary-data/requirements.txt | 5 +++-- 3-binary-data/tox.ini | 6 +----- 4-auth/requirements-dev.txt | 2 +- 4-auth/requirements.txt | 5 +++-- 4-auth/tox.ini | 8 ++------ 5-logging/requirements-dev.txt | 2 +- 5-logging/requirements.txt | 5 +++-- 5-logging/tox.ini | 8 ++------ 6-pubsub/requirements-dev.txt | 2 +- 6-pubsub/requirements.txt | 7 ++++--- 6-pubsub/tox.ini | 9 +++------ 7-gce/requirements-dev.txt | 2 +- 7-gce/requirements.txt | 7 ++++--- 7-gce/tox.ini | 9 +++------ optional-kubernetes-engine/requirements-dev.txt | 2 +- optional-kubernetes-engine/requirements.txt | 7 ++++--- optional-kubernetes-engine/tox.ini | 9 +++------ 21 files changed, 45 insertions(+), 65 deletions(-) diff --git a/2-structured-data/requirements-dev.txt b/2-structured-data/requirements-dev.txt index 9e426e45..f0c2c7db 100644 --- a/2-structured-data/requirements-dev.txt +++ b/2-structured-data/requirements-dev.txt @@ -1,6 +1,6 @@ tox==2.9.1 flake8==3.5.0 flaky==3.4.0 -pytest==3.2.3 +pytest==3.3.1 pytest-cov==2.5.1 retrying==1.3.3 diff --git a/2-structured-data/requirements.txt b/2-structured-data/requirements.txt index 65313ba1..58094a71 100644 --- a/2-structured-data/requirements.txt +++ b/2-structured-data/requirements.txt @@ -1,9 +1,8 @@ Flask==0.12.2 -google-cloud==0.27.0 +google-cloud-datastore==1.4.0 gunicorn==19.7.1 -oauth2client==4.1.2 Flask-SQLAlchemy==2.3.2 PyMySQL==0.7.11 Flask-PyMongo==0.5.1 -PyMongo==3.5.1 +PyMongo==3.6.0 six==1.11.0 diff --git a/2-structured-data/tox.ini b/2-structured-data/tox.ini index 9c9bf102..40c86660 100644 --- a/2-structured-data/tox.ini +++ b/2-structured-data/tox.ini @@ -1,9 +1,8 @@ [tox] skipsdist = True -envlist = lint,py27,py34 +envlist = lint,py27,py36 [testenv] -basepython = python2.7 deps = -rrequirements.txt -rrequirements-dev.txt @@ -12,9 +11,6 @@ commands = passenv = GOOGLE_APPLICATION_CREDENTIALS DATASTORE_HOST setenv = PYTHONPATH={toxinidir} -[testenv:py34] -basepython = python3.4 - [testenv:lint] deps = flake8 diff --git a/3-binary-data/requirements-dev.txt b/3-binary-data/requirements-dev.txt index 9e426e45..f0c2c7db 100644 --- a/3-binary-data/requirements-dev.txt +++ b/3-binary-data/requirements-dev.txt @@ -1,6 +1,6 @@ tox==2.9.1 flake8==3.5.0 flaky==3.4.0 -pytest==3.2.3 +pytest==3.3.1 pytest-cov==2.5.1 retrying==1.3.3 diff --git a/3-binary-data/requirements.txt b/3-binary-data/requirements.txt index 65313ba1..6d8e5102 100644 --- a/3-binary-data/requirements.txt +++ b/3-binary-data/requirements.txt @@ -1,9 +1,10 @@ Flask==0.12.2 -google-cloud==0.27.0 +google-cloud-datastore==1.4.0 +google-cloud-storage==1.6.0 gunicorn==19.7.1 oauth2client==4.1.2 Flask-SQLAlchemy==2.3.2 PyMySQL==0.7.11 Flask-PyMongo==0.5.1 -PyMongo==3.5.1 +PyMongo==3.6.0 six==1.11.0 diff --git a/3-binary-data/tox.ini b/3-binary-data/tox.ini index 9c9bf102..40c86660 100644 --- a/3-binary-data/tox.ini +++ b/3-binary-data/tox.ini @@ -1,9 +1,8 @@ [tox] skipsdist = True -envlist = lint,py27,py34 +envlist = lint,py27,py36 [testenv] -basepython = python2.7 deps = -rrequirements.txt -rrequirements-dev.txt @@ -12,9 +11,6 @@ commands = passenv = GOOGLE_APPLICATION_CREDENTIALS DATASTORE_HOST setenv = PYTHONPATH={toxinidir} -[testenv:py34] -basepython = python3.4 - [testenv:lint] deps = flake8 diff --git a/4-auth/requirements-dev.txt b/4-auth/requirements-dev.txt index 1b4cd065..21390658 100644 --- a/4-auth/requirements-dev.txt +++ b/4-auth/requirements-dev.txt @@ -1,7 +1,7 @@ tox==2.9.1 flake8==3.5.0 flaky==3.4.0 -pytest==3.2.3 +pytest==3.3.1 pytest-cov==2.5.1 retrying==1.3.3 mock==2.0.0 diff --git a/4-auth/requirements.txt b/4-auth/requirements.txt index 65313ba1..6d8e5102 100644 --- a/4-auth/requirements.txt +++ b/4-auth/requirements.txt @@ -1,9 +1,10 @@ Flask==0.12.2 -google-cloud==0.27.0 +google-cloud-datastore==1.4.0 +google-cloud-storage==1.6.0 gunicorn==19.7.1 oauth2client==4.1.2 Flask-SQLAlchemy==2.3.2 PyMySQL==0.7.11 Flask-PyMongo==0.5.1 -PyMongo==3.5.1 +PyMongo==3.6.0 six==1.11.0 diff --git a/4-auth/tox.ini b/4-auth/tox.ini index 680033e1..40c86660 100644 --- a/4-auth/tox.ini +++ b/4-auth/tox.ini @@ -1,20 +1,16 @@ [tox] skipsdist = True -envlist = lint,py27,py34 +envlist = lint,py27,py36 [testenv] -basepython = python2.7 deps = -rrequirements.txt -rrequirements-dev.txt commands = - py.test --cov=bookshelf --no-success-flaky-report {posargs:tests} + py.test --cov=bookshelf --no-success-flaky-report {posargs} tests passenv = GOOGLE_APPLICATION_CREDENTIALS DATASTORE_HOST setenv = PYTHONPATH={toxinidir} -[testenv:py34] -basepython = python3.4 - [testenv:lint] deps = flake8 diff --git a/5-logging/requirements-dev.txt b/5-logging/requirements-dev.txt index 1b4cd065..21390658 100644 --- a/5-logging/requirements-dev.txt +++ b/5-logging/requirements-dev.txt @@ -1,7 +1,7 @@ tox==2.9.1 flake8==3.5.0 flaky==3.4.0 -pytest==3.2.3 +pytest==3.3.1 pytest-cov==2.5.1 retrying==1.3.3 mock==2.0.0 diff --git a/5-logging/requirements.txt b/5-logging/requirements.txt index 65313ba1..6d8e5102 100644 --- a/5-logging/requirements.txt +++ b/5-logging/requirements.txt @@ -1,9 +1,10 @@ Flask==0.12.2 -google-cloud==0.27.0 +google-cloud-datastore==1.4.0 +google-cloud-storage==1.6.0 gunicorn==19.7.1 oauth2client==4.1.2 Flask-SQLAlchemy==2.3.2 PyMySQL==0.7.11 Flask-PyMongo==0.5.1 -PyMongo==3.5.1 +PyMongo==3.6.0 six==1.11.0 diff --git a/5-logging/tox.ini b/5-logging/tox.ini index 680033e1..40c86660 100644 --- a/5-logging/tox.ini +++ b/5-logging/tox.ini @@ -1,20 +1,16 @@ [tox] skipsdist = True -envlist = lint,py27,py34 +envlist = lint,py27,py36 [testenv] -basepython = python2.7 deps = -rrequirements.txt -rrequirements-dev.txt commands = - py.test --cov=bookshelf --no-success-flaky-report {posargs:tests} + py.test --cov=bookshelf --no-success-flaky-report {posargs} tests passenv = GOOGLE_APPLICATION_CREDENTIALS DATASTORE_HOST setenv = PYTHONPATH={toxinidir} -[testenv:py34] -basepython = python3.4 - [testenv:lint] deps = flake8 diff --git a/6-pubsub/requirements-dev.txt b/6-pubsub/requirements-dev.txt index a9438ff4..018b1978 100644 --- a/6-pubsub/requirements-dev.txt +++ b/6-pubsub/requirements-dev.txt @@ -2,7 +2,7 @@ tox==2.9.1 flake8==3.5.0 flaky==3.4.0 mock==2.0.0 -pytest==3.2.3 +pytest==3.3.1 pytest-cov==2.5.1 BeautifulSoup4==4.6.0 requests==2.18.4 diff --git a/6-pubsub/requirements.txt b/6-pubsub/requirements.txt index c07544f3..9134fe60 100644 --- a/6-pubsub/requirements.txt +++ b/6-pubsub/requirements.txt @@ -1,12 +1,13 @@ Flask==0.12.2 -google-cloud==0.27.0 +google-cloud-datastore==1.4.0 +google-cloud-storage==1.6.0 gunicorn==19.7.1 oauth2client==4.1.2 Flask-SQLAlchemy==2.3.2 PyMySQL==0.7.11 Flask-PyMongo==0.5.1 -PyMongo==3.5.1 +PyMongo==3.6.0 six==1.11.0 requests[security]==2.18.4 honcho==1.0.1 -psq==0.5.0 +psq==0.6.0 diff --git a/6-pubsub/tox.ini b/6-pubsub/tox.ini index 430b03fa..8a04f077 100644 --- a/6-pubsub/tox.ini +++ b/6-pubsub/tox.ini @@ -1,9 +1,8 @@ [tox] skipsdist = True -envlist = lint,py27,py34 +envlist = lint,py27,py36 [testenv] -basepython = python2.7 deps = -rrequirements.txt -rrequirements-dev.txt @@ -12,16 +11,14 @@ commands = passenv = GOOGLE_APPLICATION_CREDENTIALS DATASTORE_HOST E2E_URL setenv = PYTHONPATH={toxinidir} -[testenv:py34] -basepython = python3.4 [testenv:py27-e2e] basepython = python2.7 commands = py.test --no-success-flaky-report -m "e2e" {posargs: tests} -[testenv:py34-e2e] -basepython = python2.7 +[testenv:py36-e2e] +basepython = python3.6 commands = py.test --no-success-flaky-report -m "e2e" {posargs: tests} diff --git a/7-gce/requirements-dev.txt b/7-gce/requirements-dev.txt index a9438ff4..018b1978 100644 --- a/7-gce/requirements-dev.txt +++ b/7-gce/requirements-dev.txt @@ -2,7 +2,7 @@ tox==2.9.1 flake8==3.5.0 flaky==3.4.0 mock==2.0.0 -pytest==3.2.3 +pytest==3.3.1 pytest-cov==2.5.1 BeautifulSoup4==4.6.0 requests==2.18.4 diff --git a/7-gce/requirements.txt b/7-gce/requirements.txt index c07544f3..9134fe60 100644 --- a/7-gce/requirements.txt +++ b/7-gce/requirements.txt @@ -1,12 +1,13 @@ Flask==0.12.2 -google-cloud==0.27.0 +google-cloud-datastore==1.4.0 +google-cloud-storage==1.6.0 gunicorn==19.7.1 oauth2client==4.1.2 Flask-SQLAlchemy==2.3.2 PyMySQL==0.7.11 Flask-PyMongo==0.5.1 -PyMongo==3.5.1 +PyMongo==3.6.0 six==1.11.0 requests[security]==2.18.4 honcho==1.0.1 -psq==0.5.0 +psq==0.6.0 diff --git a/7-gce/tox.ini b/7-gce/tox.ini index 430b03fa..8a04f077 100644 --- a/7-gce/tox.ini +++ b/7-gce/tox.ini @@ -1,9 +1,8 @@ [tox] skipsdist = True -envlist = lint,py27,py34 +envlist = lint,py27,py36 [testenv] -basepython = python2.7 deps = -rrequirements.txt -rrequirements-dev.txt @@ -12,16 +11,14 @@ commands = passenv = GOOGLE_APPLICATION_CREDENTIALS DATASTORE_HOST E2E_URL setenv = PYTHONPATH={toxinidir} -[testenv:py34] -basepython = python3.4 [testenv:py27-e2e] basepython = python2.7 commands = py.test --no-success-flaky-report -m "e2e" {posargs: tests} -[testenv:py34-e2e] -basepython = python2.7 +[testenv:py36-e2e] +basepython = python3.6 commands = py.test --no-success-flaky-report -m "e2e" {posargs: tests} diff --git a/optional-kubernetes-engine/requirements-dev.txt b/optional-kubernetes-engine/requirements-dev.txt index e124a4cc..db053830 100644 --- a/optional-kubernetes-engine/requirements-dev.txt +++ b/optional-kubernetes-engine/requirements-dev.txt @@ -1,7 +1,7 @@ tox==2.9.1 flake8==3.5.0 flaky==3.4.0 -pytest==3.2.3 +pytest==3.3.1 pytest-cov==2.5.1 BeautifulSoup4==4.6.0 requests==2.18.4 diff --git a/optional-kubernetes-engine/requirements.txt b/optional-kubernetes-engine/requirements.txt index 0c6e7be4..cb4fbd50 100644 --- a/optional-kubernetes-engine/requirements.txt +++ b/optional-kubernetes-engine/requirements.txt @@ -1,13 +1,14 @@ Flask==0.12.2 -google-cloud==0.27.0 +google-cloud-datastore==1.4.0 +google-cloud-storage==1.6.0 gunicorn==19.7.1 oauth2client==4.1.2 mock==2.0.0 Flask-SQLAlchemy==2.3.2 PyMySQL==0.7.11 Flask-PyMongo==0.5.1 -PyMongo==3.5.1 +PyMongo==3.6.0 six==1.11.0 requests[security]==2.18.4 honcho==1.0.1 -psq==0.5.0 +psq==0.6.0 diff --git a/optional-kubernetes-engine/tox.ini b/optional-kubernetes-engine/tox.ini index 430b03fa..8a04f077 100644 --- a/optional-kubernetes-engine/tox.ini +++ b/optional-kubernetes-engine/tox.ini @@ -1,9 +1,8 @@ [tox] skipsdist = True -envlist = lint,py27,py34 +envlist = lint,py27,py36 [testenv] -basepython = python2.7 deps = -rrequirements.txt -rrequirements-dev.txt @@ -12,16 +11,14 @@ commands = passenv = GOOGLE_APPLICATION_CREDENTIALS DATASTORE_HOST E2E_URL setenv = PYTHONPATH={toxinidir} -[testenv:py34] -basepython = python3.4 [testenv:py27-e2e] basepython = python2.7 commands = py.test --no-success-flaky-report -m "e2e" {posargs: tests} -[testenv:py34-e2e] -basepython = python2.7 +[testenv:py36-e2e] +basepython = python3.6 commands = py.test --no-success-flaky-report -m "e2e" {posargs: tests} From b39751f256eb289c1b5025fc781f7fd3650540a2 Mon Sep 17 00:00:00 2001 From: Jon Wayne Parrott Date: Thu, 7 Dec 2017 13:09:50 -0800 Subject: [PATCH 003/395] Fix bad config --- 3-binary-data/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/3-binary-data/config.py b/3-binary-data/config.py index 9deba49d..7d397967 100644 --- a/3-binary-data/config.py +++ b/3-binary-data/config.py @@ -32,7 +32,7 @@ # Google Cloud Project ID. This can be found on the 'Overview' page at # https://console.developers.google.com -PROJECT_ID = 'python-docs-samples-tests' +PROJECT_ID = 'your-project-id' # CloudSQL & SQLAlchemy configuration # Replace the following values the respective values of your Cloud SQL @@ -90,6 +90,6 @@ # # You can adjust the max content length and allow extensions settings to allow # larger or more varied file types if desired. -CLOUD_STORAGE_BUCKET = 'python-docs-samples-tests' +CLOUD_STORAGE_BUCKET = 'your-bucket-name' MAX_CONTENT_LENGTH = 8 * 1024 * 1024 ALLOWED_EXTENSIONS = set(['png', 'jpg', 'jpeg', 'gif']) From be0ef080204830cb33421c1829552be4a27a3379 Mon Sep 17 00:00:00 2001 From: Jon Wayne Parrott Date: Thu, 7 Dec 2017 13:21:03 -0800 Subject: [PATCH 004/395] Update 6-pubsub to use latest psq --- 6-pubsub/bookshelf/tasks.py | 10 +++++++--- 6-pubsub/requirements.txt | 2 ++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/6-pubsub/bookshelf/tasks.py b/6-pubsub/bookshelf/tasks.py index cf5b8d5d..c162e5b3 100644 --- a/6-pubsub/bookshelf/tasks.py +++ b/6-pubsub/bookshelf/tasks.py @@ -21,17 +21,21 @@ import requests +publisher_client = pubsub.PublisherClient() +subscriber_client = pubsub.SubscriberClient() + + # [START get_books_queue] def get_books_queue(): - client = pubsub.Client( - project=current_app.config['PROJECT_ID']) + project = current_app.config['PROJECT_ID'] # Create a queue specifically for processing books and pass in the # Flask application context. This ensures that tasks will have access # to any extensions / configuration specified to the app, such as # models. return psq.Queue( - client, 'books', extra_context=current_app.app_context) + publisher_client, subscriber_client, project, + 'books', extra_context=current_app.app_context) # [END get_books_queue] diff --git a/6-pubsub/requirements.txt b/6-pubsub/requirements.txt index 9134fe60..65d50a58 100644 --- a/6-pubsub/requirements.txt +++ b/6-pubsub/requirements.txt @@ -1,6 +1,8 @@ Flask==0.12.2 google-cloud-datastore==1.4.0 google-cloud-storage==1.6.0 +google-cloud-logging==1.4.0 +google-cloud-error_reporting==0.28.0 gunicorn==19.7.1 oauth2client==4.1.2 Flask-SQLAlchemy==2.3.2 From b4556af0b2fbbd54ee751f4eb81bcbd7be2990ba Mon Sep 17 00:00:00 2001 From: Jon Wayne Parrott Date: Thu, 7 Dec 2017 13:24:06 -0800 Subject: [PATCH 005/395] Fix logging requirements --- 5-logging/requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/5-logging/requirements.txt b/5-logging/requirements.txt index 6d8e5102..76947c53 100644 --- a/5-logging/requirements.txt +++ b/5-logging/requirements.txt @@ -1,6 +1,8 @@ Flask==0.12.2 google-cloud-datastore==1.4.0 google-cloud-storage==1.6.0 +google-cloud-logging==1.4.0 +google-cloud-error_reporting==0.28.0 gunicorn==19.7.1 oauth2client==4.1.2 Flask-SQLAlchemy==2.3.2 From 5a7a526d75b67ff4e1f861f9c324cd3df18418ad Mon Sep 17 00:00:00 2001 From: Jon Wayne Parrott Date: Thu, 7 Dec 2017 13:27:33 -0800 Subject: [PATCH 006/395] Update 7-gce to use new psq --- 7-gce/bookshelf/tasks.py | 10 +++++++--- 7-gce/requirements.txt | 2 ++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/7-gce/bookshelf/tasks.py b/7-gce/bookshelf/tasks.py index e6ca652f..c2fc5a84 100644 --- a/7-gce/bookshelf/tasks.py +++ b/7-gce/bookshelf/tasks.py @@ -21,16 +21,20 @@ import requests +publisher_client = pubsub.PublisherClient() +subscriber_client = pubsub.SubscriberClient() + + def get_books_queue(): - client = pubsub.Client( - project=current_app.config['PROJECT_ID']) + project = current_app.config['PROJECT_ID'] # Create a queue specifically for processing books and pass in the # Flask application context. This ensures that tasks will have access # to any extensions / configuration specified to the app, such as # models. return psq.Queue( - client, 'books', extra_context=current_app.app_context) + publisher_client, subscriber_client, project, + 'books', extra_context=current_app.app_context) def process_book(book_id): diff --git a/7-gce/requirements.txt b/7-gce/requirements.txt index 9134fe60..65d50a58 100644 --- a/7-gce/requirements.txt +++ b/7-gce/requirements.txt @@ -1,6 +1,8 @@ Flask==0.12.2 google-cloud-datastore==1.4.0 google-cloud-storage==1.6.0 +google-cloud-logging==1.4.0 +google-cloud-error_reporting==0.28.0 gunicorn==19.7.1 oauth2client==4.1.2 Flask-SQLAlchemy==2.3.2 From 07d2f10cfe7866a6650744515e8a1cb664bf31f6 Mon Sep 17 00:00:00 2001 From: Jon Wayne Parrott Date: Thu, 7 Dec 2017 13:28:31 -0800 Subject: [PATCH 007/395] Update optional-kubernetes-engine to use new psq --- optional-kubernetes-engine/bookshelf/tasks.py | 10 +++++++--- optional-kubernetes-engine/requirements.txt | 2 ++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/optional-kubernetes-engine/bookshelf/tasks.py b/optional-kubernetes-engine/bookshelf/tasks.py index e6ca652f..c2fc5a84 100644 --- a/optional-kubernetes-engine/bookshelf/tasks.py +++ b/optional-kubernetes-engine/bookshelf/tasks.py @@ -21,16 +21,20 @@ import requests +publisher_client = pubsub.PublisherClient() +subscriber_client = pubsub.SubscriberClient() + + def get_books_queue(): - client = pubsub.Client( - project=current_app.config['PROJECT_ID']) + project = current_app.config['PROJECT_ID'] # Create a queue specifically for processing books and pass in the # Flask application context. This ensures that tasks will have access # to any extensions / configuration specified to the app, such as # models. return psq.Queue( - client, 'books', extra_context=current_app.app_context) + publisher_client, subscriber_client, project, + 'books', extra_context=current_app.app_context) def process_book(book_id): diff --git a/optional-kubernetes-engine/requirements.txt b/optional-kubernetes-engine/requirements.txt index cb4fbd50..0d1eb6ae 100644 --- a/optional-kubernetes-engine/requirements.txt +++ b/optional-kubernetes-engine/requirements.txt @@ -1,6 +1,8 @@ Flask==0.12.2 google-cloud-datastore==1.4.0 google-cloud-storage==1.6.0 +google-cloud-logging==1.4.0 +google-cloud-error_reporting==0.28.0 gunicorn==19.7.1 oauth2client==4.1.2 mock==2.0.0 From fc301949334b132ee7d33a9ba348adbe3938948a Mon Sep 17 00:00:00 2001 From: Jon Wayne Parrott Date: Thu, 7 Dec 2017 13:39:29 -0800 Subject: [PATCH 008/395] Fix lint --- 4-auth/bookshelf/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/4-auth/bookshelf/__init__.py b/4-auth/bookshelf/__init__.py index 15d738c4..b245f2ba 100644 --- a/4-auth/bookshelf/__init__.py +++ b/4-auth/bookshelf/__init__.py @@ -17,10 +17,10 @@ from flask import current_app, Flask, redirect, request, session, url_for import httplib2 - # [START include] from oauth2client.contrib.flask_util import UserOAuth2 + oauth2 = UserOAuth2() # [END include] From 500657dea929163aafc6e97a4480adb038c4f8f6 Mon Sep 17 00:00:00 2001 From: Jon Wayne Parrott Date: Fri, 8 Dec 2017 10:22:52 -0800 Subject: [PATCH 009/395] Adjust code include comments for the pubsub clients --- 6-pubsub/bookshelf/tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/6-pubsub/bookshelf/tasks.py b/6-pubsub/bookshelf/tasks.py index c162e5b3..ad0a583f 100644 --- a/6-pubsub/bookshelf/tasks.py +++ b/6-pubsub/bookshelf/tasks.py @@ -21,11 +21,11 @@ import requests +# [START get_books_queue] publisher_client = pubsub.PublisherClient() subscriber_client = pubsub.SubscriberClient() -# [START get_books_queue] def get_books_queue(): project = current_app.config['PROJECT_ID'] From 2efd97e8ad9f40076d39b3dd3c27686838bc0a91 Mon Sep 17 00:00:00 2001 From: michaelawyu Date: Wed, 13 Dec 2017 17:30:14 -0800 Subject: [PATCH 010/395] Update Dockerfile Reported in b/70627152 --- optional-kubernetes-engine/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/optional-kubernetes-engine/Dockerfile b/optional-kubernetes-engine/Dockerfile index a1f75dc7..69f9d260 100644 --- a/optional-kubernetes-engine/Dockerfile +++ b/optional-kubernetes-engine/Dockerfile @@ -2,7 +2,7 @@ # and various os-level packages to allow installation of popular Python # libraries. The source is on github at: # https://github.com/GoogleCloudPlatform/python-docker -FROM gcr.io/google_appengine/python +FROM gcr.io/google-appengine/python # Create a virtualenv for the application dependencies. # If you want to use Python 2, add the -p python2.7 flag. From 3fd10567047777fd576ab9c7c93b56f2f93d1732 Mon Sep 17 00:00:00 2001 From: DPE bot Date: Tue, 2 Jan 2018 10:48:10 -0800 Subject: [PATCH 011/395] Auto-update dependencies. (#152) --- 2-structured-data/requirements.txt | 2 +- 3-binary-data/requirements.txt | 2 +- 4-auth/requirements.txt | 2 +- 5-logging/requirements.txt | 2 +- 6-pubsub/requirements.txt | 2 +- 7-gce/requirements.txt | 2 +- optional-kubernetes-engine/requirements.txt | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/2-structured-data/requirements.txt b/2-structured-data/requirements.txt index 58094a71..b45693f2 100644 --- a/2-structured-data/requirements.txt +++ b/2-structured-data/requirements.txt @@ -2,7 +2,7 @@ Flask==0.12.2 google-cloud-datastore==1.4.0 gunicorn==19.7.1 Flask-SQLAlchemy==2.3.2 -PyMySQL==0.7.11 +PyMySQL==0.8.0 Flask-PyMongo==0.5.1 PyMongo==3.6.0 six==1.11.0 diff --git a/3-binary-data/requirements.txt b/3-binary-data/requirements.txt index 6d8e5102..42a3b84f 100644 --- a/3-binary-data/requirements.txt +++ b/3-binary-data/requirements.txt @@ -4,7 +4,7 @@ google-cloud-storage==1.6.0 gunicorn==19.7.1 oauth2client==4.1.2 Flask-SQLAlchemy==2.3.2 -PyMySQL==0.7.11 +PyMySQL==0.8.0 Flask-PyMongo==0.5.1 PyMongo==3.6.0 six==1.11.0 diff --git a/4-auth/requirements.txt b/4-auth/requirements.txt index 6d8e5102..42a3b84f 100644 --- a/4-auth/requirements.txt +++ b/4-auth/requirements.txt @@ -4,7 +4,7 @@ google-cloud-storage==1.6.0 gunicorn==19.7.1 oauth2client==4.1.2 Flask-SQLAlchemy==2.3.2 -PyMySQL==0.7.11 +PyMySQL==0.8.0 Flask-PyMongo==0.5.1 PyMongo==3.6.0 six==1.11.0 diff --git a/5-logging/requirements.txt b/5-logging/requirements.txt index 76947c53..0a9541da 100644 --- a/5-logging/requirements.txt +++ b/5-logging/requirements.txt @@ -6,7 +6,7 @@ google-cloud-error_reporting==0.28.0 gunicorn==19.7.1 oauth2client==4.1.2 Flask-SQLAlchemy==2.3.2 -PyMySQL==0.7.11 +PyMySQL==0.8.0 Flask-PyMongo==0.5.1 PyMongo==3.6.0 six==1.11.0 diff --git a/6-pubsub/requirements.txt b/6-pubsub/requirements.txt index 65d50a58..f65f19ec 100644 --- a/6-pubsub/requirements.txt +++ b/6-pubsub/requirements.txt @@ -6,7 +6,7 @@ google-cloud-error_reporting==0.28.0 gunicorn==19.7.1 oauth2client==4.1.2 Flask-SQLAlchemy==2.3.2 -PyMySQL==0.7.11 +PyMySQL==0.8.0 Flask-PyMongo==0.5.1 PyMongo==3.6.0 six==1.11.0 diff --git a/7-gce/requirements.txt b/7-gce/requirements.txt index 65d50a58..f65f19ec 100644 --- a/7-gce/requirements.txt +++ b/7-gce/requirements.txt @@ -6,7 +6,7 @@ google-cloud-error_reporting==0.28.0 gunicorn==19.7.1 oauth2client==4.1.2 Flask-SQLAlchemy==2.3.2 -PyMySQL==0.7.11 +PyMySQL==0.8.0 Flask-PyMongo==0.5.1 PyMongo==3.6.0 six==1.11.0 diff --git a/optional-kubernetes-engine/requirements.txt b/optional-kubernetes-engine/requirements.txt index 0d1eb6ae..5fe48629 100644 --- a/optional-kubernetes-engine/requirements.txt +++ b/optional-kubernetes-engine/requirements.txt @@ -7,7 +7,7 @@ gunicorn==19.7.1 oauth2client==4.1.2 mock==2.0.0 Flask-SQLAlchemy==2.3.2 -PyMySQL==0.7.11 +PyMySQL==0.8.0 Flask-PyMongo==0.5.1 PyMongo==3.6.0 six==1.11.0 From 13b14bde087b53cd9aa2481616c09284d99920ec Mon Sep 17 00:00:00 2001 From: DPE bot Date: Fri, 5 Jan 2018 10:20:41 -0800 Subject: [PATCH 012/395] Auto-update dependencies. (#153) --- 2-structured-data/requirements-dev.txt | 2 +- 3-binary-data/requirements-dev.txt | 2 +- 4-auth/requirements-dev.txt | 2 +- 5-logging/requirements-dev.txt | 2 +- 6-pubsub/requirements-dev.txt | 2 +- 7-gce/requirements-dev.txt | 2 +- optional-kubernetes-engine/requirements-dev.txt | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/2-structured-data/requirements-dev.txt b/2-structured-data/requirements-dev.txt index f0c2c7db..555825ee 100644 --- a/2-structured-data/requirements-dev.txt +++ b/2-structured-data/requirements-dev.txt @@ -1,6 +1,6 @@ tox==2.9.1 flake8==3.5.0 flaky==3.4.0 -pytest==3.3.1 +pytest==3.3.2 pytest-cov==2.5.1 retrying==1.3.3 diff --git a/3-binary-data/requirements-dev.txt b/3-binary-data/requirements-dev.txt index f0c2c7db..555825ee 100644 --- a/3-binary-data/requirements-dev.txt +++ b/3-binary-data/requirements-dev.txt @@ -1,6 +1,6 @@ tox==2.9.1 flake8==3.5.0 flaky==3.4.0 -pytest==3.3.1 +pytest==3.3.2 pytest-cov==2.5.1 retrying==1.3.3 diff --git a/4-auth/requirements-dev.txt b/4-auth/requirements-dev.txt index 21390658..d31627dd 100644 --- a/4-auth/requirements-dev.txt +++ b/4-auth/requirements-dev.txt @@ -1,7 +1,7 @@ tox==2.9.1 flake8==3.5.0 flaky==3.4.0 -pytest==3.3.1 +pytest==3.3.2 pytest-cov==2.5.1 retrying==1.3.3 mock==2.0.0 diff --git a/5-logging/requirements-dev.txt b/5-logging/requirements-dev.txt index 21390658..d31627dd 100644 --- a/5-logging/requirements-dev.txt +++ b/5-logging/requirements-dev.txt @@ -1,7 +1,7 @@ tox==2.9.1 flake8==3.5.0 flaky==3.4.0 -pytest==3.3.1 +pytest==3.3.2 pytest-cov==2.5.1 retrying==1.3.3 mock==2.0.0 diff --git a/6-pubsub/requirements-dev.txt b/6-pubsub/requirements-dev.txt index 018b1978..3bcf70a5 100644 --- a/6-pubsub/requirements-dev.txt +++ b/6-pubsub/requirements-dev.txt @@ -2,7 +2,7 @@ tox==2.9.1 flake8==3.5.0 flaky==3.4.0 mock==2.0.0 -pytest==3.3.1 +pytest==3.3.2 pytest-cov==2.5.1 BeautifulSoup4==4.6.0 requests==2.18.4 diff --git a/7-gce/requirements-dev.txt b/7-gce/requirements-dev.txt index 018b1978..3bcf70a5 100644 --- a/7-gce/requirements-dev.txt +++ b/7-gce/requirements-dev.txt @@ -2,7 +2,7 @@ tox==2.9.1 flake8==3.5.0 flaky==3.4.0 mock==2.0.0 -pytest==3.3.1 +pytest==3.3.2 pytest-cov==2.5.1 BeautifulSoup4==4.6.0 requests==2.18.4 diff --git a/optional-kubernetes-engine/requirements-dev.txt b/optional-kubernetes-engine/requirements-dev.txt index db053830..42789c4a 100644 --- a/optional-kubernetes-engine/requirements-dev.txt +++ b/optional-kubernetes-engine/requirements-dev.txt @@ -1,7 +1,7 @@ tox==2.9.1 flake8==3.5.0 flaky==3.4.0 -pytest==3.3.1 +pytest==3.3.2 pytest-cov==2.5.1 BeautifulSoup4==4.6.0 requests==2.18.4 From 504b3d550b551502cfe96f32542c31b232135eff Mon Sep 17 00:00:00 2001 From: DPE bot Date: Thu, 18 Jan 2018 16:08:28 -0800 Subject: [PATCH 013/395] Auto-update dependencies. (#154) --- 3-binary-data/requirements.txt | 2 +- 4-auth/requirements.txt | 2 +- 5-logging/requirements.txt | 4 ++-- 6-pubsub/requirements.txt | 4 ++-- 7-gce/requirements.txt | 4 ++-- optional-kubernetes-engine/requirements.txt | 4 ++-- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/3-binary-data/requirements.txt b/3-binary-data/requirements.txt index 42a3b84f..da821968 100644 --- a/3-binary-data/requirements.txt +++ b/3-binary-data/requirements.txt @@ -1,6 +1,6 @@ Flask==0.12.2 google-cloud-datastore==1.4.0 -google-cloud-storage==1.6.0 +google-cloud-storage==1.7.0 gunicorn==19.7.1 oauth2client==4.1.2 Flask-SQLAlchemy==2.3.2 diff --git a/4-auth/requirements.txt b/4-auth/requirements.txt index 42a3b84f..da821968 100644 --- a/4-auth/requirements.txt +++ b/4-auth/requirements.txt @@ -1,6 +1,6 @@ Flask==0.12.2 google-cloud-datastore==1.4.0 -google-cloud-storage==1.6.0 +google-cloud-storage==1.7.0 gunicorn==19.7.1 oauth2client==4.1.2 Flask-SQLAlchemy==2.3.2 diff --git a/5-logging/requirements.txt b/5-logging/requirements.txt index 0a9541da..9393f9f3 100644 --- a/5-logging/requirements.txt +++ b/5-logging/requirements.txt @@ -1,8 +1,8 @@ Flask==0.12.2 google-cloud-datastore==1.4.0 -google-cloud-storage==1.6.0 +google-cloud-storage==1.7.0 google-cloud-logging==1.4.0 -google-cloud-error_reporting==0.28.0 +google-cloud-error_reporting==0.29.0 gunicorn==19.7.1 oauth2client==4.1.2 Flask-SQLAlchemy==2.3.2 diff --git a/6-pubsub/requirements.txt b/6-pubsub/requirements.txt index f65f19ec..d6bebdba 100644 --- a/6-pubsub/requirements.txt +++ b/6-pubsub/requirements.txt @@ -1,8 +1,8 @@ Flask==0.12.2 google-cloud-datastore==1.4.0 -google-cloud-storage==1.6.0 +google-cloud-storage==1.7.0 google-cloud-logging==1.4.0 -google-cloud-error_reporting==0.28.0 +google-cloud-error_reporting==0.29.0 gunicorn==19.7.1 oauth2client==4.1.2 Flask-SQLAlchemy==2.3.2 diff --git a/7-gce/requirements.txt b/7-gce/requirements.txt index f65f19ec..d6bebdba 100644 --- a/7-gce/requirements.txt +++ b/7-gce/requirements.txt @@ -1,8 +1,8 @@ Flask==0.12.2 google-cloud-datastore==1.4.0 -google-cloud-storage==1.6.0 +google-cloud-storage==1.7.0 google-cloud-logging==1.4.0 -google-cloud-error_reporting==0.28.0 +google-cloud-error_reporting==0.29.0 gunicorn==19.7.1 oauth2client==4.1.2 Flask-SQLAlchemy==2.3.2 diff --git a/optional-kubernetes-engine/requirements.txt b/optional-kubernetes-engine/requirements.txt index 5fe48629..e16663c2 100644 --- a/optional-kubernetes-engine/requirements.txt +++ b/optional-kubernetes-engine/requirements.txt @@ -1,8 +1,8 @@ Flask==0.12.2 google-cloud-datastore==1.4.0 -google-cloud-storage==1.6.0 +google-cloud-storage==1.7.0 google-cloud-logging==1.4.0 -google-cloud-error_reporting==0.28.0 +google-cloud-error_reporting==0.29.0 gunicorn==19.7.1 oauth2client==4.1.2 mock==2.0.0 From faa713b7296fdf6f677209ca7c0edbba2130c7dc Mon Sep 17 00:00:00 2001 From: Med Potter Date: Thu, 12 Jul 2018 23:26:55 -0700 Subject: [PATCH 014/395] Fixes Issue 50 --- 2-structured-data/bookshelf/model_datastore.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/2-structured-data/bookshelf/model_datastore.py b/2-structured-data/bookshelf/model_datastore.py index 54afd3c2..e33a4a68 100644 --- a/2-structured-data/bookshelf/model_datastore.py +++ b/2-structured-data/bookshelf/model_datastore.py @@ -24,7 +24,7 @@ def init_app(app): def get_client(): - return datastore.Client(current_app.config['PROJECT_ID']) + return datastore.Client() # [START from_datastore] From 095e5f78f583e5ec4a3c7b77ae66e1fbe1f839f0 Mon Sep 17 00:00:00 2001 From: Julien Phalip Date: Wed, 25 Jul 2018 20:14:25 -0700 Subject: [PATCH 015/395] Set better default value for CLOUD_STORAGE_BUCKET --- optional-kubernetes-engine/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/optional-kubernetes-engine/config.py b/optional-kubernetes-engine/config.py index f77c8c75..0fd601ed 100644 --- a/optional-kubernetes-engine/config.py +++ b/optional-kubernetes-engine/config.py @@ -87,7 +87,7 @@ # # You can adjust the max content length and allow extensions settings to allow # larger or more varied file types if desired. -CLOUD_STORAGE_BUCKET = 'your-project-id' +CLOUD_STORAGE_BUCKET = 'your-bucket-name' MAX_CONTENT_LENGTH = 8 * 1024 * 1024 ALLOWED_EXTENSIONS = set(['png', 'jpg', 'jpeg', 'gif']) From 39f5722ad069047af8f7a218bf0fbf402ce5b87c Mon Sep 17 00:00:00 2001 From: Mike Truty Date: Fri, 10 Aug 2018 16:55:57 -0500 Subject: [PATCH 016/395] Fix Python Bookshelf for GCE. --- 6-pubsub/requirements.txt | 2 +- 7-gce/bookshelf/templates/base.html | 13 ------------- 7-gce/gce/deploy.sh | 10 +++++----- 7-gce/gce/deployment_manager/config.yaml | 2 +- 7-gce/gce/startup-script.sh | 7 ++++--- 7-gce/gce/teardown.sh | 2 +- 7-gce/procfile | 2 +- 7-gce/requirements.txt | 2 +- .../bookshelf/templates/base.html | 13 ------------- optional-kubernetes-engine/requirements.txt | 2 +- 10 files changed, 15 insertions(+), 40 deletions(-) diff --git a/6-pubsub/requirements.txt b/6-pubsub/requirements.txt index d6bebdba..976f5bf6 100644 --- a/6-pubsub/requirements.txt +++ b/6-pubsub/requirements.txt @@ -12,4 +12,4 @@ PyMongo==3.6.0 six==1.11.0 requests[security]==2.18.4 honcho==1.0.1 -psq==0.6.0 +psq==0.7.0 diff --git a/7-gce/bookshelf/templates/base.html b/7-gce/bookshelf/templates/base.html index 4b843b0a..a18c7908 100644 --- a/7-gce/bookshelf/templates/base.html +++ b/7-gce/bookshelf/templates/base.html @@ -29,20 +29,7 @@ -
diff --git a/7-gce/gce/deploy.sh b/7-gce/gce/deploy.sh index 70d4ad0a..0d7482b4 100755 --- a/7-gce/gce/deploy.sh +++ b/7-gce/gce/deploy.sh @@ -20,7 +20,7 @@ ZONE=us-central1-f GROUP=frontend-group TEMPLATE=$GROUP-tmpl MACHINE_TYPE=f1-micro -IMAGE_FAMILY=debian-8 +IMAGE_FAMILY=debian-9 IMAGE_PROJECT=debian-cloud STARTUP_SCRIPT=startup-script.sh SCOPES="userinfo-email,cloud-platform" @@ -30,7 +30,7 @@ MIN_INSTANCES=1 MAX_INSTANCES=10 TARGET_UTILIZATION=0.6 -SERVICE=frontend-web-service +SERVICE=my-app-service # # Instance group setup @@ -58,7 +58,7 @@ gcloud compute instance-groups managed \ --base-instance-name $GROUP \ --size $MIN_INSTANCES \ --template $TEMPLATE \ - --zone $ZONE + --zone $ZONE # [END create_group] # [START create_named_port] @@ -140,7 +140,7 @@ gcloud compute instance-groups managed set-autoscaling \ $GROUP \ --max-num-replicas $MAX_INSTANCES \ --target-load-balancing-utilization $TARGET_UTILIZATION \ - --zone $ZONE + --zone $ZONE # [END set_autoscaling] # [START create_firewall] @@ -153,7 +153,7 @@ else --allow tcp:8080 \ --source-ranges 0.0.0.0/0 \ --target-tags http-server \ - --description "Allow port 8080 access to http-server" + --description "Allow port 8080 access to http-server" fi # [END create_firewall] diff --git a/7-gce/gce/deployment_manager/config.yaml b/7-gce/gce/deployment_manager/config.yaml index 8ae3c25f..901b55fb 100644 --- a/7-gce/gce/deployment_manager/config.yaml +++ b/7-gce/gce/deployment_manager/config.yaml @@ -23,7 +23,7 @@ resources: properties: zone: us-central1-f machine-type: n1-standard-1 - machine-image: https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/family/debian-8 + machine-image: https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/family/debian-9 min-instances: 1 max-instances: 10 target-utilization: 0.6 diff --git a/7-gce/gce/startup-script.sh b/7-gce/gce/startup-script.sh index 1f74ee2f..6f05e7ce 100644 --- a/7-gce/gce/startup-script.sh +++ b/7-gce/gce/startup-script.sh @@ -45,7 +45,8 @@ git config --global credential.helper gcloud.sh git clone https://source.developers.google.com/p/$PROJECTID/r/[YOUR_REPO_NAME] /opt/app # Install app dependencies -virtualenv /opt/app/7-gce/env +virtualenv -p python3 /opt/app/7-gce/env +source /opt/app/7-gce/env/bin/activate /opt/app/7-gce/env/bin/pip install -r /opt/app/7-gce/requirements.txt # Make sure the pythonapp user owns the application code @@ -56,13 +57,13 @@ chown -R pythonapp:pythonapp /opt/app cat >/etc/supervisor/conf.d/python-app.conf << EOF [program:pythonapp] directory=/opt/app/7-gce -command=/opt/app/7-gce/env/bin/gunicorn main:app --bind 0.0.0.0:8080 +command=/opt/app/7-gce/env/bin/honcho start -f ./procfile worker bookshelf autostart=true autorestart=true user=pythonapp # Environment variables ensure that the application runs inside of the # configured virtualenv. -environment=VIRTUAL_ENV="/opt/app/env/7-gce",PATH="/opt/app/7-gce/env/bin",\ +environment=VIRTUAL_ENV="/opt/app/7-gce/env",PATH="/opt/app/7-gce/env/bin",\ HOME="/home/pythonapp",USER="pythonapp" stdout_logfile=syslog stderr_logfile=syslog diff --git a/7-gce/gce/teardown.sh b/7-gce/gce/teardown.sh index 1ff104c4..8f123a10 100755 --- a/7-gce/gce/teardown.sh +++ b/7-gce/gce/teardown.sh @@ -20,7 +20,7 @@ gcloud config set compute/zone $ZONE GROUP=frontend-group TEMPLATE=$GROUP-tmpl -SERVICE=frontend-web-service +SERVICE=my-app-service gcloud compute instance-groups managed stop-autoscaling $GROUP --zone $ZONE diff --git a/7-gce/procfile b/7-gce/procfile index 0c4fcec5..d2b676b5 100644 --- a/7-gce/procfile +++ b/7-gce/procfile @@ -1,3 +1,3 @@ -bookshelf: gunicorn -b 0.0.0.0:$PORT main:app +bookshelf: gunicorn -b 0.0.0.0:8080 main:app worker: psqworker --pid /tmp/psq.pid main.books_queue monitor: python monitor.py /tmp/psq.pid diff --git a/7-gce/requirements.txt b/7-gce/requirements.txt index d6bebdba..976f5bf6 100644 --- a/7-gce/requirements.txt +++ b/7-gce/requirements.txt @@ -12,4 +12,4 @@ PyMongo==3.6.0 six==1.11.0 requests[security]==2.18.4 honcho==1.0.1 -psq==0.6.0 +psq==0.7.0 diff --git a/optional-kubernetes-engine/bookshelf/templates/base.html b/optional-kubernetes-engine/bookshelf/templates/base.html index 4b843b0a..a18c7908 100644 --- a/optional-kubernetes-engine/bookshelf/templates/base.html +++ b/optional-kubernetes-engine/bookshelf/templates/base.html @@ -29,20 +29,7 @@
-
diff --git a/optional-kubernetes-engine/requirements.txt b/optional-kubernetes-engine/requirements.txt index e16663c2..8539383d 100644 --- a/optional-kubernetes-engine/requirements.txt +++ b/optional-kubernetes-engine/requirements.txt @@ -13,4 +13,4 @@ PyMongo==3.6.0 six==1.11.0 requests[security]==2.18.4 honcho==1.0.1 -psq==0.6.0 +psq==0.7.0 From 3cffeae3fa051b7c2627717d23c1d72c701f0db4 Mon Sep 17 00:00:00 2001 From: Spencer Ng Date: Mon, 8 Oct 2018 13:07:20 -0400 Subject: [PATCH 017/395] Update CONTRIBUTING.md --- CONTRIBUTING.md | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6736efd9..c42ef953 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,14 +6,12 @@ We'd love to accept your sample apps and patches! Before we can take them, we have to jump a couple of legal hurdles. Please fill out either the individual or corporate Contributor License Agreement -(CLA). +(CLA): * If you are an individual writing original source code and you're sure you - own the intellectual property, then you'll need to sign an [individual CLA] - (https://developers.google.com/open-source/cla/individual). + own the intellectual property, then you'll need to sign an [individual CLA](https://developers.google.com/open-source/cla/individual). * If you work for a company that wants to allow you to contribute your work, - then you'll need to sign a [corporate CLA] - (https://developers.google.com/open-source/cla/corporate). + then you'll need to sign a [corporate CLA](https://developers.google.com/open-source/cla/corporate). Follow either of the two links above to access the appropriate CLA and instructions for how to sign and return it. Once we receive it, we'll be able to @@ -21,15 +19,14 @@ accept your pull requests. ## Contributing A Patch -1. Submit an issue describing your proposed change to the repo in question. -1. The repo owner will respond to your issue promptly. +1. Submit an issue describing your proposed change to the repository in question. +1. The repository owner will respond to your issue promptly. 1. If your proposed change is accepted, and you haven't already done so, sign a - Contributor License Agreement (see details above). -1. Fork the desired repo, develop and test your code changes. + CLA (see details above). +1. Fork the desired repo, then develop and test your code changes. 1. Ensure that your code adheres to the existing style in the sample to which - you are contributing. Refer to the - [Google Cloud Platform Samples Style Guide] - (https://github.com/GoogleCloudPlatform/Template/wiki/style.html) for the + you are contributing. Refer to the [Google Python Style Guide](https://github.com/google/styleguide/blob/gh-pages/pyguide.md) and the + [Google Cloud Platform Community Style Guide](https://cloud.google.com/community/tutorials/styleguide) for the recommended coding standards for this organization. 1. Ensure that your code has an appropriate set of unit tests which all pass. 1. Submit a pull request. From e91fca716bc16a6e7e92f4367edf5765f5b576ea Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Wed, 7 Nov 2018 12:39:14 -0800 Subject: [PATCH 018/395] Update pytest version to support latest pluggy version --- 2-structured-data/requirements-dev.txt | 2 +- 3-binary-data/requirements-dev.txt | 2 +- 4-auth/requirements-dev.txt | 2 +- 5-logging/requirements-dev.txt | 2 +- 6-pubsub/requirements-dev.txt | 2 +- 7-gce/requirements-dev.txt | 2 +- optional-kubernetes-engine/requirements-dev.txt | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/2-structured-data/requirements-dev.txt b/2-structured-data/requirements-dev.txt index 555825ee..c5f07101 100644 --- a/2-structured-data/requirements-dev.txt +++ b/2-structured-data/requirements-dev.txt @@ -1,6 +1,6 @@ tox==2.9.1 flake8==3.5.0 flaky==3.4.0 -pytest==3.3.2 +pytest==3.8.2 pytest-cov==2.5.1 retrying==1.3.3 diff --git a/3-binary-data/requirements-dev.txt b/3-binary-data/requirements-dev.txt index 555825ee..c5f07101 100644 --- a/3-binary-data/requirements-dev.txt +++ b/3-binary-data/requirements-dev.txt @@ -1,6 +1,6 @@ tox==2.9.1 flake8==3.5.0 flaky==3.4.0 -pytest==3.3.2 +pytest==3.8.2 pytest-cov==2.5.1 retrying==1.3.3 diff --git a/4-auth/requirements-dev.txt b/4-auth/requirements-dev.txt index d31627dd..8406c088 100644 --- a/4-auth/requirements-dev.txt +++ b/4-auth/requirements-dev.txt @@ -1,7 +1,7 @@ tox==2.9.1 flake8==3.5.0 flaky==3.4.0 -pytest==3.3.2 +pytest==3.8.2 pytest-cov==2.5.1 retrying==1.3.3 mock==2.0.0 diff --git a/5-logging/requirements-dev.txt b/5-logging/requirements-dev.txt index d31627dd..8406c088 100644 --- a/5-logging/requirements-dev.txt +++ b/5-logging/requirements-dev.txt @@ -1,7 +1,7 @@ tox==2.9.1 flake8==3.5.0 flaky==3.4.0 -pytest==3.3.2 +pytest==3.8.2 pytest-cov==2.5.1 retrying==1.3.3 mock==2.0.0 diff --git a/6-pubsub/requirements-dev.txt b/6-pubsub/requirements-dev.txt index 3bcf70a5..dbe05429 100644 --- a/6-pubsub/requirements-dev.txt +++ b/6-pubsub/requirements-dev.txt @@ -2,7 +2,7 @@ tox==2.9.1 flake8==3.5.0 flaky==3.4.0 mock==2.0.0 -pytest==3.3.2 +pytest==3.8.2 pytest-cov==2.5.1 BeautifulSoup4==4.6.0 requests==2.18.4 diff --git a/7-gce/requirements-dev.txt b/7-gce/requirements-dev.txt index 3bcf70a5..dbe05429 100644 --- a/7-gce/requirements-dev.txt +++ b/7-gce/requirements-dev.txt @@ -2,7 +2,7 @@ tox==2.9.1 flake8==3.5.0 flaky==3.4.0 mock==2.0.0 -pytest==3.3.2 +pytest==3.8.2 pytest-cov==2.5.1 BeautifulSoup4==4.6.0 requests==2.18.4 diff --git a/optional-kubernetes-engine/requirements-dev.txt b/optional-kubernetes-engine/requirements-dev.txt index 42789c4a..c80b2ef6 100644 --- a/optional-kubernetes-engine/requirements-dev.txt +++ b/optional-kubernetes-engine/requirements-dev.txt @@ -1,7 +1,7 @@ tox==2.9.1 flake8==3.5.0 flaky==3.4.0 -pytest==3.3.2 +pytest==3.8.2 pytest-cov==2.5.1 BeautifulSoup4==4.6.0 requests==2.18.4 From 90624c4e11dc0c027bde8e1f45fa56a58353d352 Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Thu, 8 Nov 2018 08:55:37 -0800 Subject: [PATCH 019/395] Use new trampoline.sh Cleans up temp files --- .kokoro/trampoline.sh | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.kokoro/trampoline.sh b/.kokoro/trampoline.sh index 6e293e63..fef7c24d 100755 --- a/.kokoro/trampoline.sh +++ b/.kokoro/trampoline.sh @@ -1,5 +1,4 @@ #!/bin/bash - # Copyright 2017 Google Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,4 +13,16 @@ # See the License for the specific language governing permissions and # limitations under the License. +set -eo pipefail + +# Always run the cleanup script, regardless of the success of bouncing into +# the container. + +function cleanup() { + chmod +x ${KOKORO_GFILE_DIR}/trampoline_cleanup.sh + ${KOKORO_GFILE_DIR}/trampoline_cleanup.sh + echo "cleanup"; +} +trap cleanup EXIT + python3 "${KOKORO_GFILE_DIR}/trampoline_v1.py" From c52278ab633e15b206539c2e3499d07254935c80 Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Thu, 8 Nov 2018 11:01:12 -0800 Subject: [PATCH 020/395] Use newer trampoline image Matching python-docs-samples-tests. --- .kokoro/common.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.kokoro/common.cfg b/.kokoro/common.cfg index edcc33d4..4af0df96 100644 --- a/.kokoro/common.cfg +++ b/.kokoro/common.cfg @@ -9,5 +9,5 @@ build_file: "getting-started-python/.kokoro/trampoline.sh" # Use the Python worker docker iamge. env_vars: { key: "TRAMPOLINE_IMAGE" - value: "gcr.io/silver-python2/python-worker" + value: "gcr.io/cloud-devrel-kokoro-resources/python@sha256:4b6ba8c199e96248980db4538065cddeea594138b9b9fb2d0388603922087747" } From b2affa2928b4d1f70c8306b9ebebdd4de25f3993 Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Thu, 8 Nov 2018 12:43:34 -0800 Subject: [PATCH 021/395] Removed pip install New image should already have nox. --- .kokoro/system_tests.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/.kokoro/system_tests.sh b/.kokoro/system_tests.sh index 81a91533..3ece8473 100755 --- a/.kokoro/system_tests.sh +++ b/.kokoro/system_tests.sh @@ -28,6 +28,4 @@ SECRETS_PASSWORD=$(cat "${KOKORO_GFILE_DIR}/secrets-password.txt") export GOOGLE_APPLICATION_CREDENTIALS="$(pwd)/service-account.json" # Run tests -pip install --upgrade nox-automation - nox -s run_tests From c0d95a3846cb3d99b5dc26e6b70ee309def41053 Mon Sep 17 00:00:00 2001 From: Bu Sun Kim Date: Thu, 8 Nov 2018 15:04:42 -0800 Subject: [PATCH 022/395] Update noxfile --- nox.py => noxfile.py | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) rename nox.py => noxfile.py (74%) diff --git a/nox.py b/noxfile.py similarity index 74% rename from nox.py rename to noxfile.py index c21996d1..f9e569a1 100644 --- a/nox.py +++ b/noxfile.py @@ -1,5 +1,4 @@ from glob import glob -import shutil import nox @@ -15,10 +14,13 @@ '4-auth', '5-logging', '6-pubsub', - '7-gce'] + '7-gce', + 'optional-kubernetes-engine' +] -def session_check_requirements(session): +@nox.session +def check_requirements(session): session.install(REPO_TOOLS_REQ) if 'update' in session.posargs: @@ -30,7 +32,8 @@ def session_check_requirements(session): session.run('gcp-devrel-py-tools', command, reqfile) -def session_lint(session): +@nox.session +def lint(session): session.install('flake8', 'flake8-import-order') session.run( 'flake8', '--exclude=env,.nox,._config.py,.tox', @@ -38,28 +41,22 @@ def session_lint(session): def run_test(session, dir, toxargs): - shutil.copy('config.py', dir) session.chdir(dir) session.run('tox', *(toxargs or [])) +@nox.session @nox.parametrize('dir', DIRS) -def session_run_tests(session, dir=None, toxargs=None): +def run_tests(session, dir=None, toxargs=None): """Run all tests for all directories (slow!)""" run_test(session, dir, toxargs) -def session_run_one_test(session): - dir = session.posargs[0] - toxargs = session.posargs[1:] - run_test(session, dir, toxargs) - - +@nox.session @nox.parametrize('dir', DIRS) -def session_travis(session, dir=None): +def travis(session, dir=None): """On travis, only run the py3.4 and cloudsql tests.""" - shutil.copy('config.py', dir) - session_run_tests( + run_tests( session, dir=dir, toxargs=['-e', 'py34', '--', '-k', 'cloudsql']) From 39f58f8b9c7bd82dfb2d4596e54e28fe1937f595 Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Mon, 12 Nov 2018 11:25:39 -0800 Subject: [PATCH 023/395] Getting environment right to run tests --- .kokoro/common.cfg | 6 ++++++ conftest.py | 38 ++++++++++++++++++++++++++++++++++++++ noxfile.py | 24 ++++++++++++++++++------ pytest.ini | 6 ++++++ requirements.txt | 8 ++++++++ 5 files changed, 76 insertions(+), 6 deletions(-) create mode 100644 conftest.py create mode 100644 pytest.ini create mode 100644 requirements.txt diff --git a/.kokoro/common.cfg b/.kokoro/common.cfg index 4af0df96..c87eeb5a 100644 --- a/.kokoro/common.cfg +++ b/.kokoro/common.cfg @@ -11,3 +11,9 @@ env_vars: { key: "TRAMPOLINE_IMAGE" value: "gcr.io/cloud-devrel-kokoro-resources/python@sha256:4b6ba8c199e96248980db4538065cddeea594138b9b9fb2d0388603922087747" } + +# Specify project ID +env_vars: { + key: "GCLOUD_PROJECT" + value: "python-docs-samples" +} diff --git a/conftest.py b/conftest.py new file mode 100644 index 00000000..104baa32 --- /dev/null +++ b/conftest.py @@ -0,0 +1,38 @@ +# Copyright 2016 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +import mock +import pytest + +PROJECT = os.environ['GCLOUD_PROJECT'] + + +@pytest.fixture +def api_client_inject_project_id(): + """Patches all googleapiclient requests to replace 'YOUR_PROJECT_ID' with + the project ID.""" + import googleapiclient.http + + old_execute = googleapiclient.http.HttpRequest.execute + + def new_execute(self, http=None, num_retries=0): + self.uri = self.uri.replace('YOUR_PROJECT_ID', PROJECT) + return old_execute(self, http=http, num_retries=num_retries) + + with mock.patch( + 'googleapiclient.http.HttpRequest.execute', + new=new_execute): + yield diff --git a/noxfile.py b/noxfile.py index f9e569a1..ce8b773e 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1,4 +1,5 @@ from glob import glob +import os import nox @@ -18,6 +19,8 @@ 'optional-kubernetes-engine' ] +PYTEST_COMMON_ARGS = ['--junitxml=sponge_log.xml'] + @nox.session def check_requirements(session): @@ -40,16 +43,26 @@ def lint(session): '--import-order-style=google', '.') -def run_test(session, dir, toxargs): +def run_test(session, dir): + session.install('-r', 'requirements.txt') session.chdir(dir) - session.run('tox', *(toxargs or [])) + if os.path.exists('requirements.txt'): + session.install('-r', 'requirements.txt') + + session.run( + 'pytest', + *(PYTEST_COMMON_ARGS + session.posargs), + # Pytest will return 5 when no tests are collected. This can happen + # on travis where slow and flaky tests are excluded. + # See http://doc.pytest.org/en/latest/_modules/_pytest/main.html + success_codes=[0, 5]) @nox.session @nox.parametrize('dir', DIRS) -def run_tests(session, dir=None, toxargs=None): +def run_tests(session, dir=None): """Run all tests for all directories (slow!)""" - run_test(session, dir, toxargs) + run_test(session, dir) @nox.session @@ -58,5 +71,4 @@ def travis(session, dir=None): """On travis, only run the py3.4 and cloudsql tests.""" run_tests( session, - dir=dir, - toxargs=['-e', 'py34', '--', '-k', 'cloudsql']) + dir=dir) diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..2008cdfa --- /dev/null +++ b/pytest.ini @@ -0,0 +1,6 @@ +[pytest] +addopts = + -v + --no-success-flaky-report + --tb=native +norecursedirs = .git env lib .tox .nox diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..e540e499 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +BeautifulSoup4==4.6.0 +flake8==3.5.0 +flaky==3.4.0 +mock==2.0.0 +pytest==3.8.2 +pytest-cov==2.5.1 +retrying==1.3.3 +requests==2.18.4 From 6b179dc4fb95f4db380b9156381b6210adeef2e5 Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Mon, 12 Nov 2018 11:40:12 -0800 Subject: [PATCH 024/395] Set the Project in code --- conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conftest.py b/conftest.py index 104baa32..3df98777 100644 --- a/conftest.py +++ b/conftest.py @@ -17,7 +17,7 @@ import mock import pytest -PROJECT = os.environ['GCLOUD_PROJECT'] +PROJECT = 'python-docs-samples' @pytest.fixture From 07343820d2842049b4ce00619bc710c20da3eefa Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Mon, 12 Nov 2018 11:56:15 -0800 Subject: [PATCH 025/395] Continuing adjustments to get tests running --- .kokoro/common.cfg | 6 ------ .travis.yml | 2 -- 2 files changed, 8 deletions(-) diff --git a/.kokoro/common.cfg b/.kokoro/common.cfg index c87eeb5a..4af0df96 100644 --- a/.kokoro/common.cfg +++ b/.kokoro/common.cfg @@ -11,9 +11,3 @@ env_vars: { key: "TRAMPOLINE_IMAGE" value: "gcr.io/cloud-devrel-kokoro-resources/python@sha256:4b6ba8c199e96248980db4538065cddeea594138b9b9fb2d0388603922087747" } - -# Specify project ID -env_vars: { - key: "GCLOUD_PROJECT" - value: "python-docs-samples" -} diff --git a/.travis.yml b/.travis.yml index 02e0aadb..b3bf7316 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,5 @@ env: - secure: S/A6XENZTy+py6kQxUX5EWgNJv3NGRaSTo4LdWVaZ7qKww1+akeMNHKvPXjp398UtsXkCWCpdy5HeLDYdmzX83uuFoPD1S/z7ilVpo/q4sRxDQmnw4diPHSpx32+XRyKpMpbjgJTMhocqZGcJFI9jxZ4QAHK3rul8ulvTojCyQf7OrePhgfBB72f/ZHho73VqmsngGyWNI6MtGGrBzMTtXlAOt8BgDhYXzPjhEWkE+OKAF9sMJqFmxUXyKkfTBfcGHDYcAOlNtvVdZGRC9NmzRvZxC5T7lQv/WdfGBAT3Zr4NSobKHK6DAv9Q7QvVgHIs7HRomgpHQ9IKnwyhR4RTYAnTtZyHe0/fUlPUXC8+Pkk634eRgYuVNMH8Cf7eRXcLRGF6wgBe9nHnx/o0w/kLbEyirQo3kVIM4Y6yfR5MhZ0Nb4oRq5tuXzqN1B5ZFyfmHhkwsJoeL5wgIGcwOxkoCTH8HqvPfW+SJuUpQC/sB89ixfTfUXYyMJxgGTOdg+1wD4IAfivGyWiUBcx06glfC3Sn+75oflgMz4M8y6zafXTzux755U5pEMa8Gw+5BqpDlkRliZwdAJ6UwHo/XHeJ2rgiTddEgSZYN0CR5XQFcEc69lauUEPkeE5fPubSgcH28xvbhWq7WJ1qdi3Wdw8RaCLOys/B6vwL8GhSJP+ROg= before_install: - ./decrypt-secrets.sh "$SECRETS_PASSWORD" -install: -- pip install nox-automation tox script: - nox --session lint travis From 43a9561e73e56c157ffaee9bcff70c96df2afe94 Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Wed, 14 Nov 2018 09:18:00 -0800 Subject: [PATCH 026/395] Fix path for Pytest runs --- noxfile.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/noxfile.py b/noxfile.py index ce8b773e..37877381 100644 --- a/noxfile.py +++ b/noxfile.py @@ -44,11 +44,15 @@ def lint(session): def run_test(session, dir): + print('Running a test') + session.install('-r', 'requirements.txt') session.chdir(dir) if os.path.exists('requirements.txt'): session.install('-r', 'requirements.txt') + session.env['PYTHONPATH'] = os.getcwd() + print(session.env) session.run( 'pytest', *(PYTEST_COMMON_ARGS + session.posargs), From 82d2a56f2ef270073e6a225c1f179e230760be54 Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Wed, 14 Nov 2018 09:56:45 -0800 Subject: [PATCH 027/395] Fixed deprecated mongo code --- 2-structured-data/bookshelf/model_mongodb.py | 16 +++++++++++----- 2-structured-data/requirements.txt | 4 ++-- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/2-structured-data/bookshelf/model_mongodb.py b/2-structured-data/bookshelf/model_mongodb.py index 2296ba5c..3a856e33 100644 --- a/2-structured-data/bookshelf/model_mongodb.py +++ b/2-structured-data/bookshelf/model_mongodb.py @@ -12,14 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os + from bson.objectid import ObjectId -from flask.ext.pymongo import PyMongo +from flask_pymongo import PyMongo builtin_list = list -mongo = PyMongo() +mongo = None def _id(id): @@ -43,6 +45,10 @@ def from_mongo(data): def init_app(app): + global mongo + + app.config["MONGO_URI"] = os.environ["MONGO_URI"] + mongo = PyMongo(app) mongo.init_app(app) @@ -67,17 +73,17 @@ def read(id): # [START create] def create(data): - new_id = mongo.db.books.insert(data) + new_id = mongo.db.books.insert_one(data) return read(new_id) # [END create] # [START update] def update(data, id): - mongo.db.books.update({'_id': _id(id)}, data) + mongo.db.books.update_one({'_id': _id(id)}, data) return read(id) # [END update] def delete(id): - mongo.db.books.remove(_id(id)) + mongo.db.books.delete_one(_id(id)) diff --git a/2-structured-data/requirements.txt b/2-structured-data/requirements.txt index b45693f2..6f7e0c23 100644 --- a/2-structured-data/requirements.txt +++ b/2-structured-data/requirements.txt @@ -1,8 +1,8 @@ -Flask==0.12.2 +Flask>=1.0.0 google-cloud-datastore==1.4.0 gunicorn==19.7.1 Flask-SQLAlchemy==2.3.2 PyMySQL==0.8.0 -Flask-PyMongo==0.5.1 +Flask-PyMongo>=2.0.0 PyMongo==3.6.0 six==1.11.0 From e120cef000135ed6d36325b2e73f6e9d8310b607 Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Wed, 14 Nov 2018 11:14:04 -0800 Subject: [PATCH 028/395] Incremental fixes for tests --- 2-structured-data/bookshelf/model_mongodb.py | 3 --- noxfile.py | 3 --- 2 files changed, 6 deletions(-) diff --git a/2-structured-data/bookshelf/model_mongodb.py b/2-structured-data/bookshelf/model_mongodb.py index 3a856e33..1ad36ce9 100644 --- a/2-structured-data/bookshelf/model_mongodb.py +++ b/2-structured-data/bookshelf/model_mongodb.py @@ -12,8 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os - from bson.objectid import ObjectId from flask_pymongo import PyMongo @@ -47,7 +45,6 @@ def from_mongo(data): def init_app(app): global mongo - app.config["MONGO_URI"] = os.environ["MONGO_URI"] mongo = PyMongo(app) mongo.init_app(app) diff --git a/noxfile.py b/noxfile.py index 37877381..0657ea43 100644 --- a/noxfile.py +++ b/noxfile.py @@ -44,15 +44,12 @@ def lint(session): def run_test(session, dir): - print('Running a test') - session.install('-r', 'requirements.txt') session.chdir(dir) if os.path.exists('requirements.txt'): session.install('-r', 'requirements.txt') session.env['PYTHONPATH'] = os.getcwd() - print(session.env) session.run( 'pytest', *(PYTEST_COMMON_ARGS + session.posargs), From d034d1d20b4bdcda3fb50d21781aaae881b98251 Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Wed, 14 Nov 2018 11:27:25 -0800 Subject: [PATCH 029/395] Continue updating for PyMongo API changes --- 2-structured-data/bookshelf/model_mongodb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/2-structured-data/bookshelf/model_mongodb.py b/2-structured-data/bookshelf/model_mongodb.py index 1ad36ce9..5c9c1976 100644 --- a/2-structured-data/bookshelf/model_mongodb.py +++ b/2-structured-data/bookshelf/model_mongodb.py @@ -83,4 +83,4 @@ def update(data, id): def delete(id): - mongo.db.books.delete_one(_id(id)) + mongo.db.books.delete_one({'_id': _id(id)}) From ec242cd8afe17374fa3b9b192cd983e799badd36 Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Wed, 14 Nov 2018 11:50:20 -0800 Subject: [PATCH 030/395] Continue updating for PyMongo API changes --- 2-structured-data/bookshelf/model_mongodb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/2-structured-data/bookshelf/model_mongodb.py b/2-structured-data/bookshelf/model_mongodb.py index 5c9c1976..8db0a75f 100644 --- a/2-structured-data/bookshelf/model_mongodb.py +++ b/2-structured-data/bookshelf/model_mongodb.py @@ -63,7 +63,7 @@ def list(limit=10, cursor=None): # [START read] def read(id): - result = mongo.db.books.find_one(_id(id)) + result = mongo.db.books.find_one({'_id': _id(id)}) return from_mongo(result) # [END read] From 92adddf4c1bb891fc84b9ad0d0231193da1f6b65 Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Wed, 14 Nov 2018 13:00:47 -0800 Subject: [PATCH 031/395] Continue updating for PyMongo API changes --- 2-structured-data/bookshelf/model_mongodb.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/2-structured-data/bookshelf/model_mongodb.py b/2-structured-data/bookshelf/model_mongodb.py index 8db0a75f..e2f83045 100644 --- a/2-structured-data/bookshelf/model_mongodb.py +++ b/2-structured-data/bookshelf/model_mongodb.py @@ -70,8 +70,8 @@ def read(id): # [START create] def create(data): - new_id = mongo.db.books.insert_one(data) - return read(new_id) + result = mongo.db.books.insert_one(data) + return read(result.inserted_id) # [END create] From 8321491d647caacd91ca66826d33a79e93b1772e Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Wed, 14 Nov 2018 13:18:08 -0800 Subject: [PATCH 032/395] Continue updating for PyMongo API changes --- 2-structured-data/bookshelf/model_mongodb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/2-structured-data/bookshelf/model_mongodb.py b/2-structured-data/bookshelf/model_mongodb.py index e2f83045..7cb7615b 100644 --- a/2-structured-data/bookshelf/model_mongodb.py +++ b/2-structured-data/bookshelf/model_mongodb.py @@ -77,7 +77,7 @@ def create(data): # [START update] def update(data, id): - mongo.db.books.update_one({'_id': _id(id)}, data) + mongo.db.books.replace_one({'_id': _id(id)}, data) return read(id) # [END update] From d2c7059628eb8ffe9097f7eef3a3b0aba11bbb3e Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Wed, 14 Nov 2018 13:34:34 -0800 Subject: [PATCH 033/395] Continue updating for PyMongo API changes --- 3-binary-data/bookshelf/model_mongodb.py | 27 +++++++++--- 4-auth/bookshelf/model_mongodb.py | 43 ++++++++++++------- 5-logging/bookshelf/model_mongodb.py | 41 ++++++++++++------ 6-pubsub/bookshelf/model_mongodb.py | 41 ++++++++++++------ 7-gce/bookshelf/model_mongodb.py | 41 ++++++++++++------ .../bookshelf/model_mongodb.py | 41 ++++++++++++------ 6 files changed, 160 insertions(+), 74 deletions(-) diff --git a/3-binary-data/bookshelf/model_mongodb.py b/3-binary-data/bookshelf/model_mongodb.py index 95e245d8..7cb7615b 100644 --- a/3-binary-data/bookshelf/model_mongodb.py +++ b/3-binary-data/bookshelf/model_mongodb.py @@ -13,13 +13,13 @@ # limitations under the License. from bson.objectid import ObjectId -from flask.ext.pymongo import PyMongo +from flask_pymongo import PyMongo builtin_list = list -mongo = PyMongo() +mongo = None def _id(id): @@ -28,6 +28,7 @@ def _id(id): return id +# [START from_mongo] def from_mongo(data): """ Translates the MongoDB dictionary format into the format that's expected @@ -38,12 +39,17 @@ def from_mongo(data): data['id'] = str(data['_id']) return data +# [END from_mongo] def init_app(app): + global mongo + + mongo = PyMongo(app) mongo.init_app(app) +# [START list] def list(limit=10, cursor=None): cursor = int(cursor) if cursor else 0 @@ -52,22 +58,29 @@ def list(limit=10, cursor=None): next_page = cursor + limit if len(books) == limit else None return (books, next_page) +# [END list] +# [START read] def read(id): - result = mongo.db.books.find_one(_id(id)) + result = mongo.db.books.find_one({'_id': _id(id)}) return from_mongo(result) +# [END read] +# [START create] def create(data): - new_id = mongo.db.books.insert(data) - return read(new_id) + result = mongo.db.books.insert_one(data) + return read(result.inserted_id) +# [END create] +# [START update] def update(data, id): - mongo.db.books.update({'_id': _id(id)}, data) + mongo.db.books.replace_one({'_id': _id(id)}, data) return read(id) +# [END update] def delete(id): - mongo.db.books.remove(_id(id)) + mongo.db.books.delete_one({'_id': _id(id)}) diff --git a/4-auth/bookshelf/model_mongodb.py b/4-auth/bookshelf/model_mongodb.py index 95470b81..aaccc4f8 100644 --- a/4-auth/bookshelf/model_mongodb.py +++ b/4-auth/bookshelf/model_mongodb.py @@ -13,13 +13,13 @@ # limitations under the License. from bson.objectid import ObjectId -from flask.ext.pymongo import PyMongo +from flask_pymongo import PyMongo builtin_list = list -mongo = PyMongo() +mongo = None def _id(id): @@ -28,6 +28,7 @@ def _id(id): return id +# [START from_mongo] def from_mongo(data): """ Translates the MongoDB dictionary format into the format that's expected @@ -38,50 +39,62 @@ def from_mongo(data): data['id'] = str(data['_id']) return data +# [END from_mongo] def init_app(app): + global mongo + + mongo = PyMongo(app) mongo.init_app(app) -def list(limit=10, cursor=None): +# [START list_by_user] +def list_by_user(user_id, limit=10, cursor=None): cursor = int(cursor) if cursor else 0 - results = mongo.db.books.find(skip=cursor, limit=10).sort('title') + results = mongo.db.books\ + .find({'createdById': user_id}, skip=cursor, limit=10)\ + .sort('title') books = builtin_list(map(from_mongo, results)) next_page = cursor + limit if len(books) == limit else None return (books, next_page) +# [END list_by_user] -# [START list_by_user] -def list_by_user(user_id, limit=10, cursor=None): +# [START list] +def list(limit=10, cursor=None): cursor = int(cursor) if cursor else 0 - results = mongo.db.books\ - .find({'createdById': user_id}, skip=cursor, limit=10)\ - .sort('title') + results = mongo.db.books.find(skip=cursor, limit=10).sort('title') books = builtin_list(map(from_mongo, results)) next_page = cursor + limit if len(books) == limit else None return (books, next_page) -# [END list_by_user] +# [END list] +# [START read] def read(id): - result = mongo.db.books.find_one(_id(id)) + result = mongo.db.books.find_one({'_id': _id(id)}) return from_mongo(result) +# [END read] +# [START create] def create(data): - new_id = mongo.db.books.insert(data) - return read(new_id) + result = mongo.db.books.insert_one(data) + return read(result.inserted_id) +# [END create] +# [START update] def update(data, id): - mongo.db.books.update({'_id': _id(id)}, data) + mongo.db.books.replace_one({'_id': _id(id)}, data) return read(id) +# [END update] def delete(id): - mongo.db.books.remove(_id(id)) + mongo.db.books.delete_one({'_id': _id(id)}) diff --git a/5-logging/bookshelf/model_mongodb.py b/5-logging/bookshelf/model_mongodb.py index c49d5bbc..aaccc4f8 100644 --- a/5-logging/bookshelf/model_mongodb.py +++ b/5-logging/bookshelf/model_mongodb.py @@ -13,13 +13,13 @@ # limitations under the License. from bson.objectid import ObjectId -from flask.ext.pymongo import PyMongo +from flask_pymongo import PyMongo builtin_list = list -mongo = PyMongo() +mongo = None def _id(id): @@ -28,6 +28,7 @@ def _id(id): return id +# [START from_mongo] def from_mongo(data): """ Translates the MongoDB dictionary format into the format that's expected @@ -38,48 +39,62 @@ def from_mongo(data): data['id'] = str(data['_id']) return data +# [END from_mongo] def init_app(app): + global mongo + + mongo = PyMongo(app) mongo.init_app(app) -def list(limit=10, cursor=None): +# [START list_by_user] +def list_by_user(user_id, limit=10, cursor=None): cursor = int(cursor) if cursor else 0 - results = mongo.db.books.find(skip=cursor, limit=10).sort('title') + results = mongo.db.books\ + .find({'createdById': user_id}, skip=cursor, limit=10)\ + .sort('title') books = builtin_list(map(from_mongo, results)) next_page = cursor + limit if len(books) == limit else None return (books, next_page) +# [END list_by_user] -def list_by_user(user_id, limit=10, cursor=None): +# [START list] +def list(limit=10, cursor=None): cursor = int(cursor) if cursor else 0 - results = mongo.db.books\ - .find({'createdById': user_id}, skip=cursor, limit=10)\ - .sort('title') + results = mongo.db.books.find(skip=cursor, limit=10).sort('title') books = builtin_list(map(from_mongo, results)) next_page = cursor + limit if len(books) == limit else None return (books, next_page) +# [END list] +# [START read] def read(id): - result = mongo.db.books.find_one(_id(id)) + result = mongo.db.books.find_one({'_id': _id(id)}) return from_mongo(result) +# [END read] +# [START create] def create(data): - new_id = mongo.db.books.insert(data) - return read(new_id) + result = mongo.db.books.insert_one(data) + return read(result.inserted_id) +# [END create] +# [START update] def update(data, id): - mongo.db.books.update({'_id': _id(id)}, data) + mongo.db.books.replace_one({'_id': _id(id)}, data) return read(id) +# [END update] def delete(id): - mongo.db.books.remove(_id(id)) + mongo.db.books.delete_one({'_id': _id(id)}) diff --git a/6-pubsub/bookshelf/model_mongodb.py b/6-pubsub/bookshelf/model_mongodb.py index c49d5bbc..aaccc4f8 100644 --- a/6-pubsub/bookshelf/model_mongodb.py +++ b/6-pubsub/bookshelf/model_mongodb.py @@ -13,13 +13,13 @@ # limitations under the License. from bson.objectid import ObjectId -from flask.ext.pymongo import PyMongo +from flask_pymongo import PyMongo builtin_list = list -mongo = PyMongo() +mongo = None def _id(id): @@ -28,6 +28,7 @@ def _id(id): return id +# [START from_mongo] def from_mongo(data): """ Translates the MongoDB dictionary format into the format that's expected @@ -38,48 +39,62 @@ def from_mongo(data): data['id'] = str(data['_id']) return data +# [END from_mongo] def init_app(app): + global mongo + + mongo = PyMongo(app) mongo.init_app(app) -def list(limit=10, cursor=None): +# [START list_by_user] +def list_by_user(user_id, limit=10, cursor=None): cursor = int(cursor) if cursor else 0 - results = mongo.db.books.find(skip=cursor, limit=10).sort('title') + results = mongo.db.books\ + .find({'createdById': user_id}, skip=cursor, limit=10)\ + .sort('title') books = builtin_list(map(from_mongo, results)) next_page = cursor + limit if len(books) == limit else None return (books, next_page) +# [END list_by_user] -def list_by_user(user_id, limit=10, cursor=None): +# [START list] +def list(limit=10, cursor=None): cursor = int(cursor) if cursor else 0 - results = mongo.db.books\ - .find({'createdById': user_id}, skip=cursor, limit=10)\ - .sort('title') + results = mongo.db.books.find(skip=cursor, limit=10).sort('title') books = builtin_list(map(from_mongo, results)) next_page = cursor + limit if len(books) == limit else None return (books, next_page) +# [END list] +# [START read] def read(id): - result = mongo.db.books.find_one(_id(id)) + result = mongo.db.books.find_one({'_id': _id(id)}) return from_mongo(result) +# [END read] +# [START create] def create(data): - new_id = mongo.db.books.insert(data) - return read(new_id) + result = mongo.db.books.insert_one(data) + return read(result.inserted_id) +# [END create] +# [START update] def update(data, id): - mongo.db.books.update({'_id': _id(id)}, data) + mongo.db.books.replace_one({'_id': _id(id)}, data) return read(id) +# [END update] def delete(id): - mongo.db.books.remove(_id(id)) + mongo.db.books.delete_one({'_id': _id(id)}) diff --git a/7-gce/bookshelf/model_mongodb.py b/7-gce/bookshelf/model_mongodb.py index c49d5bbc..aaccc4f8 100644 --- a/7-gce/bookshelf/model_mongodb.py +++ b/7-gce/bookshelf/model_mongodb.py @@ -13,13 +13,13 @@ # limitations under the License. from bson.objectid import ObjectId -from flask.ext.pymongo import PyMongo +from flask_pymongo import PyMongo builtin_list = list -mongo = PyMongo() +mongo = None def _id(id): @@ -28,6 +28,7 @@ def _id(id): return id +# [START from_mongo] def from_mongo(data): """ Translates the MongoDB dictionary format into the format that's expected @@ -38,48 +39,62 @@ def from_mongo(data): data['id'] = str(data['_id']) return data +# [END from_mongo] def init_app(app): + global mongo + + mongo = PyMongo(app) mongo.init_app(app) -def list(limit=10, cursor=None): +# [START list_by_user] +def list_by_user(user_id, limit=10, cursor=None): cursor = int(cursor) if cursor else 0 - results = mongo.db.books.find(skip=cursor, limit=10).sort('title') + results = mongo.db.books\ + .find({'createdById': user_id}, skip=cursor, limit=10)\ + .sort('title') books = builtin_list(map(from_mongo, results)) next_page = cursor + limit if len(books) == limit else None return (books, next_page) +# [END list_by_user] -def list_by_user(user_id, limit=10, cursor=None): +# [START list] +def list(limit=10, cursor=None): cursor = int(cursor) if cursor else 0 - results = mongo.db.books\ - .find({'createdById': user_id}, skip=cursor, limit=10)\ - .sort('title') + results = mongo.db.books.find(skip=cursor, limit=10).sort('title') books = builtin_list(map(from_mongo, results)) next_page = cursor + limit if len(books) == limit else None return (books, next_page) +# [END list] +# [START read] def read(id): - result = mongo.db.books.find_one(_id(id)) + result = mongo.db.books.find_one({'_id': _id(id)}) return from_mongo(result) +# [END read] +# [START create] def create(data): - new_id = mongo.db.books.insert(data) - return read(new_id) + result = mongo.db.books.insert_one(data) + return read(result.inserted_id) +# [END create] +# [START update] def update(data, id): - mongo.db.books.update({'_id': _id(id)}, data) + mongo.db.books.replace_one({'_id': _id(id)}, data) return read(id) +# [END update] def delete(id): - mongo.db.books.remove(_id(id)) + mongo.db.books.delete_one({'_id': _id(id)}) diff --git a/optional-kubernetes-engine/bookshelf/model_mongodb.py b/optional-kubernetes-engine/bookshelf/model_mongodb.py index c49d5bbc..aaccc4f8 100644 --- a/optional-kubernetes-engine/bookshelf/model_mongodb.py +++ b/optional-kubernetes-engine/bookshelf/model_mongodb.py @@ -13,13 +13,13 @@ # limitations under the License. from bson.objectid import ObjectId -from flask.ext.pymongo import PyMongo +from flask_pymongo import PyMongo builtin_list = list -mongo = PyMongo() +mongo = None def _id(id): @@ -28,6 +28,7 @@ def _id(id): return id +# [START from_mongo] def from_mongo(data): """ Translates the MongoDB dictionary format into the format that's expected @@ -38,48 +39,62 @@ def from_mongo(data): data['id'] = str(data['_id']) return data +# [END from_mongo] def init_app(app): + global mongo + + mongo = PyMongo(app) mongo.init_app(app) -def list(limit=10, cursor=None): +# [START list_by_user] +def list_by_user(user_id, limit=10, cursor=None): cursor = int(cursor) if cursor else 0 - results = mongo.db.books.find(skip=cursor, limit=10).sort('title') + results = mongo.db.books\ + .find({'createdById': user_id}, skip=cursor, limit=10)\ + .sort('title') books = builtin_list(map(from_mongo, results)) next_page = cursor + limit if len(books) == limit else None return (books, next_page) +# [END list_by_user] -def list_by_user(user_id, limit=10, cursor=None): +# [START list] +def list(limit=10, cursor=None): cursor = int(cursor) if cursor else 0 - results = mongo.db.books\ - .find({'createdById': user_id}, skip=cursor, limit=10)\ - .sort('title') + results = mongo.db.books.find(skip=cursor, limit=10).sort('title') books = builtin_list(map(from_mongo, results)) next_page = cursor + limit if len(books) == limit else None return (books, next_page) +# [END list] +# [START read] def read(id): - result = mongo.db.books.find_one(_id(id)) + result = mongo.db.books.find_one({'_id': _id(id)}) return from_mongo(result) +# [END read] +# [START create] def create(data): - new_id = mongo.db.books.insert(data) - return read(new_id) + result = mongo.db.books.insert_one(data) + return read(result.inserted_id) +# [END create] +# [START update] def update(data, id): - mongo.db.books.update({'_id': _id(id)}, data) + mongo.db.books.replace_one({'_id': _id(id)}, data) return read(id) +# [END update] def delete(id): - mongo.db.books.remove(_id(id)) + mongo.db.books.delete_one({'_id': _id(id)}) From 458b6a91e05994700579c7a45b22380bcde6578c Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Wed, 14 Nov 2018 14:04:46 -0800 Subject: [PATCH 034/395] Making requirements current and consistent --- 4-auth/requirements.txt | 4 ++-- 5-logging/requirements.txt | 4 ++-- 6-pubsub/requirements.txt | 4 ++-- 7-gce/requirements.txt | 4 ++-- optional-kubernetes-engine/requirements.txt | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/4-auth/requirements.txt b/4-auth/requirements.txt index da821968..93bcfee7 100644 --- a/4-auth/requirements.txt +++ b/4-auth/requirements.txt @@ -1,10 +1,10 @@ -Flask==0.12.2 +Flask>=1.0.0 google-cloud-datastore==1.4.0 google-cloud-storage==1.7.0 gunicorn==19.7.1 oauth2client==4.1.2 Flask-SQLAlchemy==2.3.2 PyMySQL==0.8.0 -Flask-PyMongo==0.5.1 +Flask-PyMongo>=2.0.0 PyMongo==3.6.0 six==1.11.0 diff --git a/5-logging/requirements.txt b/5-logging/requirements.txt index 9393f9f3..46b4d315 100644 --- a/5-logging/requirements.txt +++ b/5-logging/requirements.txt @@ -1,4 +1,4 @@ -Flask==0.12.2 +Flask>=1.0.0 google-cloud-datastore==1.4.0 google-cloud-storage==1.7.0 google-cloud-logging==1.4.0 @@ -7,6 +7,6 @@ gunicorn==19.7.1 oauth2client==4.1.2 Flask-SQLAlchemy==2.3.2 PyMySQL==0.8.0 -Flask-PyMongo==0.5.1 +Flask-PyMongo>=2.0.0 PyMongo==3.6.0 six==1.11.0 diff --git a/6-pubsub/requirements.txt b/6-pubsub/requirements.txt index 976f5bf6..9ce6f268 100644 --- a/6-pubsub/requirements.txt +++ b/6-pubsub/requirements.txt @@ -1,4 +1,4 @@ -Flask==0.12.2 +Flask>=1.0.0 google-cloud-datastore==1.4.0 google-cloud-storage==1.7.0 google-cloud-logging==1.4.0 @@ -7,7 +7,7 @@ gunicorn==19.7.1 oauth2client==4.1.2 Flask-SQLAlchemy==2.3.2 PyMySQL==0.8.0 -Flask-PyMongo==0.5.1 +Flask-PyMongo>=2.0.0 PyMongo==3.6.0 six==1.11.0 requests[security]==2.18.4 diff --git a/7-gce/requirements.txt b/7-gce/requirements.txt index 976f5bf6..9ce6f268 100644 --- a/7-gce/requirements.txt +++ b/7-gce/requirements.txt @@ -1,4 +1,4 @@ -Flask==0.12.2 +Flask>=1.0.0 google-cloud-datastore==1.4.0 google-cloud-storage==1.7.0 google-cloud-logging==1.4.0 @@ -7,7 +7,7 @@ gunicorn==19.7.1 oauth2client==4.1.2 Flask-SQLAlchemy==2.3.2 PyMySQL==0.8.0 -Flask-PyMongo==0.5.1 +Flask-PyMongo>=2.0.0 PyMongo==3.6.0 six==1.11.0 requests[security]==2.18.4 diff --git a/optional-kubernetes-engine/requirements.txt b/optional-kubernetes-engine/requirements.txt index 8539383d..ae4ebb3a 100644 --- a/optional-kubernetes-engine/requirements.txt +++ b/optional-kubernetes-engine/requirements.txt @@ -1,4 +1,4 @@ -Flask==0.12.2 +Flask>=1.0.0 google-cloud-datastore==1.4.0 google-cloud-storage==1.7.0 google-cloud-logging==1.4.0 @@ -8,7 +8,7 @@ oauth2client==4.1.2 mock==2.0.0 Flask-SQLAlchemy==2.3.2 PyMySQL==0.8.0 -Flask-PyMongo==0.5.1 +Flask-PyMongo>=2.0.0 PyMongo==3.6.0 six==1.11.0 requests[security]==2.18.4 From 0a83c25c0694a16b290c5c62c3e24825b82661da Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Wed, 14 Nov 2018 14:25:57 -0800 Subject: [PATCH 035/395] More Mongo client code fixes --- 3-binary-data/bookshelf/model_mongodb.py | 1 - 3-binary-data/requirements.txt | 4 ++-- optional-kubernetes-engine/bookshelf/model_mongodb.py | 1 - 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/3-binary-data/bookshelf/model_mongodb.py b/3-binary-data/bookshelf/model_mongodb.py index 7cb7615b..09633f56 100644 --- a/3-binary-data/bookshelf/model_mongodb.py +++ b/3-binary-data/bookshelf/model_mongodb.py @@ -46,7 +46,6 @@ def init_app(app): global mongo mongo = PyMongo(app) - mongo.init_app(app) # [START list] diff --git a/3-binary-data/requirements.txt b/3-binary-data/requirements.txt index da821968..93bcfee7 100644 --- a/3-binary-data/requirements.txt +++ b/3-binary-data/requirements.txt @@ -1,10 +1,10 @@ -Flask==0.12.2 +Flask>=1.0.0 google-cloud-datastore==1.4.0 google-cloud-storage==1.7.0 gunicorn==19.7.1 oauth2client==4.1.2 Flask-SQLAlchemy==2.3.2 PyMySQL==0.8.0 -Flask-PyMongo==0.5.1 +Flask-PyMongo>=2.0.0 PyMongo==3.6.0 six==1.11.0 diff --git a/optional-kubernetes-engine/bookshelf/model_mongodb.py b/optional-kubernetes-engine/bookshelf/model_mongodb.py index aaccc4f8..ea823cd0 100644 --- a/optional-kubernetes-engine/bookshelf/model_mongodb.py +++ b/optional-kubernetes-engine/bookshelf/model_mongodb.py @@ -46,7 +46,6 @@ def init_app(app): global mongo mongo = PyMongo(app) - mongo.init_app(app) # [START list_by_user] From a490a1ea4ba72dc1c87cecde8a7e54b30113e53d Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Wed, 14 Nov 2018 15:35:59 -0800 Subject: [PATCH 036/395] More Mongo client code fixes --- .kokoro/common.cfg | 5 +++++ 3-binary-data/tests/test_crud.py | 2 +- conftest.py | 2 -- noxfile.py | 1 - 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.kokoro/common.cfg b/.kokoro/common.cfg index 4af0df96..f8bcdf12 100644 --- a/.kokoro/common.cfg +++ b/.kokoro/common.cfg @@ -11,3 +11,8 @@ env_vars: { key: "TRAMPOLINE_IMAGE" value: "gcr.io/cloud-devrel-kokoro-resources/python@sha256:4b6ba8c199e96248980db4538065cddeea594138b9b9fb2d0388603922087747" } + +env_vars: { + key: "GCLOUD_PROJECT" + value: "python-docs-samples-tests" +} diff --git a/3-binary-data/tests/test_crud.py b/3-binary-data/tests/test_crud.py index c0d2f40f..b09e224b 100644 --- a/3-binary-data/tests/test_crud.py +++ b/3-binary-data/tests/test_crud.py @@ -40,7 +40,7 @@ def test_list(self, app, model): body = rv.data.decode('utf-8') assert 'Book 1' in body, "Should show books" - assert len(re.findall('

Book', body)) == 10, ( + assert len(re.findall('

Book', body)) <= 10, ( "Should not show more than 10 books") assert 'More' in body, "Should have more than one page" diff --git a/conftest.py b/conftest.py index 3df98777..b6078c07 100644 --- a/conftest.py +++ b/conftest.py @@ -17,8 +17,6 @@ import mock import pytest -PROJECT = 'python-docs-samples' - @pytest.fixture def api_client_inject_project_id(): diff --git a/noxfile.py b/noxfile.py index 0657ea43..7c337a60 100644 --- a/noxfile.py +++ b/noxfile.py @@ -16,7 +16,6 @@ '5-logging', '6-pubsub', '7-gce', - 'optional-kubernetes-engine' ] PYTEST_COMMON_ARGS = ['--junitxml=sponge_log.xml'] From 6834faa318e9451672dbb670bef06df2ed72c58c Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Wed, 14 Nov 2018 15:40:05 -0800 Subject: [PATCH 037/395] Got ahead of other configuration. Step back for now. --- .kokoro/common.cfg | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.kokoro/common.cfg b/.kokoro/common.cfg index f8bcdf12..4af0df96 100644 --- a/.kokoro/common.cfg +++ b/.kokoro/common.cfg @@ -11,8 +11,3 @@ env_vars: { key: "TRAMPOLINE_IMAGE" value: "gcr.io/cloud-devrel-kokoro-resources/python@sha256:4b6ba8c199e96248980db4538065cddeea594138b9b9fb2d0388603922087747" } - -env_vars: { - key: "GCLOUD_PROJECT" - value: "python-docs-samples-tests" -} From 24c93fc5f9651874bd3d0b97b653a990dd4bfe9f Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Wed, 14 Nov 2018 16:10:46 -0800 Subject: [PATCH 038/395] Deselect end-to-end from automated tests --- noxfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index 7c337a60..35d37061 100644 --- a/noxfile.py +++ b/noxfile.py @@ -18,7 +18,7 @@ '7-gce', ] -PYTEST_COMMON_ARGS = ['--junitxml=sponge_log.xml'] +PYTEST_COMMON_ARGS = ['--junitxml=sponge_log.xml', '-m', 'not e2e'] @nox.session From 275c8ab16458bccc44fc8366d1c5cc1f3677e8e2 Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Wed, 14 Nov 2018 16:33:04 -0800 Subject: [PATCH 039/395] Fixed inconsistency --- 7-gce/procfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/7-gce/procfile b/7-gce/procfile index d2b676b5..0c4fcec5 100644 --- a/7-gce/procfile +++ b/7-gce/procfile @@ -1,3 +1,3 @@ -bookshelf: gunicorn -b 0.0.0.0:8080 main:app +bookshelf: gunicorn -b 0.0.0.0:$PORT main:app worker: psqworker --pid /tmp/psq.pid main.books_queue monitor: python monitor.py /tmp/psq.pid From 95d71825b1cd4be2b14f96430a807088187d4fb3 Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Thu, 15 Nov 2018 09:23:25 -0800 Subject: [PATCH 040/395] Fix URL for login test --- 7-gce/tests/test_auth.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/7-gce/tests/test_auth.py b/7-gce/tests/test_auth.py index 7fc7dabc..936736fc 100644 --- a/7-gce/tests/test_auth.py +++ b/7-gce/tests/test_auth.py @@ -60,7 +60,7 @@ class TestAuth(object): def test_not_logged_in(self, app): with app.test_client() as c: - rv = c.get('/books/') + rv = c.get('/books/mine') assert rv.status == '200 OK' body = rv.data.decode('utf-8') @@ -68,7 +68,7 @@ def test_not_logged_in(self, app): def test_logged_in(self, client_with_credentials): with client_with_credentials() as c: - rv = c.get('/books/') + rv = c.get('/books/mine') assert rv.status == '200 OK' body = rv.data.decode('utf-8') From 4b4276d6750ae8558d3bb359ab7191a4d1146470 Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Thu, 15 Nov 2018 09:44:11 -0800 Subject: [PATCH 041/395] Check for not failed rather than OK --- 7-gce/tests/test_auth.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/7-gce/tests/test_auth.py b/7-gce/tests/test_auth.py index 936736fc..155b4dc1 100644 --- a/7-gce/tests/test_auth.py +++ b/7-gce/tests/test_auth.py @@ -62,7 +62,7 @@ def test_not_logged_in(self, app): with app.test_client() as c: rv = c.get('/books/mine') - assert rv.status == '200 OK' + assert rv.status < '400' body = rv.data.decode('utf-8') assert 'Login' in body @@ -70,7 +70,7 @@ def test_logged_in(self, client_with_credentials): with client_with_credentials() as c: rv = c.get('/books/mine') - assert rv.status == '200 OK' + assert rv.status < '400' body = rv.data.decode('utf-8') assert 'Test User' in body From 4709b81b07a6be3f0a391d470b2074af9e4584bd Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Thu, 15 Nov 2018 09:55:10 -0800 Subject: [PATCH 042/395] Logging in uses a redirect, check for that. --- 7-gce/tests/test_auth.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/7-gce/tests/test_auth.py b/7-gce/tests/test_auth.py index 155b4dc1..b58cea3f 100644 --- a/7-gce/tests/test_auth.py +++ b/7-gce/tests/test_auth.py @@ -64,7 +64,7 @@ def test_not_logged_in(self, app): assert rv.status < '400' body = rv.data.decode('utf-8') - assert 'Login' in body + assert 'Redirecting' in body def test_logged_in(self, client_with_credentials): with client_with_credentials() as c: @@ -72,7 +72,7 @@ def test_logged_in(self, client_with_credentials): assert rv.status < '400' body = rv.data.decode('utf-8') - assert 'Test User' in body + assert 'Redirecting' not in body def test_add_anonymous(self, app): data = { From 0133ad152eb1ca6a9abe610424b492b797e30e67 Mon Sep 17 00:00:00 2001 From: DPE bot Date: Sun, 25 Nov 2018 20:18:22 -0800 Subject: [PATCH 043/395] Auto-update dependencies. --- 1-hello-world/requirements.txt | 4 ++-- 2-structured-data/requirements-dev.txt | 8 ++++---- 2-structured-data/requirements.txt | 8 ++++---- 3-binary-data/requirements-dev.txt | 8 ++++---- 3-binary-data/requirements.txt | 12 ++++++------ 4-auth/requirements-dev.txt | 8 ++++---- 4-auth/requirements.txt | 12 ++++++------ 5-logging/requirements-dev.txt | 8 ++++---- 5-logging/requirements.txt | 16 ++++++++-------- 6-pubsub/requirements-dev.txt | 12 ++++++------ 6-pubsub/requirements.txt | 18 +++++++++--------- 7-gce/requirements-dev.txt | 12 ++++++------ 7-gce/requirements.txt | 18 +++++++++--------- .../requirements-dev.txt | 12 ++++++------ optional-kubernetes-engine/requirements.txt | 18 +++++++++--------- 15 files changed, 87 insertions(+), 87 deletions(-) diff --git a/1-hello-world/requirements.txt b/1-hello-world/requirements.txt index bb84e50e..a34d076b 100644 --- a/1-hello-world/requirements.txt +++ b/1-hello-world/requirements.txt @@ -1,2 +1,2 @@ -Flask==0.12.2 -gunicorn==19.7.1 +Flask==1.0.2 +gunicorn==19.9.0 diff --git a/2-structured-data/requirements-dev.txt b/2-structured-data/requirements-dev.txt index c5f07101..5bbc2e5d 100644 --- a/2-structured-data/requirements-dev.txt +++ b/2-structured-data/requirements-dev.txt @@ -1,6 +1,6 @@ -tox==2.9.1 -flake8==3.5.0 +tox==3.5.3 +flake8==3.6.0 flaky==3.4.0 -pytest==3.8.2 -pytest-cov==2.5.1 +pytest==4.0.1 +pytest-cov==2.6.0 retrying==1.3.3 diff --git a/2-structured-data/requirements.txt b/2-structured-data/requirements.txt index 6f7e0c23..18edbd4c 100644 --- a/2-structured-data/requirements.txt +++ b/2-structured-data/requirements.txt @@ -1,8 +1,8 @@ Flask>=1.0.0 -google-cloud-datastore==1.4.0 -gunicorn==19.7.1 +google-cloud-datastore==1.7.1 +gunicorn==19.9.0 Flask-SQLAlchemy==2.3.2 -PyMySQL==0.8.0 +PyMySQL==0.9.2 Flask-PyMongo>=2.0.0 -PyMongo==3.6.0 +PyMongo==3.7.2 six==1.11.0 diff --git a/3-binary-data/requirements-dev.txt b/3-binary-data/requirements-dev.txt index c5f07101..5bbc2e5d 100644 --- a/3-binary-data/requirements-dev.txt +++ b/3-binary-data/requirements-dev.txt @@ -1,6 +1,6 @@ -tox==2.9.1 -flake8==3.5.0 +tox==3.5.3 +flake8==3.6.0 flaky==3.4.0 -pytest==3.8.2 -pytest-cov==2.5.1 +pytest==4.0.1 +pytest-cov==2.6.0 retrying==1.3.3 diff --git a/3-binary-data/requirements.txt b/3-binary-data/requirements.txt index 93bcfee7..fa332f42 100644 --- a/3-binary-data/requirements.txt +++ b/3-binary-data/requirements.txt @@ -1,10 +1,10 @@ Flask>=1.0.0 -google-cloud-datastore==1.4.0 -google-cloud-storage==1.7.0 -gunicorn==19.7.1 -oauth2client==4.1.2 +google-cloud-datastore==1.7.1 +google-cloud-storage==1.13.0 +gunicorn==19.9.0 +oauth2client==4.1.3 Flask-SQLAlchemy==2.3.2 -PyMySQL==0.8.0 +PyMySQL==0.9.2 Flask-PyMongo>=2.0.0 -PyMongo==3.6.0 +PyMongo==3.7.2 six==1.11.0 diff --git a/4-auth/requirements-dev.txt b/4-auth/requirements-dev.txt index 8406c088..282373c0 100644 --- a/4-auth/requirements-dev.txt +++ b/4-auth/requirements-dev.txt @@ -1,7 +1,7 @@ -tox==2.9.1 -flake8==3.5.0 +tox==3.5.3 +flake8==3.6.0 flaky==3.4.0 -pytest==3.8.2 -pytest-cov==2.5.1 +pytest==4.0.1 +pytest-cov==2.6.0 retrying==1.3.3 mock==2.0.0 diff --git a/4-auth/requirements.txt b/4-auth/requirements.txt index 93bcfee7..fa332f42 100644 --- a/4-auth/requirements.txt +++ b/4-auth/requirements.txt @@ -1,10 +1,10 @@ Flask>=1.0.0 -google-cloud-datastore==1.4.0 -google-cloud-storage==1.7.0 -gunicorn==19.7.1 -oauth2client==4.1.2 +google-cloud-datastore==1.7.1 +google-cloud-storage==1.13.0 +gunicorn==19.9.0 +oauth2client==4.1.3 Flask-SQLAlchemy==2.3.2 -PyMySQL==0.8.0 +PyMySQL==0.9.2 Flask-PyMongo>=2.0.0 -PyMongo==3.6.0 +PyMongo==3.7.2 six==1.11.0 diff --git a/5-logging/requirements-dev.txt b/5-logging/requirements-dev.txt index 8406c088..282373c0 100644 --- a/5-logging/requirements-dev.txt +++ b/5-logging/requirements-dev.txt @@ -1,7 +1,7 @@ -tox==2.9.1 -flake8==3.5.0 +tox==3.5.3 +flake8==3.6.0 flaky==3.4.0 -pytest==3.8.2 -pytest-cov==2.5.1 +pytest==4.0.1 +pytest-cov==2.6.0 retrying==1.3.3 mock==2.0.0 diff --git a/5-logging/requirements.txt b/5-logging/requirements.txt index 46b4d315..32ff3033 100644 --- a/5-logging/requirements.txt +++ b/5-logging/requirements.txt @@ -1,12 +1,12 @@ Flask>=1.0.0 -google-cloud-datastore==1.4.0 -google-cloud-storage==1.7.0 -google-cloud-logging==1.4.0 -google-cloud-error_reporting==0.29.0 -gunicorn==19.7.1 -oauth2client==4.1.2 +google-cloud-datastore==1.7.1 +google-cloud-storage==1.13.0 +google-cloud-logging==1.8.0 +google-cloud-error_reporting==0.30.0 +gunicorn==19.9.0 +oauth2client==4.1.3 Flask-SQLAlchemy==2.3.2 -PyMySQL==0.8.0 +PyMySQL==0.9.2 Flask-PyMongo>=2.0.0 -PyMongo==3.6.0 +PyMongo==3.7.2 six==1.11.0 diff --git a/6-pubsub/requirements-dev.txt b/6-pubsub/requirements-dev.txt index dbe05429..7e4b9ace 100644 --- a/6-pubsub/requirements-dev.txt +++ b/6-pubsub/requirements-dev.txt @@ -1,9 +1,9 @@ -tox==2.9.1 -flake8==3.5.0 +tox==3.5.3 +flake8==3.6.0 flaky==3.4.0 mock==2.0.0 -pytest==3.8.2 -pytest-cov==2.5.1 -BeautifulSoup4==4.6.0 -requests==2.18.4 +pytest==4.0.1 +pytest-cov==2.6.0 +BeautifulSoup4==4.6.3 +requests==2.20.1 retrying==1.3.3 diff --git a/6-pubsub/requirements.txt b/6-pubsub/requirements.txt index 9ce6f268..94c7387b 100644 --- a/6-pubsub/requirements.txt +++ b/6-pubsub/requirements.txt @@ -1,15 +1,15 @@ Flask>=1.0.0 -google-cloud-datastore==1.4.0 -google-cloud-storage==1.7.0 -google-cloud-logging==1.4.0 -google-cloud-error_reporting==0.29.0 -gunicorn==19.7.1 -oauth2client==4.1.2 +google-cloud-datastore==1.7.1 +google-cloud-storage==1.13.0 +google-cloud-logging==1.8.0 +google-cloud-error_reporting==0.30.0 +gunicorn==19.9.0 +oauth2client==4.1.3 Flask-SQLAlchemy==2.3.2 -PyMySQL==0.8.0 +PyMySQL==0.9.2 Flask-PyMongo>=2.0.0 -PyMongo==3.6.0 +PyMongo==3.7.2 six==1.11.0 -requests[security]==2.18.4 +requests[security]==2.20.1 honcho==1.0.1 psq==0.7.0 diff --git a/7-gce/requirements-dev.txt b/7-gce/requirements-dev.txt index dbe05429..7e4b9ace 100644 --- a/7-gce/requirements-dev.txt +++ b/7-gce/requirements-dev.txt @@ -1,9 +1,9 @@ -tox==2.9.1 -flake8==3.5.0 +tox==3.5.3 +flake8==3.6.0 flaky==3.4.0 mock==2.0.0 -pytest==3.8.2 -pytest-cov==2.5.1 -BeautifulSoup4==4.6.0 -requests==2.18.4 +pytest==4.0.1 +pytest-cov==2.6.0 +BeautifulSoup4==4.6.3 +requests==2.20.1 retrying==1.3.3 diff --git a/7-gce/requirements.txt b/7-gce/requirements.txt index 9ce6f268..94c7387b 100644 --- a/7-gce/requirements.txt +++ b/7-gce/requirements.txt @@ -1,15 +1,15 @@ Flask>=1.0.0 -google-cloud-datastore==1.4.0 -google-cloud-storage==1.7.0 -google-cloud-logging==1.4.0 -google-cloud-error_reporting==0.29.0 -gunicorn==19.7.1 -oauth2client==4.1.2 +google-cloud-datastore==1.7.1 +google-cloud-storage==1.13.0 +google-cloud-logging==1.8.0 +google-cloud-error_reporting==0.30.0 +gunicorn==19.9.0 +oauth2client==4.1.3 Flask-SQLAlchemy==2.3.2 -PyMySQL==0.8.0 +PyMySQL==0.9.2 Flask-PyMongo>=2.0.0 -PyMongo==3.6.0 +PyMongo==3.7.2 six==1.11.0 -requests[security]==2.18.4 +requests[security]==2.20.1 honcho==1.0.1 psq==0.7.0 diff --git a/optional-kubernetes-engine/requirements-dev.txt b/optional-kubernetes-engine/requirements-dev.txt index c80b2ef6..3118b924 100644 --- a/optional-kubernetes-engine/requirements-dev.txt +++ b/optional-kubernetes-engine/requirements-dev.txt @@ -1,8 +1,8 @@ -tox==2.9.1 -flake8==3.5.0 +tox==3.5.3 +flake8==3.6.0 flaky==3.4.0 -pytest==3.8.2 -pytest-cov==2.5.1 -BeautifulSoup4==4.6.0 -requests==2.18.4 +pytest==4.0.1 +pytest-cov==2.6.0 +BeautifulSoup4==4.6.3 +requests==2.20.1 retrying==1.3.3 diff --git a/optional-kubernetes-engine/requirements.txt b/optional-kubernetes-engine/requirements.txt index ae4ebb3a..5d998499 100644 --- a/optional-kubernetes-engine/requirements.txt +++ b/optional-kubernetes-engine/requirements.txt @@ -1,16 +1,16 @@ Flask>=1.0.0 -google-cloud-datastore==1.4.0 -google-cloud-storage==1.7.0 -google-cloud-logging==1.4.0 -google-cloud-error_reporting==0.29.0 -gunicorn==19.7.1 -oauth2client==4.1.2 +google-cloud-datastore==1.7.1 +google-cloud-storage==1.13.0 +google-cloud-logging==1.8.0 +google-cloud-error_reporting==0.30.0 +gunicorn==19.9.0 +oauth2client==4.1.3 mock==2.0.0 Flask-SQLAlchemy==2.3.2 -PyMySQL==0.8.0 +PyMySQL==0.9.2 Flask-PyMongo>=2.0.0 -PyMongo==3.6.0 +PyMongo==3.7.2 six==1.11.0 -requests[security]==2.18.4 +requests[security]==2.20.1 honcho==1.0.1 psq==0.7.0 From 9130a3c8389fa40b8822ce99534388789287f153 Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Mon, 26 Nov 2018 13:40:45 -0800 Subject: [PATCH 044/395] Address vulnerability alert --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e540e499..a18371a9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,4 @@ mock==2.0.0 pytest==3.8.2 pytest-cov==2.5.1 retrying==1.3.3 -requests==2.18.4 +requests>=2.20.0 From 7011df7a1479ffe8cbee5787e1230232d820d3c2 Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Wed, 28 Nov 2018 09:05:33 -0800 Subject: [PATCH 045/395] Update requirements.txt --- 2-structured-data/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/2-structured-data/requirements.txt b/2-structured-data/requirements.txt index 18edbd4c..deaf011a 100644 --- a/2-structured-data/requirements.txt +++ b/2-structured-data/requirements.txt @@ -4,5 +4,6 @@ gunicorn==19.9.0 Flask-SQLAlchemy==2.3.2 PyMySQL==0.9.2 Flask-PyMongo>=2.0.0 +oauth2client==4.1.2 PyMongo==3.7.2 six==1.11.0 From 209ee8384421fa564be364c95e1c7227e8b7b213 Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Fri, 15 Feb 2019 12:24:29 -0800 Subject: [PATCH 046/395] Changed to people API from plus API --- 4-auth/bookshelf/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/4-auth/bookshelf/__init__.py b/4-auth/bookshelf/__init__.py index b245f2ba..7f755e05 100644 --- a/4-auth/bookshelf/__init__.py +++ b/4-auth/bookshelf/__init__.py @@ -114,7 +114,7 @@ def _request_user_info(credentials): http = httplib2.Http() credentials.authorize(http) resp, content = http.request( - 'https://www.googleapis.com/plus/v1/people/me') + 'https://people.googleapis.com/v1/people/me') if resp.status != 200: current_app.logger.error( From a2660b805384918573f648eab751497ea818cfbc Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Fri, 15 Feb 2019 12:37:47 -0800 Subject: [PATCH 047/395] Use better API --- 4-auth/bookshelf/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/4-auth/bookshelf/__init__.py b/4-auth/bookshelf/__init__.py index 7f755e05..46816f1a 100644 --- a/4-auth/bookshelf/__init__.py +++ b/4-auth/bookshelf/__init__.py @@ -114,7 +114,7 @@ def _request_user_info(credentials): http = httplib2.Http() credentials.authorize(http) resp, content = http.request( - 'https://people.googleapis.com/v1/people/me') + 'https://www.googleapis.com/oauth2/v3/userinfo') if resp.status != 200: current_app.logger.error( From f2d7ad9dd70fa4d2cb24f2bb0c9b40a9c50c6b84 Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Sat, 16 Feb 2019 16:04:19 -0800 Subject: [PATCH 048/395] Use new API authentication fields --- 4-auth/bookshelf/crud.py | 7 ++++--- 4-auth/bookshelf/templates/base.html | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/4-auth/bookshelf/crud.py b/4-auth/bookshelf/crud.py index 8991bcfe..03b0c8c3 100644 --- a/4-auth/bookshelf/crud.py +++ b/4-auth/bookshelf/crud.py @@ -62,8 +62,9 @@ def list_mine(): if token: token = token.encode('utf-8') + print(session['profile']) books, next_page_token = get_model().list_by_user( - user_id=session['profile']['id'], + user_id=session['profile']['email'], cursor=token) return render_template( @@ -93,8 +94,8 @@ def add(): # If the user is logged in, associate their profile with the new book. if 'profile' in session: - data['createdBy'] = session['profile']['displayName'] - data['createdById'] = session['profile']['id'] + data['createdBy'] = session['profile']['name'] + data['createdById'] = session['profile']['email'] book = get_model().create(data) diff --git a/4-auth/bookshelf/templates/base.html b/4-auth/bookshelf/templates/base.html index e7beae7f..145ba4e1 100644 --- a/4-auth/bookshelf/templates/base.html +++ b/4-auth/bookshelf/templates/base.html @@ -35,10 +35,10 @@

Hello {}

".format(email) + return page diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt new file mode 100644 index 00000000..5379a109 --- /dev/null +++ b/authenticating-users/requirements.txt @@ -0,0 +1,4 @@ +Flask>=1.0.0 +cryptography +python-jose[cryptography] +requests From ee61ccf6c33fcc16830a5e59404837cbde233c0c Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Fri, 12 Jul 2019 11:22:52 -0700 Subject: [PATCH 060/395] Capitalization typo fixed --- authenticating-users/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authenticating-users/main.py b/authenticating-users/main.py index 51494e7b..1d4af471 100644 --- a/authenticating-users/main.py +++ b/authenticating-users/main.py @@ -72,7 +72,7 @@ def validate_assertion(assertion): @app.route('/', methods=['GET']) def say_hello(): - from Flask import request + from flask import request assertion = request.headers.get('X-Goog-IAP-JWT-Assertion') email, id = validate_assertion(assertion) From 72f7355ab8d8e52e9afa49940da4fb5e32258eff Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Mon, 15 Jul 2019 09:31:02 -0700 Subject: [PATCH 061/395] Some debugging info added --- authenticating-users/main.py | 7 +++++-- authenticating-users/requirements.txt | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/authenticating-users/main.py b/authenticating-users/main.py index 1d4af471..2b3e1c71 100644 --- a/authenticating-users/main.py +++ b/authenticating-users/main.py @@ -12,6 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. +import googleclouddebugger +googleclouddebugger.enable() + from flask import Flask app = Flask(__name__) @@ -66,8 +69,8 @@ def validate_assertion(assertion): audience=audience() ) return info['email'], info['sub'] - except: - return None, None + except Exception as e: + return e.message, None @app.route('/', methods=['GET']) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index 5379a109..9ae9e24d 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -2,3 +2,4 @@ Flask>=1.0.0 cryptography python-jose[cryptography] requests +google-python-cloud-debugger From d04ab5dfebcf94cc4ff34965c480705ca239b4e4 Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Mon, 15 Jul 2019 09:36:51 -0700 Subject: [PATCH 062/395] Some debugging info added --- authenticating-users/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authenticating-users/main.py b/authenticating-users/main.py index 2b3e1c71..0165a667 100644 --- a/authenticating-users/main.py +++ b/authenticating-users/main.py @@ -70,7 +70,7 @@ def validate_assertion(assertion): ) return info['email'], info['sub'] except Exception as e: - return e.message, None + return str(e), None @app.route('/', methods=['GET']) From 81d1f4c138e03dbe9c2cd4cbd943f329bff0e80c Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Mon, 15 Jul 2019 09:39:26 -0700 Subject: [PATCH 063/395] Some debugging info added --- authenticating-users/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/authenticating-users/main.py b/authenticating-users/main.py index 0165a667..1022d3cb 100644 --- a/authenticating-users/main.py +++ b/authenticating-users/main.py @@ -50,8 +50,8 @@ def get_metadata(item_name): def audience(): global AUDIENCE if AUDIENCE is None: - project_number = get_metadata(numeric_project_id) - project_id = get_metadata(project_id) + project_number = get_metadata('numeric_project_id') + project_id = get_metadata('project_id') AUDIENCE = '/projects/{}/apps/{}'.format( project_number, project_id ) From 31a4c3d2a1289cac5a4cd428a8bea419cbd63bdf Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Mon, 15 Jul 2019 12:14:56 -0700 Subject: [PATCH 064/395] Logging errors --- authenticating-users/main.py | 39 +++++++++++++++++---------- authenticating-users/requirements.txt | 1 - 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/authenticating-users/main.py b/authenticating-users/main.py index 1022d3cb..d99d6287 100644 --- a/authenticating-users/main.py +++ b/authenticating-users/main.py @@ -12,8 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -import googleclouddebugger -googleclouddebugger.enable() +import sys from flask import Flask app = Flask(__name__) @@ -27,10 +26,13 @@ def certs(): global CERTS if CERTS is None: - resp = requests.get( - 'https://www.gstatic.com/iap/verify/public_key' - ) - CERTS = resp.json() + try: + response = requests.get( + 'https://www.gstatic.com/iap/verify/public_key' + ) + CERTS = response.json() + except Exception as e: + print('Failed to fetch CERTS: {}'.format(e), file=sys.stderr) return CERTS @@ -40,18 +42,26 @@ def get_metadata(item_name): endpoint = 'http://metadata.google.internal' path = '/computeMetadata/v1/project/' path += item_name - response = requests.get( - '{}{}'.format(endpoint, path), - headers = {'Metadata-Flavor': 'Google'} - ) - return response.json() + try: + response = requests.get( + '{}{}'.format(endpoint, path), + headers = {'Metadata-Flavor': 'Google'} + ) + metadata = response.json() + except Exception as e: + print( + 'Failed to get metadata for {}: {}'.format(item_name, e), + file=sys.stderr + ) + metadata = None + return metadata def audience(): global AUDIENCE if AUDIENCE is None: - project_number = get_metadata('numeric_project_id') - project_id = get_metadata('project_id') + project_number = get_metadata('numeric-project-id') + project_id = get_metadata('project-id') AUDIENCE = '/projects/{}/apps/{}'.format( project_number, project_id ) @@ -70,7 +80,8 @@ def validate_assertion(assertion): ) return info['email'], info['sub'] except Exception as e: - return str(e), None + print('Failed to validate assertion: {}'.format(e), file=sys.stderr) + return None, None @app.route('/', methods=['GET']) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index 9ae9e24d..5379a109 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -2,4 +2,3 @@ Flask>=1.0.0 cryptography python-jose[cryptography] requests -google-python-cloud-debugger From 7f92e7b0ef068f8297e5cfa17f001912e39aa1b7 Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Mon, 15 Jul 2019 15:07:06 -0700 Subject: [PATCH 065/395] Use text, not JSON, for metadata --- authenticating-users/main.py | 28 +++++++++------------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/authenticating-users/main.py b/authenticating-users/main.py index d99d6287..233ac28e 100644 --- a/authenticating-users/main.py +++ b/authenticating-users/main.py @@ -26,13 +26,10 @@ def certs(): global CERTS if CERTS is None: - try: - response = requests.get( - 'https://www.gstatic.com/iap/verify/public_key' - ) - CERTS = response.json() - except Exception as e: - print('Failed to fetch CERTS: {}'.format(e), file=sys.stderr) + response = requests.get( + 'https://www.gstatic.com/iap/verify/public_key' + ) + CERTS = response.json() return CERTS @@ -42,18 +39,11 @@ def get_metadata(item_name): endpoint = 'http://metadata.google.internal' path = '/computeMetadata/v1/project/' path += item_name - try: - response = requests.get( - '{}{}'.format(endpoint, path), - headers = {'Metadata-Flavor': 'Google'} - ) - metadata = response.json() - except Exception as e: - print( - 'Failed to get metadata for {}: {}'.format(item_name, e), - file=sys.stderr - ) - metadata = None + response = requests.get( + '{}{}'.format(endpoint, path), + headers = {'Metadata-Flavor': 'Google'} + ) + metadata = response.text return metadata From 5c35be447fffd99a972b9424153d21d82a63217b Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Tue, 16 Jul 2019 09:49:44 -0700 Subject: [PATCH 066/395] Skip Cloud SQL tests due to new firewall --- 2-structured-data/tests/conftest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/2-structured-data/tests/conftest.py b/2-structured-data/tests/conftest.py index 8123575b..62c2a480 100644 --- a/2-structured-data/tests/conftest.py +++ b/2-structured-data/tests/conftest.py @@ -22,7 +22,8 @@ from retrying import retry -@pytest.yield_fixture(params=['datastore', 'cloudsql', 'mongodb']) +#@pytest.yield_fixture(params=['datastore', 'cloudsql', 'mongodb']) +@pytest.yield_fixture(params=['datastore', 'mongodb']) def app(request): """This fixtures provides a Flask app instance configured for testing. From 06eb33645403dd3381685c400f9a7302936f23ca Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Tue, 16 Jul 2019 09:55:38 -0700 Subject: [PATCH 067/395] Skip Cloud SQL tests due to new firewall --- 3-binary-data/tests/conftest.py | 3 ++- 4-auth/tests/conftest.py | 3 ++- 5-logging/tests/conftest.py | 3 ++- 6-pubsub/tests/conftest.py | 3 ++- 7-gce/tests/conftest.py | 3 ++- optional-kubernetes-engine/tests/conftest.py | 3 ++- 6 files changed, 12 insertions(+), 6 deletions(-) diff --git a/3-binary-data/tests/conftest.py b/3-binary-data/tests/conftest.py index 8123575b..62c2a480 100644 --- a/3-binary-data/tests/conftest.py +++ b/3-binary-data/tests/conftest.py @@ -22,7 +22,8 @@ from retrying import retry -@pytest.yield_fixture(params=['datastore', 'cloudsql', 'mongodb']) +#@pytest.yield_fixture(params=['datastore', 'cloudsql', 'mongodb']) +@pytest.yield_fixture(params=['datastore', 'mongodb']) def app(request): """This fixtures provides a Flask app instance configured for testing. diff --git a/4-auth/tests/conftest.py b/4-auth/tests/conftest.py index 8123575b..62c2a480 100644 --- a/4-auth/tests/conftest.py +++ b/4-auth/tests/conftest.py @@ -22,7 +22,8 @@ from retrying import retry -@pytest.yield_fixture(params=['datastore', 'cloudsql', 'mongodb']) +#@pytest.yield_fixture(params=['datastore', 'cloudsql', 'mongodb']) +@pytest.yield_fixture(params=['datastore', 'mongodb']) def app(request): """This fixtures provides a Flask app instance configured for testing. diff --git a/5-logging/tests/conftest.py b/5-logging/tests/conftest.py index 8123575b..62c2a480 100644 --- a/5-logging/tests/conftest.py +++ b/5-logging/tests/conftest.py @@ -22,7 +22,8 @@ from retrying import retry -@pytest.yield_fixture(params=['datastore', 'cloudsql', 'mongodb']) +#@pytest.yield_fixture(params=['datastore', 'cloudsql', 'mongodb']) +@pytest.yield_fixture(params=['datastore', 'mongodb']) def app(request): """This fixtures provides a Flask app instance configured for testing. diff --git a/6-pubsub/tests/conftest.py b/6-pubsub/tests/conftest.py index 8123575b..62c2a480 100644 --- a/6-pubsub/tests/conftest.py +++ b/6-pubsub/tests/conftest.py @@ -22,7 +22,8 @@ from retrying import retry -@pytest.yield_fixture(params=['datastore', 'cloudsql', 'mongodb']) +#@pytest.yield_fixture(params=['datastore', 'cloudsql', 'mongodb']) +@pytest.yield_fixture(params=['datastore', 'mongodb']) def app(request): """This fixtures provides a Flask app instance configured for testing. diff --git a/7-gce/tests/conftest.py b/7-gce/tests/conftest.py index 8123575b..62c2a480 100644 --- a/7-gce/tests/conftest.py +++ b/7-gce/tests/conftest.py @@ -22,7 +22,8 @@ from retrying import retry -@pytest.yield_fixture(params=['datastore', 'cloudsql', 'mongodb']) +#@pytest.yield_fixture(params=['datastore', 'cloudsql', 'mongodb']) +@pytest.yield_fixture(params=['datastore', 'mongodb']) def app(request): """This fixtures provides a Flask app instance configured for testing. diff --git a/optional-kubernetes-engine/tests/conftest.py b/optional-kubernetes-engine/tests/conftest.py index 8123575b..62c2a480 100644 --- a/optional-kubernetes-engine/tests/conftest.py +++ b/optional-kubernetes-engine/tests/conftest.py @@ -22,7 +22,8 @@ from retrying import retry -@pytest.yield_fixture(params=['datastore', 'cloudsql', 'mongodb']) +#@pytest.yield_fixture(params=['datastore', 'cloudsql', 'mongodb']) +@pytest.yield_fixture(params=['datastore', 'mongodb']) def app(request): """This fixtures provides a Flask app instance configured for testing. From 057f0e80f1903d86bf930324faca3da65d5cc3ee Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Thu, 1 Aug 2019 15:49:16 -0700 Subject: [PATCH 068/395] Add docstring comments to functions --- authenticating-users/main.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/authenticating-users/main.py b/authenticating-users/main.py index 233ac28e..5e690228 100644 --- a/authenticating-users/main.py +++ b/authenticating-users/main.py @@ -22,6 +22,10 @@ def certs(): + """Returns a dictionary of current Google public key certificates for + validating Google-signed JWTs. Since these change rarely, the result + is cached on first request for faster subsequent responses. + """ import requests global CERTS @@ -34,6 +38,10 @@ def certs(): def get_metadata(item_name): + """Returns a string with the project metadata value for the item_name. + See https://cloud.google.com/compute/docs/storing-retrieving-metadata for + possible item_name values. + """ import requests endpoint = 'http://metadata.google.internal' @@ -48,6 +56,10 @@ def get_metadata(item_name): def audience(): + """Returns the audience value (the JWT 'aud' property) for the current + running instance. Since this involves a metadata lookup, the result is + cached when first requested for faster future responses. + """ global AUDIENCE if AUDIENCE is None: project_number = get_metadata('numeric-project-id') @@ -59,6 +71,10 @@ def audience(): def validate_assertion(assertion): + """Checks that the JWT assertion is valid (properly signed, for the + correct audience) and if so, returns strings for the requesting user's + email and a persistent user ID. If not valid, returns None for each field. + """ from jose import jwt try: From b7de78d9b396b0dfa42d887100690a64a167dbab Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Mon, 5 Aug 2019 17:42:32 -0700 Subject: [PATCH 069/395] Revert change Due to the test environment, the project ID cannot be inferred but must be provided. --- 2-structured-data/bookshelf/model_datastore.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/2-structured-data/bookshelf/model_datastore.py b/2-structured-data/bookshelf/model_datastore.py index e33a4a68..54afd3c2 100644 --- a/2-structured-data/bookshelf/model_datastore.py +++ b/2-structured-data/bookshelf/model_datastore.py @@ -24,7 +24,7 @@ def init_app(app): def get_client(): - return datastore.Client() + return datastore.Client(current_app.config['PROJECT_ID']) # [START from_datastore] From 957019f8eac8d78bfbca01c5ef84d4c6bd6d6dba Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Tue, 20 Aug 2019 14:45:54 -0700 Subject: [PATCH 070/395] Initial background processing sample --- background/README.md | 17 ++++ background/app/app.yaml | 15 +++ background/app/main.py | 77 ++++++++++++++ background/app/main_test.py | 13 +++ background/app/requirements.txt | 3 + background/app/templates/index.html | 145 +++++++++++++++++++++++++++ background/function/main.py | 98 ++++++++++++++++++ background/function/main_test.py | 13 +++ background/function/requirements.txt | 2 + 9 files changed, 383 insertions(+) create mode 100644 background/README.md create mode 100644 background/app/app.yaml create mode 100644 background/app/main.py create mode 100644 background/app/main_test.py create mode 100644 background/app/requirements.txt create mode 100644 background/app/templates/index.html create mode 100644 background/function/main.py create mode 100644 background/function/main_test.py create mode 100644 background/function/requirements.txt diff --git a/background/README.md b/background/README.md new file mode 100644 index 00000000..4c4dc5e0 --- /dev/null +++ b/background/README.md @@ -0,0 +1,17 @@ +Background Processing +--------------------- + +This directory contains an example of doing background processing with App +Engine, Cloud Pub/Sub, Cloud Functions, and Firestore. + +Deploy commands: + +From the app directory: +``` +$ gcloud app deploy +``` + +From the function directory, after creating the PubSub topic: +``` +$ gcloud functions deploy --runtime=go111 --trigger-topic=translate Translate --set-env-vars GOOGLE_CLOUD_PROJECT=my-project +``` diff --git a/background/app/app.yaml b/background/app/app.yaml new file mode 100644 index 00000000..620fcb3a --- /dev/null +++ b/background/app/app.yaml @@ -0,0 +1,15 @@ +# Copyright 2019 Google LLC All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +runtime: python37 diff --git a/background/app/main.py b/background/app/main.py new file mode 100644 index 00000000..6e330295 --- /dev/null +++ b/background/app/main.py @@ -0,0 +1,77 @@ +# Copyright 2019 Google LLC All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" This web app shows translations that have been previously requested, and + provides a form to request a new translation. +""" + +import json +import os + +from google.cloud import firestore +from google.cloud import pubsub + +from flask import Flask, render_template, redirect, request +app = Flask(__name__) + +# Get client objects to reuse over multiple invocations +db = firestore.Client() +publisher = pubsub.PublisherClient() + +# Keep this list of supported languages up to date +ACCEPTABLE_LANGUAGES = ('de', 'en', 'es', 'fr', 'ja', 'sw') + + +@app.route('/', methods=['GET']) +def index(): + """ The home page has a list of prior translations and a form to + ask for a new translation. + """ + + doc_list = [] + docs = db.collection('translations').stream() + for doc in docs: + doc_list.append(doc.to_dict()) + + return render_template('index.html', translations=doc_list) + + +@app.route('/request-translation', methods=['POST']) +def translate(): + """ Handle a request to translate a string (form field 'v') to a given + language (form field 'lang'), by sending a PubSub message to a topic. + """ + source_string = request.form.get('v', '') + to_language = request.form.get('lang', '') + + if source_string == '': + error_message = 'Empty value' + return error_message, 400 + + if to_language not in ACCEPTABLE_LANGUAGES: + error_message = 'Unsupported language: {}'.format(to_language) + return error_message, 400 + + message = { + 'Original': source_string, + 'Language': to_language, + 'Translated': '', + 'OriginalLanguage': '', + } + + topic_name = 'projects/{}/topics/{}'.format( + os.getenv('GOOGLE_CLOUD_PROJECT'), 'translate' + ) + publisher.publish(topic_name, json.dumps(message).encode('utf8')) + return redirect('/') diff --git a/background/app/main_test.py b/background/app/main_test.py new file mode 100644 index 00000000..849e178e --- /dev/null +++ b/background/app/main_test.py @@ -0,0 +1,13 @@ +# Copyright 2019 Google LLC All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/background/app/requirements.txt b/background/app/requirements.txt new file mode 100644 index 00000000..78b2e87d --- /dev/null +++ b/background/app/requirements.txt @@ -0,0 +1,3 @@ +google-cloud-firestore==1.4.0 +google-cloud-pubsub==0.45.0 +flask>=1.0.0 diff --git a/background/app/templates/index.html b/background/app/templates/index.html new file mode 100644 index 00000000..6f30e24d --- /dev/null +++ b/background/app/templates/index.html @@ -0,0 +1,145 @@ + + + + + + + + + + Translations + + + + + + + + + + + +
+
+
+ + Translate with Background Processing +
+
+
+
+
+
+
+
+
+ + +
+ + +
+
+
+ + + + + + + + + {% for translation in translations %} + + + + + {% endfor %} + +
OriginalTranslation
+ + {{ translation['OriginalLanguage'] }} + + {{ translation['Original'] }} + + + {{ translation['Language'] }} + + {{ translation['Translated'] }} +
+
+ +
+
+
+
+
+ +
+
+
+ + + + diff --git a/background/function/main.py b/background/function/main.py new file mode 100644 index 00000000..ca8233f8 --- /dev/null +++ b/background/function/main.py @@ -0,0 +1,98 @@ +# Copyright 2019 Google LLC All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" This function handles messages posted to a pubsub topic by translating + the data in the message as requested. The message must be a JSON encoded + dictionary with fields: + + Original - the string to translate + Language - the language to translate the string to + + The dictionary may have other fields, which will be ignored. +""" + +import base64 +import hashlib +import json + +from google.cloud import firestore +from google.cloud import translate + +# Get client objects once to reuse over multiple invocations. +xlate = translate.Client() +db = firestore.Client() + + +def document_name(message): + """ Messages are saved in a Firestore database with document IDs generated + from the original string and destination language. If the exact same + translation is requested a second time, the result will overwrite the + prior result. + + message - a dictionary with fields named Language and Original, and + optionally other fields with any names + + Returns a unique name that is an allowed Firestore document ID + """ + key = '{}/{}'.format(message['Language'], message['Original']) + hashed = hashlib.sha512(key.encode()).digest() + + # Note that document IDs should not contain the '/' character + name = base64.b64encode(hashed, altchars=b'+-').decode('utf-8') + + +@firestore.transactional +def update_database(transaction, message): + name = document_name(message) + doc_ref = db.collection("translations").document(document_id=name) + + try: + doc_ref.get(transaction=transaction) + except NotFound: + return # Don't replace an existing translation + + transaction.set(doc_ref, message) + + +def translate_string(from_string, to_language): + """ Translates a string to a specified language. + + from_string - the original string before translation + + to_language - the language to translate to, as a two-letter code (e.g., + 'en' for english, 'de' for german) + + Returns the translated string and the code for original language + """ + result = xlate.translate(from_string, target_language=to_language) + db = firestore.Client() + return result['translatedText'], result['detectedSourceLanguage'] + + +def translate_message(event, context): + """ Process a pubsub message requesting a translation + """ + message_data = base64.b64decode(event['data']).decode('utf-8') + message = json.loads(message_data) + + from_string = message['Original'] + to_language = message['Language'] + + to_string, from_language = translate_string(from_string, to_language) + + message['Translated'] = to_string + message['OriginalLanguage'] = from_language + + transaction = db.transaction() + update_database(transaction, message) diff --git a/background/function/main_test.py b/background/function/main_test.py new file mode 100644 index 00000000..849e178e --- /dev/null +++ b/background/function/main_test.py @@ -0,0 +1,13 @@ +# Copyright 2019 Google LLC All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/background/function/requirements.txt b/background/function/requirements.txt new file mode 100644 index 00000000..fef973c8 --- /dev/null +++ b/background/function/requirements.txt @@ -0,0 +1,2 @@ +google-cloud-translate==1.6.0 +google-cloud-firestore==1.4.0 From 9645a450494da3c05d6340adbfa7f0f5d0881b97 Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Wed, 21 Aug 2019 08:02:22 -0700 Subject: [PATCH 071/395] Fix typos --- background/app/main.py | 2 +- background/function/main.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/background/app/main.py b/background/app/main.py index 6e330295..204859b6 100644 --- a/background/app/main.py +++ b/background/app/main.py @@ -13,7 +13,7 @@ # limitations under the License. """ This web app shows translations that have been previously requested, and - provides a form to request a new translation. + provides a form to request a new translation. """ import json diff --git a/background/function/main.py b/background/function/main.py index ca8233f8..cdb20ef1 100644 --- a/background/function/main.py +++ b/background/function/main.py @@ -56,7 +56,7 @@ def document_name(message): def update_database(transaction, message): name = document_name(message) doc_ref = db.collection("translations").document(document_id=name) - + try: doc_ref.get(transaction=transaction) except NotFound: @@ -76,7 +76,6 @@ def translate_string(from_string, to_language): Returns the translated string and the code for original language """ result = xlate.translate(from_string, target_language=to_language) - db = firestore.Client() return result['translatedText'], result['detectedSourceLanguage'] From 87c47b6aa8afed7dacf1eba3e2bd0b9098748106 Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Wed, 21 Aug 2019 13:20:11 -0700 Subject: [PATCH 072/395] Added region tags --- background/README.md | 2 +- background/app/app.yaml | 2 ++ background/app/main.py | 6 ++++++ background/function/main.py | 36 ++++++++++++++++++++++-------------- 4 files changed, 31 insertions(+), 15 deletions(-) diff --git a/background/README.md b/background/README.md index 4c4dc5e0..d79c95fe 100644 --- a/background/README.md +++ b/background/README.md @@ -13,5 +13,5 @@ $ gcloud app deploy From the function directory, after creating the PubSub topic: ``` -$ gcloud functions deploy --runtime=go111 --trigger-topic=translate Translate --set-env-vars GOOGLE_CLOUD_PROJECT=my-project +$ gcloud functions deploy --runtime=python37 --trigger-topic=translate Translate --set-env-vars GOOGLE_CLOUD_PROJECT=my-project ``` diff --git a/background/app/app.yaml b/background/app/app.yaml index 620fcb3a..fb63451a 100644 --- a/background/app/app.yaml +++ b/background/app/app.yaml @@ -12,4 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. +# [START getting_started_background_config] runtime: python37 +# [END getting_started_background_config] diff --git a/background/app/main.py b/background/app/main.py index 204859b6..efb432bd 100644 --- a/background/app/main.py +++ b/background/app/main.py @@ -16,6 +16,7 @@ provides a form to request a new translation. """ +# [START getting_started_background_app_main] import json import os @@ -31,8 +32,10 @@ # Keep this list of supported languages up to date ACCEPTABLE_LANGUAGES = ('de', 'en', 'es', 'fr', 'ja', 'sw') +# [END getting_started_background_app_main] +# [START getting_started_background_app_list] @app.route('/', methods=['GET']) def index(): """ The home page has a list of prior translations and a form to @@ -45,8 +48,10 @@ def index(): doc_list.append(doc.to_dict()) return render_template('index.html', translations=doc_list) +# [END getting_started_background_app_list] +# [START getting_started_background_app_request] @app.route('/request-translation', methods=['POST']) def translate(): """ Handle a request to translate a string (form field 'v') to a given @@ -75,3 +80,4 @@ def translate(): ) publisher.publish(topic_name, json.dumps(message).encode('utf8')) return redirect('/') +# [END getting_started_background_app_request] diff --git a/background/function/main.py b/background/function/main.py index cdb20ef1..7681e580 100644 --- a/background/function/main.py +++ b/background/function/main.py @@ -22,18 +22,39 @@ The dictionary may have other fields, which will be ignored. """ +# [START getting_started_background_translate_setup] import base64 import hashlib import json from google.cloud import firestore from google.cloud import translate +# [END getting_started_background_translate_setup] +# [START getting_started_background_translate_init] # Get client objects once to reuse over multiple invocations. xlate = translate.Client() db = firestore.Client() +# [END getting_started_background_translate_init] +# [START getting_started_background_translate_string] +def translate_string(from_string, to_language): + """ Translates a string to a specified language. + + from_string - the original string before translation + + to_language - the language to translate to, as a two-letter code (e.g., + 'en' for english, 'de' for german) + + Returns the translated string and the code for original language + """ + result = xlate.translate(from_string, target_language=to_language) + return result['translatedText'], result['detectedSourceLanguage'] +# [END getting_started_background_translate_string] + + +# [START getting_started_background_translate] def document_name(message): """ Messages are saved in a Firestore database with document IDs generated from the original string and destination language. If the exact same @@ -65,20 +86,6 @@ def update_database(transaction, message): transaction.set(doc_ref, message) -def translate_string(from_string, to_language): - """ Translates a string to a specified language. - - from_string - the original string before translation - - to_language - the language to translate to, as a two-letter code (e.g., - 'en' for english, 'de' for german) - - Returns the translated string and the code for original language - """ - result = xlate.translate(from_string, target_language=to_language) - return result['translatedText'], result['detectedSourceLanguage'] - - def translate_message(event, context): """ Process a pubsub message requesting a translation """ @@ -95,3 +102,4 @@ def translate_message(event, context): transaction = db.transaction() update_database(transaction, message) +# [END getting_started_background_translate] From e0fe4aafe5b0a9598e7de9ae6dd5fdba06534813 Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Wed, 21 Aug 2019 14:30:11 -0700 Subject: [PATCH 073/395] Added region tags --- authenticating-users/app.yaml | 16 ++++++++++++++++ authenticating-users/main.py | 12 ++++++++++++ authenticating-users/requirements.txt | 2 ++ 3 files changed, 30 insertions(+) diff --git a/authenticating-users/app.yaml b/authenticating-users/app.yaml index a0b719d6..9010f5b3 100644 --- a/authenticating-users/app.yaml +++ b/authenticating-users/app.yaml @@ -1 +1,17 @@ +# Copyright 2019 Google LLC All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START getting_started_app_yaml] runtime: python37 +# [END getting_started_app_yaml] diff --git a/authenticating-users/main.py b/authenticating-users/main.py index 5e690228..b1a37722 100644 --- a/authenticating-users/main.py +++ b/authenticating-users/main.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +# [START getting_started_auth_all] import sys from flask import Flask @@ -21,6 +22,7 @@ AUDIENCE = None +# [START getting_started_auth_certs] def certs(): """Returns a dictionary of current Google public key certificates for validating Google-signed JWTs. Since these change rarely, the result @@ -35,8 +37,10 @@ def certs(): ) CERTS = response.json() return CERTS +# [END getting_started_auth_certs] +# [START getting_started_auth_metadata] def get_metadata(item_name): """Returns a string with the project metadata value for the item_name. See https://cloud.google.com/compute/docs/storing-retrieving-metadata for @@ -53,8 +57,10 @@ def get_metadata(item_name): ) metadata = response.text return metadata +# [END getting_started_auth_metadata] +# [START getting_started_auth_audience] def audience(): """Returns the audience value (the JWT 'aud' property) for the current running instance. Since this involves a metadata lookup, the result is @@ -68,8 +74,10 @@ def audience(): project_number, project_id ) return AUDIENCE +# [END getting_started_auth_audience] +# [START getting_started_auth_validate_assertion] def validate_assertion(assertion): """Checks that the JWT assertion is valid (properly signed, for the correct audience) and if so, returns strings for the requesting user's @@ -88,8 +96,10 @@ def validate_assertion(assertion): except Exception as e: print('Failed to validate assertion: {}'.format(e), file=sys.stderr) return None, None +# [END getting_started_auth_validate_assertion] +# [START getting_started_auth_front_controller] @app.route('/', methods=['GET']) def say_hello(): from flask import request @@ -98,3 +108,5 @@ def say_hello(): email, id = validate_assertion(assertion) page = "

Hello {}

".format(email) return page +# [END getting_started_auth_front_controller] +# [END getting_started_auth_all] diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index 5379a109..464a3752 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -1,4 +1,6 @@ +# [START getting_started_requirements] Flask>=1.0.0 cryptography python-jose[cryptography] requests +# [END getting_started_requirements] From cb1b422d460acf4ecbdb2feee8d6f1fbb955f86d Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Thu, 22 Aug 2019 12:35:40 -0700 Subject: [PATCH 074/395] Added tests for background tasks --- background/app/main_test.py | 80 ++++++++++++++++++++++++++++++++ background/function/main_test.py | 48 +++++++++++++++++++ 2 files changed, 128 insertions(+) diff --git a/background/app/main_test.py b/background/app/main_test.py index 849e178e..312e9c64 100644 --- a/background/app/main_test.py +++ b/background/app/main_test.py @@ -11,3 +11,83 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + +import json +import main +import pytest + + +class MockFirestoreClient(): + def __init__(self, *args, **kwargs): + pass + + def collection(self, name): + return self + + def stream(self): + return [self] + + def to_dict(self): + return { + 'Original': 'This is a test sentence', + 'Language': 'de', + 'Translated': 'This should be in German in a real environment', + 'OriginalLanguage': 'en', + } + + +class MockPublisherClient(): + def __init__(self, *args, **kwargs): + self.messages = [] + + def publish(self, topic_name, json_message): + message = json.loads(json_message) + self.messages.append({ + 'topic': topic_name, + 'message': message, + }) + + +@pytest.yield_fixture +def db(): + client = MockFirestoreClient() + yield client + + +@pytest.yield_fixture +def publisher(): + client = MockPublisherClient() + yield client + + + +def test_index(db, publisher): + main.app.testing = True + main.db = db + main.publisher = publisher + client = main.app.test_client() + + r = client.get('/') + assert r.status_code == 200 + response_text = r.data.decode('utf-8') + assert 'Text to translate' in response_text + assert 'This should be in German' in response_text + + +def test_translate(db, publisher): + main.app.testing = True + main.db = db + main.publisher = publisher + client = main.app.test_client() + + r = client.post('/request-translation', data={ + 'v': 'This is a test', + 'lang': 'fr', + }) + + assert r.status_code < 400 + assert len(publisher.messages) == 1 + assert '/translate' in publisher.messages[0]['topic'] + assert publisher.messages[0]['message']['Original'] == 'This is a test' + assert publisher.messages[0]['message']['Language'] == 'fr' + diff --git a/background/function/main_test.py b/background/function/main_test.py index 849e178e..b1f0aa54 100644 --- a/background/function/main_test.py +++ b/background/function/main_test.py @@ -11,3 +11,51 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + +import base64 +import json +import main +import pytest + + +class MockTranslateClient(): + def __init__(self, *args, **kwargs): + pass + + def translate(self, s, target_language='N/A'): + return { + 'translatedText': 'This is the output string', + 'detectedSourceLanguage': 'en', + } + + +@pytest.yield_fixture +def xlate(): + client = MockTranslateClient() + yield client + + +last_message = None +def dummy_update(transaction, message): + global last_message + last_message = message + + +def test_invocations(xlate): + main.update_database = dummy_update + main.xlate = xlate + + event = { + 'data': base64.b64encode(json.dumps({ + 'Original': 'My test message', + 'Language': 'de', + }).encode('utf-8')) + } + + main.translate_message(event, None) + + assert last_message is not None + assert last_message['Original'] == 'My test message' + assert last_message['Language'] == 'de' + assert last_message['Translated'] == 'This is the output string' + assert last_message['OriginalLanguage'] == 'en' From 9a259b5b833344b7b06c4538dbf1aca978fc3e48 Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Thu, 5 Sep 2019 15:28:47 -0700 Subject: [PATCH 075/395] New FIRESTORE_PROJECT_ID variable in config --- secrets.tar.enc | Bin 10272 -> 10272 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/secrets.tar.enc b/secrets.tar.enc index 2942865e82983145b34aabc99b4418d240dda54e..de79dad221e1fb47560db519ef6a16f1a6d75312 100644 GIT binary patch literal 10272 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 literal 10272 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 From aa79fb73aa5c4cb62218e3234138288519862e3d Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Thu, 5 Sep 2019 23:27:23 +0000 Subject: [PATCH 076/395] Added service account for firestore testing --- encrypt-secrets.sh | 2 +- secrets.tar.enc | Bin 10272 -> 20512 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/encrypt-secrets.sh b/encrypt-secrets.sh index 10f408bb..8da7a394 100755 --- a/encrypt-secrets.sh +++ b/encrypt-secrets.sh @@ -17,7 +17,7 @@ read -s -p "Enter password for encryption: " password echo -tar cvf secrets.tar {service-account.json,config.py} +tar cvf secrets.tar {service-account.json,firestore-service-account.json,config.py} openssl aes-256-cbc -k "$password" -in secrets.tar -out secrets.tar.enc rm secrets.tar diff --git a/secrets.tar.enc b/secrets.tar.enc index de79dad221e1fb47560db519ef6a16f1a6d75312..5b505a77aab371838d308c9d99f0a78279eed983 100644 GIT binary patch literal 20512 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 literal 10272 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 From d75c9e6da4900f9fef649151da6120bf1c4daa73 Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Thu, 5 Sep 2019 16:45:22 -0700 Subject: [PATCH 077/395] Removed mocks from background tasks function test --- background/function/main.py | 3 +- background/function/main_test.py | 58 ++++++++++++++++++-------------- 2 files changed, 34 insertions(+), 27 deletions(-) diff --git a/background/function/main.py b/background/function/main.py index 7681e580..4a4292c5 100644 --- a/background/function/main.py +++ b/background/function/main.py @@ -26,6 +26,7 @@ import base64 import hashlib import json +import os from google.cloud import firestore from google.cloud import translate @@ -76,7 +77,7 @@ def document_name(message): @firestore.transactional def update_database(transaction, message): name = document_name(message) - doc_ref = db.collection("translations").document(document_id=name) + doc_ref = db.collection('translations').document(document_id=name) try: doc_ref.get(transaction=transaction) diff --git a/background/function/main_test.py b/background/function/main_test.py index b1f0aa54..683989af 100644 --- a/background/function/main_test.py +++ b/background/function/main_test.py @@ -14,36 +14,38 @@ import base64 import json -import main +import os import pytest +from google.cloud import firestore -class MockTranslateClient(): - def __init__(self, *args, **kwargs): - pass - - def translate(self, s, target_language='N/A'): - return { - 'translatedText': 'This is the output string', - 'detectedSourceLanguage': 'en', - } +os.environ['GOOGLE_CLOUD_PROJECT'] = os.environ.get('FIRESTORE_PROJECT_ID') +os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = os.path.realpath( + os.path.join( + os.path.dirname(__file__), + '..', + '..', + 'firestore-service-account.json' + ) +) +import main -@pytest.yield_fixture -def xlate(): - client = MockTranslateClient() - yield client +def clear_collection(collection): + """ Removes every document from the collection, to make it easy to see + what has been added by the current test run. + """ + for doc in collection.stream(): + doc.reference.delete() -last_message = None -def dummy_update(transaction, message): - global last_message - last_message = message +def test_invocations(): + db = firestore.Client(project=os.environ['FIRESTORE_PROJECT_ID']) + main.db = db -def test_invocations(xlate): - main.update_database = dummy_update - main.xlate = xlate + translations = db.collection('translations') + clear_collection(translations) event = { 'data': base64.b64encode(json.dumps({ @@ -54,8 +56,12 @@ def test_invocations(xlate): main.translate_message(event, None) - assert last_message is not None - assert last_message['Original'] == 'My test message' - assert last_message['Language'] == 'de' - assert last_message['Translated'] == 'This is the output string' - assert last_message['OriginalLanguage'] == 'en' + docs = [doc for doc in translations.stream()] + assert len(docs) == 1 # Should be only the one just created + + message = docs[0].to_dict() + + assert message['Original'] == 'My test message' + assert message['Language'] == 'de' + assert len(message['Translated']) > 0 + assert message['OriginalLanguage'] == 'en' From a00fd62db7fd5da55e4cbfe77469b477739ed05a Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Fri, 6 Sep 2019 12:13:17 -0700 Subject: [PATCH 078/395] All unmocked tests for background --- background/app/main_test.py | 91 ++++++++++++++++++++++--------------- noxfile.py | 1 + 2 files changed, 55 insertions(+), 37 deletions(-) diff --git a/background/app/main_test.py b/background/app/main_test.py index 312e9c64..d5cccc91 100644 --- a/background/app/main_test.py +++ b/background/app/main_test.py @@ -12,54 +12,71 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json -import main +import os import pytest +import uuid +from google.cloud import firestore +from google.cloud import pubsub -class MockFirestoreClient(): - def __init__(self, *args, **kwargs): - pass - - def collection(self, name): - return self - - def stream(self): - return [self] - - def to_dict(self): - return { - 'Original': 'This is a test sentence', - 'Language': 'de', - 'Translated': 'This should be in German in a real environment', - 'OriginalLanguage': 'en', - } +os.environ['GOOGLE_CLOUD_PROJECT'] = os.environ.get('FIRESTORE_PROJECT_ID') +os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = os.path.realpath( + os.path.join( + os.path.dirname(__file__), + '..', + '..', + 'firestore-service-account.json' + ) +) +SUBSCRIPTION_NAME = 'projects/{}/subscriptions/{}'.format( + os.getenv('FIRESTORE_PROJECT_ID'), 'test-' + str(uuid.uuid4()) +) -class MockPublisherClient(): - def __init__(self, *args, **kwargs): - self.messages = [] - - def publish(self, topic_name, json_message): - message = json.loads(json_message) - self.messages.append({ - 'topic': topic_name, - 'message': message, - }) +import main @pytest.yield_fixture def db(): - client = MockFirestoreClient() + def clear_collection(collection): + """ Removes every document from the collection, to make it easy to see + what has been added by the current test run. + """ + for doc in collection.stream(): + doc.reference.delete() + + client = firestore.Client() + translations = client.collection('translations') + clear_collection(translations) + translations.add({ + 'Original': 'A testing message', + 'Language': 'fr', + 'Translated': '"A testing message", but in French', + 'OriginalLanguage': 'en', + }, + document_id='test translation' + ) yield client @pytest.yield_fixture def publisher(): - client = MockPublisherClient() + client = pubsub.PublisherClient() yield client +@pytest.yield_fixture +def subscriber(): + subscriber = pubsub.SubscriberClient() + topic_name = 'projects/{}/topics/{}'.format( + os.getenv('FIRESTORE_PROJECT_ID'), 'translate' + ) + subscription = subscriber.create_subscription( + SUBSCRIPTION_NAME, topic_name + ) + yield subscriber + subscriber.delete_subscription(SUBSCRIPTION_NAME) + def test_index(db, publisher): main.app.testing = True @@ -71,10 +88,10 @@ def test_index(db, publisher): assert r.status_code == 200 response_text = r.data.decode('utf-8') assert 'Text to translate' in response_text - assert 'This should be in German' in response_text + assert 'but in French' in response_text -def test_translate(db, publisher): +def test_translate(db, publisher, subscriber): main.app.testing = True main.db = db main.publisher = publisher @@ -86,8 +103,8 @@ def test_translate(db, publisher): }) assert r.status_code < 400 - assert len(publisher.messages) == 1 - assert '/translate' in publisher.messages[0]['topic'] - assert publisher.messages[0]['message']['Original'] == 'This is a test' - assert publisher.messages[0]['message']['Language'] == 'fr' + response = subscriber.pull(SUBSCRIPTION_NAME, 1, timeout=2.0) + assert len(response.received_messages) == 1 + assert b'This is a test' in response.received_messages[0].message.data + assert b'fr' in response.received_messages[0].message.data diff --git a/noxfile.py b/noxfile.py index 35d37061..d1df9aa8 100644 --- a/noxfile.py +++ b/noxfile.py @@ -16,6 +16,7 @@ '5-logging', '6-pubsub', '7-gce', + 'background', ] PYTEST_COMMON_ARGS = ['--junitxml=sponge_log.xml', '-m', 'not e2e'] From 0677a551f35936abdc7e1a68eb1eb7d059977327 Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Fri, 6 Sep 2019 12:44:02 -0700 Subject: [PATCH 079/395] Update noxfile.py --- noxfile.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index d1df9aa8..427387ea 100644 --- a/noxfile.py +++ b/noxfile.py @@ -16,7 +16,8 @@ '5-logging', '6-pubsub', '7-gce', - 'background', + 'background/app', + 'background/function', ] PYTEST_COMMON_ARGS = ['--junitxml=sponge_log.xml', '-m', 'not e2e'] From ea2e43ddedaa2965a7314453df6b8a1f02a17e28 Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Fri, 6 Sep 2019 13:28:20 -0700 Subject: [PATCH 080/395] Update main_test.py --- background/app/main_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/app/main_test.py b/background/app/main_test.py index d5cccc91..c1108400 100644 --- a/background/app/main_test.py +++ b/background/app/main_test.py @@ -19,7 +19,7 @@ from google.cloud import firestore from google.cloud import pubsub -os.environ['GOOGLE_CLOUD_PROJECT'] = os.environ.get('FIRESTORE_PROJECT_ID') +os.environ['GOOGLE_CLOUD_PROJECT'] = FIRESTORE_PROJECT_ID os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = os.path.realpath( os.path.join( os.path.dirname(__file__), From 39fa0a1dc7a4f88d402f7e8a3cac69a4318a4cbe Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Fri, 6 Sep 2019 13:28:47 -0700 Subject: [PATCH 081/395] Update main_test.py --- background/function/main_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/function/main_test.py b/background/function/main_test.py index 683989af..c800908f 100644 --- a/background/function/main_test.py +++ b/background/function/main_test.py @@ -19,7 +19,7 @@ from google.cloud import firestore -os.environ['GOOGLE_CLOUD_PROJECT'] = os.environ.get('FIRESTORE_PROJECT_ID') +os.environ['GOOGLE_CLOUD_PROJECT'] = FIRESTORE_PROJECT_ID os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = os.path.realpath( os.path.join( os.path.dirname(__file__), From bf44ffb2561609924d4d6325594c4f74c001d778 Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Tue, 17 Sep 2019 12:25:42 -0700 Subject: [PATCH 082/395] Added session management example --- sessions/app.yaml | 17 +++++++++++ sessions/main.py | 64 +++++++++++++++++++++++++++++++++++++++ sessions/requirements.txt | 2 ++ 3 files changed, 83 insertions(+) create mode 100644 sessions/app.yaml create mode 100644 sessions/main.py create mode 100644 sessions/requirements.txt diff --git a/sessions/app.yaml b/sessions/app.yaml new file mode 100644 index 00000000..0b2d0e28 --- /dev/null +++ b/sessions/app.yaml @@ -0,0 +1,17 @@ +# Copyright 2019 Google LLC All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START getting_started_sessions_runtime] +runtime: python37 +# [END getting_started_sessions_runtime] diff --git a/sessions/main.py b/sessions/main.py new file mode 100644 index 00000000..50612751 --- /dev/null +++ b/sessions/main.py @@ -0,0 +1,64 @@ +# Copyright 2019 Google LLC All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import random +from uuid import uuid4 + +from google.cloud import firestore +from flask import Flask, make_response, request + + +app = Flask(__name__) +sessions = firestore.Client().collection('sessions') +colors = ['red', 'blue', 'green', 'yellow', 'pink'] + + +# Updates and returns the info for given session_id. If there is no session_id, +# it creates a random one. If there is no session data, it creates and stores +# initial values (1 view, random color). Updates the session by incrementing +# the number of views. +def update_session(session_id): + if session_id is None: + session_id = str(uuid4()) # Random, unique identifier + + doc = sessions.document(session_id).get() + if doc.exists: + session = doc.to_dict() + prior_views = session.get('views', 0) + session['views'] = prior_views + 1 + doc.reference.set(session) + else: + session = { + 'color': random.choice(colors), + 'views': 1 + } + sessions.add(session, document_id=session_id) + + session['session_id'] = session_id + return session + + +@app.route('/', methods=['GET']) +def home(): + template = 'Views {}' + + session = update_session(request.cookies.get('session_id')) + + resp = make_response(template.format(session['color'], session['views'])) + resp.set_cookie('session_id', session['session_id'], httponly=True) + return resp + + +if __name__ == '__main__': + app.run(host='127.0.0.1', port=8080) diff --git a/sessions/requirements.txt b/sessions/requirements.txt new file mode 100644 index 00000000..6bbf2aa4 --- /dev/null +++ b/sessions/requirements.txt @@ -0,0 +1,2 @@ +google-cloud-firestore==1.4.0 +flask>=1.0.0 From edf60e85e2a37202e1a064823726f9a318354d9e Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Fri, 20 Sep 2019 11:45:06 -0700 Subject: [PATCH 083/395] Updates now inside transactions. Tests added. --- sessions/main.py | 30 ++++++++++--------- sessions/main_test.py | 54 +++++++++++++++++++++++++++++++++++ sessions/requirements-dev.txt | 1 + 3 files changed, 72 insertions(+), 13 deletions(-) create mode 100644 sessions/main_test.py create mode 100644 sessions/requirements-dev.txt diff --git a/sessions/main.py b/sessions/main.py index 50612751..d88b1adf 100644 --- a/sessions/main.py +++ b/sessions/main.py @@ -20,30 +20,33 @@ app = Flask(__name__) -sessions = firestore.Client().collection('sessions') +db = firestore.Client() +sessions = db.collection('sessions') colors = ['red', 'blue', 'green', 'yellow', 'pink'] -# Updates and returns the info for given session_id. If there is no session_id, -# it creates a random one. If there is no session data, it creates and stores -# initial values (1 view, random color). Updates the session by incrementing -# the number of views. -def update_session(session_id): +@firestore.transactional +def get_session_data(transaction, session_id): + """ Looks up (or creates) the session with the given session_id. + Creates a random session_id if none is provided. Increments + the number of views in this session. Updates are done in a + transaction to make sure no saved increments are overwritten. + """ if session_id is None: session_id = str(uuid4()) # Random, unique identifier - doc = sessions.document(session_id).get() + doc_ref = sessions.document(document_id=session_id) + doc = doc_ref.get(transaction=transaction) if doc.exists: session = doc.to_dict() - prior_views = session.get('views', 0) - session['views'] = prior_views + 1 - doc.reference.set(session) else: session = { 'color': random.choice(colors), - 'views': 1 + 'views': 0 } - sessions.add(session, document_id=session_id) + + session['views'] += 1 # This counts as a view + transaction.set(doc_ref, session) session['session_id'] = session_id return session @@ -53,7 +56,8 @@ def update_session(session_id): def home(): template = 'Views {}' - session = update_session(request.cookies.get('session_id')) + transaction = db.transaction() + session = get_session_data(transaction, request.cookies.get('session_id')) resp = make_response(template.format(session['color'], session['views'])) resp.set_cookie('session_id', session['session_id'], httponly=True) diff --git a/sessions/main_test.py b/sessions/main_test.py new file mode 100644 index 00000000..ff9d0ac9 --- /dev/null +++ b/sessions/main_test.py @@ -0,0 +1,54 @@ +# Copyright 2019 Google LLC All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import main +import pytest +import re +import uuid + + +@pytest.fixture +def client(): + """ Yields a test client, AND creates and later cleans up a + dummy collection for sessions. + """ + main.app.testing = True + + # Override the Firestore collection used for sessions in main + main.sessions = main.db.collection(str(uuid.uuid4())) + + client = main.app.test_client() + yield client + + # Clean up session objects created in test collection + for doc_ref in main.sessions.list_documents(): + doc_ref.delete() + + +def test_session(client): + r = client.get('/') + assert r.status_code == 200 + data = r.data.decode('utf-8') + assert 'Views 1' in data + + match = re.search('bgcolor=([a-z]+)', data) + assert match is not None + color = match.group(1) + + r = client.get('/') + assert r.status_code == 200 + data = r.data.decode('utf-8') + assert 'Views 2' in data + assert color in data diff --git a/sessions/requirements-dev.txt b/sessions/requirements-dev.txt new file mode 100644 index 00000000..fe969938 --- /dev/null +++ b/sessions/requirements-dev.txt @@ -0,0 +1 @@ +pytest>=5.0.0 From 9d89ceabfff362fb5918698f8c8e4772f26b0916 Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Fri, 20 Sep 2019 11:49:27 -0700 Subject: [PATCH 084/395] Set variable pointing to Firestore DB project --- .kokoro/system_tests.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/.kokoro/system_tests.sh b/.kokoro/system_tests.sh index 3ece8473..c3061f0a 100755 --- a/.kokoro/system_tests.sh +++ b/.kokoro/system_tests.sh @@ -26,6 +26,7 @@ SECRETS_PASSWORD=$(cat "${KOKORO_GFILE_DIR}/secrets-password.txt") # Setup environment variables export GOOGLE_APPLICATION_CREDENTIALS="$(pwd)/service-account.json" +export FIRESTORE_PROJECT_ID="getting-started-python-tests" # Run tests nox -s run_tests From b74710cba7a9fd4af29b98e1b546a9f5bba0cdb9 Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Fri, 20 Sep 2019 12:11:37 -0700 Subject: [PATCH 085/395] Provide Firestore project ID in testing environment --- .kokoro/common.cfg | 5 +++++ .kokoro/system_tests.sh | 1 - 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.kokoro/common.cfg b/.kokoro/common.cfg index 4af0df96..f7909506 100644 --- a/.kokoro/common.cfg +++ b/.kokoro/common.cfg @@ -11,3 +11,8 @@ env_vars: { key: "TRAMPOLINE_IMAGE" value: "gcr.io/cloud-devrel-kokoro-resources/python@sha256:4b6ba8c199e96248980db4538065cddeea594138b9b9fb2d0388603922087747" } + +env_vars: { + key: "FIRESTORE_PROJECT_ID" + value: "getting-started-python-tests" +} diff --git a/.kokoro/system_tests.sh b/.kokoro/system_tests.sh index c3061f0a..3ece8473 100755 --- a/.kokoro/system_tests.sh +++ b/.kokoro/system_tests.sh @@ -26,7 +26,6 @@ SECRETS_PASSWORD=$(cat "${KOKORO_GFILE_DIR}/secrets-password.txt") # Setup environment variables export GOOGLE_APPLICATION_CREDENTIALS="$(pwd)/service-account.json" -export FIRESTORE_PROJECT_ID="getting-started-python-tests" # Run tests nox -s run_tests From 84adfdaa0d9d37808f73f5f4d9fe2b7c8289b3b2 Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Fri, 20 Sep 2019 13:32:48 -0700 Subject: [PATCH 086/395] Fixed blind spot in specifying environment --- background/app/main_test.py | 2 +- background/function/main_test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/background/app/main_test.py b/background/app/main_test.py index c1108400..cef9c154 100644 --- a/background/app/main_test.py +++ b/background/app/main_test.py @@ -19,7 +19,7 @@ from google.cloud import firestore from google.cloud import pubsub -os.environ['GOOGLE_CLOUD_PROJECT'] = FIRESTORE_PROJECT_ID +os.environ['GOOGLE_CLOUD_PROJECT'] = os.environ['FIRESTORE_PROJECT_ID'] os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = os.path.realpath( os.path.join( os.path.dirname(__file__), diff --git a/background/function/main_test.py b/background/function/main_test.py index c800908f..823ddb12 100644 --- a/background/function/main_test.py +++ b/background/function/main_test.py @@ -19,7 +19,7 @@ from google.cloud import firestore -os.environ['GOOGLE_CLOUD_PROJECT'] = FIRESTORE_PROJECT_ID +os.environ['GOOGLE_CLOUD_PROJECT'] = os.environ['FIRESTORE_PROJECT_ID'] os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = os.path.realpath( os.path.join( os.path.dirname(__file__), From d4cbf0a3f84f8c7428c9b40d6181e8c9a55ecd71 Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Fri, 20 Sep 2019 13:53:31 -0700 Subject: [PATCH 087/395] Override environment to point to special Firestore-enabled project --- sessions/main_test.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/sessions/main_test.py b/sessions/main_test.py index ff9d0ac9..62ea3bc6 100644 --- a/sessions/main_test.py +++ b/sessions/main_test.py @@ -13,11 +13,21 @@ # limitations under the License. -import main import pytest import re import uuid +os.environ['GOOGLE_CLOUD_PROJECT'] = os.environ['FIRESTORE_PROJECT_ID'] +os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = os.path.realpath( + os.path.join( + os.path.dirname(__file__), + '..', + 'firestore-service-account.json' + ) +) + +import main + @pytest.fixture def client(): From 5d9ce29572573538a65a4eebf34436657dc726a1 Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Mon, 23 Sep 2019 11:03:28 -0700 Subject: [PATCH 088/395] Configure for automated tests --- .kokoro/common.cfg | 5 +++++ noxfile.py | 13 +++++++------ sessions/main_test.py | 2 +- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/.kokoro/common.cfg b/.kokoro/common.cfg index 4af0df96..f7909506 100644 --- a/.kokoro/common.cfg +++ b/.kokoro/common.cfg @@ -11,3 +11,8 @@ env_vars: { key: "TRAMPOLINE_IMAGE" value: "gcr.io/cloud-devrel-kokoro-resources/python@sha256:4b6ba8c199e96248980db4538065cddeea594138b9b9fb2d0388603922087747" } + +env_vars: { + key: "FIRESTORE_PROJECT_ID" + value: "getting-started-python-tests" +} diff --git a/noxfile.py b/noxfile.py index 35d37061..fc498b34 100644 --- a/noxfile.py +++ b/noxfile.py @@ -10,12 +10,13 @@ # Hello world doesn't have system tests, just a lint test which will be # covered by the global lint here. # '1-hello-world', - '2-structured-data', - '3-binary-data', - '4-auth', - '5-logging', - '6-pubsub', - '7-gce', + #'2-structured-data', + #'3-binary-data', + #'4-auth', + #'5-logging', + #'6-pubsub', + #'7-gce', + sessions ] PYTEST_COMMON_ARGS = ['--junitxml=sponge_log.xml', '-m', 'not e2e'] diff --git a/sessions/main_test.py b/sessions/main_test.py index 62ea3bc6..01532093 100644 --- a/sessions/main_test.py +++ b/sessions/main_test.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. - +import os import pytest import re import uuid From 82d84e59c02229b5afedd718b0697c0d7e3a8c3d Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Mon, 23 Sep 2019 11:16:55 -0700 Subject: [PATCH 089/395] Typo --- noxfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index fc498b34..5b4bc194 100644 --- a/noxfile.py +++ b/noxfile.py @@ -16,7 +16,7 @@ #'5-logging', #'6-pubsub', #'7-gce', - sessions + 'sessions', ] PYTEST_COMMON_ARGS = ['--junitxml=sponge_log.xml', '-m', 'not e2e'] From a88ab9af15f515342cdf7339b4f5d4483fc6cc86 Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Mon, 23 Sep 2019 11:29:34 -0700 Subject: [PATCH 090/395] Need new secrets file to run tests --- secrets.tar.enc | Bin 10272 -> 20512 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/secrets.tar.enc b/secrets.tar.enc index 2942865e82983145b34aabc99b4418d240dda54e..5b505a77aab371838d308c9d99f0a78279eed983 100644 GIT binary patch literal 20512 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 literal 10272 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 From d3a40966a06493677037f8368259f46d6a36e4ac Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Mon, 23 Sep 2019 11:38:30 -0700 Subject: [PATCH 091/395] Resolve conflict --- noxfile.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/noxfile.py b/noxfile.py index 5b4bc194..75e2431b 100644 --- a/noxfile.py +++ b/noxfile.py @@ -10,12 +10,14 @@ # Hello world doesn't have system tests, just a lint test which will be # covered by the global lint here. # '1-hello-world', - #'2-structured-data', - #'3-binary-data', - #'4-auth', - #'5-logging', - #'6-pubsub', - #'7-gce', + '2-structured-data', + '3-binary-data', + '4-auth', + '5-logging', + '6-pubsub', + '7-gce', + 'background/app', + 'background/function', 'sessions', ] From f04a5fad072abf4f7c5441cac2d333b0ff136169 Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Mon, 28 Oct 2019 11:56:36 -0700 Subject: [PATCH 092/395] Use different languages instead of background colors --- sessions/main.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/sessions/main.py b/sessions/main.py index d88b1adf..04e916ca 100644 --- a/sessions/main.py +++ b/sessions/main.py @@ -22,7 +22,13 @@ app = Flask(__name__) db = firestore.Client() sessions = db.collection('sessions') -colors = ['red', 'blue', 'green', 'yellow', 'pink'] +greetings = [ + 'Hello World', + 'Hallo Welt', + 'Ciao Mondo', + 'Salut le Monde', + 'Hola Mundo', +] @firestore.transactional @@ -41,7 +47,7 @@ def get_session_data(transaction, session_id): session = doc.to_dict() else: session = { - 'color': random.choice(colors), + 'greeting': random.choice(greetings), 'views': 0 } @@ -54,12 +60,16 @@ def get_session_data(transaction, session_id): @app.route('/', methods=['GET']) def home(): - template = 'Views {}' + template = '{} views for {}' transaction = db.transaction() session = get_session_data(transaction, request.cookies.get('session_id')) - resp = make_response(template.format(session['color'], session['views'])) + resp = make_response(template.format( + session['views'], + session['greeting'] + ) + ) resp.set_cookie('session_id', session['session_id'], httponly=True) return resp From f16451bbcda93ad471dcb5be50bc470451fde708 Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Mon, 28 Oct 2019 12:03:45 -0700 Subject: [PATCH 093/395] Updated test to match --- sessions/main_test.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sessions/main_test.py b/sessions/main_test.py index 01532093..f4c99ffb 100644 --- a/sessions/main_test.py +++ b/sessions/main_test.py @@ -51,14 +51,14 @@ def test_session(client): r = client.get('/') assert r.status_code == 200 data = r.data.decode('utf-8') - assert 'Views 1' in data + assert '1 views' in data - match = re.search('bgcolor=([a-z]+)', data) + match = re.search('views for ([A-Za-z ]+)', data) assert match is not None - color = match.group(1) + greeting = match.group(1) r = client.get('/') assert r.status_code == 200 data = r.data.decode('utf-8') - assert 'Views 2' in data - assert color in data + assert '2 views' in data + assert greeting in data From 78abdb5cbcc63c7d5fe8c4516ff6b18c8eea5050 Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Tue, 24 Sep 2019 10:49:02 -0700 Subject: [PATCH 094/395] Cleaned out old steps examples --- 1-hello-world/app.yaml | 26 --- 1-hello-world/main.py | 30 ---- 1-hello-world/requirements.txt | 2 - 1-hello-world/tox.ini | 10 -- 2-structured-data/app.yaml | 34 ---- 2-structured-data/bookshelf/__init__.py | 77 -------- 2-structured-data/bookshelf/crud.py | 75 -------- 2-structured-data/bookshelf/model_cloudsql.py | 117 ------------ .../bookshelf/model_datastore.py | 99 ----------- 2-structured-data/bookshelf/model_mongodb.py | 86 --------- .../bookshelf/templates/base.html | 40 ----- .../bookshelf/templates/form.html | 49 ----- .../bookshelf/templates/list.html | 51 ------ .../bookshelf/templates/view.html | 48 ----- 2-structured-data/config.py | 79 -------- 2-structured-data/main.py | 25 --- 2-structured-data/requirements-dev.txt | 6 - 2-structured-data/requirements.txt | 9 - 2-structured-data/tests/conftest.py | 91 ---------- 2-structured-data/tests/test_crud.py | 88 --------- 2-structured-data/tox.ini | 19 -- 3-binary-data/app.yaml | 31 ---- 3-binary-data/bookshelf/__init__.py | 77 -------- 3-binary-data/bookshelf/crud.py | 109 ------------ 3-binary-data/bookshelf/model_cloudsql.py | 107 ----------- 3-binary-data/bookshelf/model_datastore.py | 93 ---------- 3-binary-data/bookshelf/model_mongodb.py | 85 --------- 3-binary-data/bookshelf/storage.py | 74 -------- 3-binary-data/bookshelf/templates/base.html | 40 ----- 3-binary-data/bookshelf/templates/form.html | 59 ------ 3-binary-data/bookshelf/templates/list.html | 55 ------ 3-binary-data/bookshelf/templates/view.html | 54 ------ 3-binary-data/config.py | 95 ---------- 3-binary-data/main.py | 25 --- 3-binary-data/requirements-dev.txt | 6 - 3-binary-data/requirements.txt | 10 -- 3-binary-data/tests/conftest.py | 91 ---------- 3-binary-data/tests/test_crud.py | 88 --------- 3-binary-data/tests/test_storage.py | 70 -------- 3-binary-data/tox.ini | 19 -- 4-auth/app.yaml | 32 ---- 4-auth/bookshelf/__init__.py | 125 ------------- 4-auth/bookshelf/crud.py | 129 -------------- 4-auth/bookshelf/model_cloudsql.py | 121 ------------- 4-auth/bookshelf/model_datastore.py | 115 ------------ 4-auth/bookshelf/model_mongodb.py | 100 ----------- 4-auth/bookshelf/storage.py | 72 -------- 4-auth/bookshelf/templates/base.html | 55 ------ 4-auth/bookshelf/templates/form.html | 67 ------- 4-auth/bookshelf/templates/list.html | 55 ------ 4-auth/bookshelf/templates/view.html | 53 ------ 4-auth/config.py | 112 ------------ 4-auth/main.py | 25 --- 4-auth/requirements-dev.txt | 7 - 4-auth/requirements.txt | 10 -- 4-auth/tests/conftest.py | 91 ---------- 4-auth/tests/test_auth.py | 133 -------------- 4-auth/tests/test_crud.py | 90 ---------- 4-auth/tests/test_storage.py | 70 -------- 4-auth/tox.ini | 19 -- 5-logging/app.yaml | 32 ---- 5-logging/bookshelf/__init__.py | 126 ------------- 5-logging/bookshelf/crud.py | 125 ------------- 5-logging/bookshelf/model_cloudsql.py | 119 ------------- 5-logging/bookshelf/model_datastore.py | 113 ------------ 5-logging/bookshelf/model_mongodb.py | 100 ----------- 5-logging/bookshelf/storage.py | 72 -------- 5-logging/bookshelf/templates/base.html | 53 ------ 5-logging/bookshelf/templates/form.html | 67 ------- 5-logging/bookshelf/templates/list.html | 55 ------ 5-logging/bookshelf/templates/view.html | 53 ------ 5-logging/config.py | 110 ------------ 5-logging/main.py | 25 --- 5-logging/requirements-dev.txt | 7 - 5-logging/requirements.txt | 12 -- 5-logging/tests/conftest.py | 91 ---------- 5-logging/tests/test_auth.py | 133 -------------- 5-logging/tests/test_crud.py | 88 --------- 5-logging/tests/test_storage.py | 70 -------- 5-logging/tox.ini | 19 -- 6-pubsub/app.yaml | 38 ---- 6-pubsub/bookshelf/__init__.py | 122 ------------- 6-pubsub/bookshelf/crud.py | 133 -------------- 6-pubsub/bookshelf/model_cloudsql.py | 119 ------------- 6-pubsub/bookshelf/model_datastore.py | 113 ------------ 6-pubsub/bookshelf/model_mongodb.py | 100 ----------- 6-pubsub/bookshelf/storage.py | 72 -------- 6-pubsub/bookshelf/tasks.py | 127 ------------- 6-pubsub/bookshelf/templates/base.html | 53 ------ 6-pubsub/bookshelf/templates/form.html | 67 ------- 6-pubsub/bookshelf/templates/list.html | 55 ------ 6-pubsub/bookshelf/templates/view.html | 53 ------ 6-pubsub/config.py | 113 ------------ 6-pubsub/main.py | 36 ---- 6-pubsub/monitor.py | 56 ------ 6-pubsub/procfile | 3 - 6-pubsub/requirements-dev.txt | 9 - 6-pubsub/requirements.txt | 16 -- 6-pubsub/tests/conftest.py | 91 ---------- 6-pubsub/tests/test_auth.py | 133 -------------- 6-pubsub/tests/test_crud.py | 88 --------- 6-pubsub/tests/test_end_to_end.py | 80 --------- 6-pubsub/tests/test_storage.py | 70 -------- 6-pubsub/tox.ini | 30 ---- 6-pubsub/worker.yaml | 40 ----- 7-gce/bookshelf/__init__.py | 131 -------------- 7-gce/bookshelf/crud.py | 131 -------------- 7-gce/bookshelf/model_cloudsql.py | 119 ------------- 7-gce/bookshelf/model_datastore.py | 113 ------------ 7-gce/bookshelf/model_mongodb.py | 100 ----------- 7-gce/bookshelf/storage.py | 72 -------- 7-gce/bookshelf/tasks.py | 121 ------------- 7-gce/bookshelf/templates/base.html | 40 ----- 7-gce/bookshelf/templates/form.html | 67 ------- 7-gce/bookshelf/templates/list.html | 55 ------ 7-gce/bookshelf/templates/view.html | 53 ------ 7-gce/config.py | 110 ------------ 7-gce/gce/deploy.sh | 159 ----------------- 7-gce/gce/deployment_manager/bookshelf.jinja | 168 ------------------ .../deployment_manager/bookshelf.jinja.schema | 59 ------ 7-gce/gce/deployment_manager/config.yaml | 33 ---- 7-gce/gce/startup-script.sh | 76 -------- 7-gce/gce/teardown.sh | 41 ----- 7-gce/main.py | 34 ---- 7-gce/monitor.py | 54 ------ 7-gce/procfile | 3 - 7-gce/requirements-dev.txt | 9 - 7-gce/requirements.txt | 15 -- 7-gce/tests/conftest.py | 91 ---------- 7-gce/tests/test_auth.py | 133 -------------- 7-gce/tests/test_crud.py | 88 --------- 7-gce/tests/test_end_to_end.py | 80 --------- 7-gce/tests/test_storage.py | 70 -------- 7-gce/tox.ini | 30 ---- noxfile.py | 7 - optional-kubernetes-engine/.dockerignore | 17 -- optional-kubernetes-engine/Dockerfile | 27 --- optional-kubernetes-engine/Makefile | 50 ------ optional-kubernetes-engine/README.md | 97 ---------- .../bookshelf-frontend.yaml | 53 ------ .../bookshelf-service.yaml | 36 ---- .../bookshelf-worker.yaml | 49 ----- .../bookshelf/__init__.py | 125 ------------- optional-kubernetes-engine/bookshelf/crud.py | 131 -------------- .../bookshelf/model_cloudsql.py | 119 ------------- .../bookshelf/model_datastore.py | 113 ------------ .../bookshelf/model_mongodb.py | 99 ----------- .../bookshelf/storage.py | 72 -------- optional-kubernetes-engine/bookshelf/tasks.py | 121 ------------- .../bookshelf/templates/base.html | 40 ----- .../bookshelf/templates/form.html | 67 ------- .../bookshelf/templates/list.html | 55 ------ .../bookshelf/templates/view.html | 53 ------ optional-kubernetes-engine/config.py | 107 ----------- optional-kubernetes-engine/main.py | 36 ---- optional-kubernetes-engine/procfile | 2 - .../requirements-dev.txt | 8 - optional-kubernetes-engine/requirements.txt | 16 -- optional-kubernetes-engine/tests/conftest.py | 91 ---------- optional-kubernetes-engine/tests/test_auth.py | 133 -------------- optional-kubernetes-engine/tests/test_crud.py | 88 --------- .../tests/test_end_to_end.py | 80 --------- .../tests/test_storage.py | 70 -------- optional-kubernetes-engine/tox.ini | 30 ---- 164 files changed, 11264 deletions(-) delete mode 100644 1-hello-world/app.yaml delete mode 100644 1-hello-world/main.py delete mode 100644 1-hello-world/requirements.txt delete mode 100644 1-hello-world/tox.ini delete mode 100644 2-structured-data/app.yaml delete mode 100644 2-structured-data/bookshelf/__init__.py delete mode 100644 2-structured-data/bookshelf/crud.py delete mode 100644 2-structured-data/bookshelf/model_cloudsql.py delete mode 100644 2-structured-data/bookshelf/model_datastore.py delete mode 100644 2-structured-data/bookshelf/model_mongodb.py delete mode 100644 2-structured-data/bookshelf/templates/base.html delete mode 100644 2-structured-data/bookshelf/templates/form.html delete mode 100644 2-structured-data/bookshelf/templates/list.html delete mode 100644 2-structured-data/bookshelf/templates/view.html delete mode 100644 2-structured-data/config.py delete mode 100644 2-structured-data/main.py delete mode 100644 2-structured-data/requirements-dev.txt delete mode 100644 2-structured-data/requirements.txt delete mode 100644 2-structured-data/tests/conftest.py delete mode 100644 2-structured-data/tests/test_crud.py delete mode 100644 2-structured-data/tox.ini delete mode 100644 3-binary-data/app.yaml delete mode 100644 3-binary-data/bookshelf/__init__.py delete mode 100644 3-binary-data/bookshelf/crud.py delete mode 100644 3-binary-data/bookshelf/model_cloudsql.py delete mode 100644 3-binary-data/bookshelf/model_datastore.py delete mode 100644 3-binary-data/bookshelf/model_mongodb.py delete mode 100644 3-binary-data/bookshelf/storage.py delete mode 100644 3-binary-data/bookshelf/templates/base.html delete mode 100644 3-binary-data/bookshelf/templates/form.html delete mode 100644 3-binary-data/bookshelf/templates/list.html delete mode 100644 3-binary-data/bookshelf/templates/view.html delete mode 100644 3-binary-data/config.py delete mode 100644 3-binary-data/main.py delete mode 100644 3-binary-data/requirements-dev.txt delete mode 100644 3-binary-data/requirements.txt delete mode 100644 3-binary-data/tests/conftest.py delete mode 100644 3-binary-data/tests/test_crud.py delete mode 100644 3-binary-data/tests/test_storage.py delete mode 100644 3-binary-data/tox.ini delete mode 100644 4-auth/app.yaml delete mode 100644 4-auth/bookshelf/__init__.py delete mode 100644 4-auth/bookshelf/crud.py delete mode 100644 4-auth/bookshelf/model_cloudsql.py delete mode 100644 4-auth/bookshelf/model_datastore.py delete mode 100644 4-auth/bookshelf/model_mongodb.py delete mode 100644 4-auth/bookshelf/storage.py delete mode 100644 4-auth/bookshelf/templates/base.html delete mode 100644 4-auth/bookshelf/templates/form.html delete mode 100644 4-auth/bookshelf/templates/list.html delete mode 100644 4-auth/bookshelf/templates/view.html delete mode 100644 4-auth/config.py delete mode 100644 4-auth/main.py delete mode 100644 4-auth/requirements-dev.txt delete mode 100644 4-auth/requirements.txt delete mode 100644 4-auth/tests/conftest.py delete mode 100644 4-auth/tests/test_auth.py delete mode 100644 4-auth/tests/test_crud.py delete mode 100644 4-auth/tests/test_storage.py delete mode 100644 4-auth/tox.ini delete mode 100644 5-logging/app.yaml delete mode 100644 5-logging/bookshelf/__init__.py delete mode 100644 5-logging/bookshelf/crud.py delete mode 100644 5-logging/bookshelf/model_cloudsql.py delete mode 100644 5-logging/bookshelf/model_datastore.py delete mode 100644 5-logging/bookshelf/model_mongodb.py delete mode 100644 5-logging/bookshelf/storage.py delete mode 100644 5-logging/bookshelf/templates/base.html delete mode 100644 5-logging/bookshelf/templates/form.html delete mode 100644 5-logging/bookshelf/templates/list.html delete mode 100644 5-logging/bookshelf/templates/view.html delete mode 100644 5-logging/config.py delete mode 100644 5-logging/main.py delete mode 100644 5-logging/requirements-dev.txt delete mode 100644 5-logging/requirements.txt delete mode 100644 5-logging/tests/conftest.py delete mode 100644 5-logging/tests/test_auth.py delete mode 100644 5-logging/tests/test_crud.py delete mode 100644 5-logging/tests/test_storage.py delete mode 100644 5-logging/tox.ini delete mode 100644 6-pubsub/app.yaml delete mode 100644 6-pubsub/bookshelf/__init__.py delete mode 100644 6-pubsub/bookshelf/crud.py delete mode 100644 6-pubsub/bookshelf/model_cloudsql.py delete mode 100644 6-pubsub/bookshelf/model_datastore.py delete mode 100644 6-pubsub/bookshelf/model_mongodb.py delete mode 100644 6-pubsub/bookshelf/storage.py delete mode 100644 6-pubsub/bookshelf/tasks.py delete mode 100644 6-pubsub/bookshelf/templates/base.html delete mode 100644 6-pubsub/bookshelf/templates/form.html delete mode 100644 6-pubsub/bookshelf/templates/list.html delete mode 100644 6-pubsub/bookshelf/templates/view.html delete mode 100644 6-pubsub/config.py delete mode 100644 6-pubsub/main.py delete mode 100644 6-pubsub/monitor.py delete mode 100644 6-pubsub/procfile delete mode 100644 6-pubsub/requirements-dev.txt delete mode 100644 6-pubsub/requirements.txt delete mode 100644 6-pubsub/tests/conftest.py delete mode 100644 6-pubsub/tests/test_auth.py delete mode 100644 6-pubsub/tests/test_crud.py delete mode 100644 6-pubsub/tests/test_end_to_end.py delete mode 100644 6-pubsub/tests/test_storage.py delete mode 100644 6-pubsub/tox.ini delete mode 100644 6-pubsub/worker.yaml delete mode 100644 7-gce/bookshelf/__init__.py delete mode 100644 7-gce/bookshelf/crud.py delete mode 100644 7-gce/bookshelf/model_cloudsql.py delete mode 100644 7-gce/bookshelf/model_datastore.py delete mode 100644 7-gce/bookshelf/model_mongodb.py delete mode 100644 7-gce/bookshelf/storage.py delete mode 100644 7-gce/bookshelf/tasks.py delete mode 100644 7-gce/bookshelf/templates/base.html delete mode 100644 7-gce/bookshelf/templates/form.html delete mode 100644 7-gce/bookshelf/templates/list.html delete mode 100644 7-gce/bookshelf/templates/view.html delete mode 100644 7-gce/config.py delete mode 100755 7-gce/gce/deploy.sh delete mode 100644 7-gce/gce/deployment_manager/bookshelf.jinja delete mode 100644 7-gce/gce/deployment_manager/bookshelf.jinja.schema delete mode 100644 7-gce/gce/deployment_manager/config.yaml delete mode 100644 7-gce/gce/startup-script.sh delete mode 100755 7-gce/gce/teardown.sh delete mode 100644 7-gce/main.py delete mode 100644 7-gce/monitor.py delete mode 100644 7-gce/procfile delete mode 100644 7-gce/requirements-dev.txt delete mode 100644 7-gce/requirements.txt delete mode 100644 7-gce/tests/conftest.py delete mode 100644 7-gce/tests/test_auth.py delete mode 100644 7-gce/tests/test_crud.py delete mode 100644 7-gce/tests/test_end_to_end.py delete mode 100644 7-gce/tests/test_storage.py delete mode 100644 7-gce/tox.ini delete mode 100644 optional-kubernetes-engine/.dockerignore delete mode 100644 optional-kubernetes-engine/Dockerfile delete mode 100644 optional-kubernetes-engine/Makefile delete mode 100644 optional-kubernetes-engine/README.md delete mode 100644 optional-kubernetes-engine/bookshelf-frontend.yaml delete mode 100644 optional-kubernetes-engine/bookshelf-service.yaml delete mode 100644 optional-kubernetes-engine/bookshelf-worker.yaml delete mode 100644 optional-kubernetes-engine/bookshelf/__init__.py delete mode 100644 optional-kubernetes-engine/bookshelf/crud.py delete mode 100644 optional-kubernetes-engine/bookshelf/model_cloudsql.py delete mode 100644 optional-kubernetes-engine/bookshelf/model_datastore.py delete mode 100644 optional-kubernetes-engine/bookshelf/model_mongodb.py delete mode 100644 optional-kubernetes-engine/bookshelf/storage.py delete mode 100644 optional-kubernetes-engine/bookshelf/tasks.py delete mode 100644 optional-kubernetes-engine/bookshelf/templates/base.html delete mode 100644 optional-kubernetes-engine/bookshelf/templates/form.html delete mode 100644 optional-kubernetes-engine/bookshelf/templates/list.html delete mode 100644 optional-kubernetes-engine/bookshelf/templates/view.html delete mode 100644 optional-kubernetes-engine/config.py delete mode 100644 optional-kubernetes-engine/main.py delete mode 100644 optional-kubernetes-engine/procfile delete mode 100644 optional-kubernetes-engine/requirements-dev.txt delete mode 100644 optional-kubernetes-engine/requirements.txt delete mode 100644 optional-kubernetes-engine/tests/conftest.py delete mode 100644 optional-kubernetes-engine/tests/test_auth.py delete mode 100644 optional-kubernetes-engine/tests/test_crud.py delete mode 100644 optional-kubernetes-engine/tests/test_end_to_end.py delete mode 100644 optional-kubernetes-engine/tests/test_storage.py delete mode 100644 optional-kubernetes-engine/tox.ini diff --git a/1-hello-world/app.yaml b/1-hello-world/app.yaml deleted file mode 100644 index 4e3cb73b..00000000 --- a/1-hello-world/app.yaml +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# This file specifies your Python application's runtime configuration. -# See https://cloud.google.com/appengine/docs/managed-vms/python/runtime -# for details. - -# [START runtime] -runtime: python -env: flex -entrypoint: gunicorn -b :$PORT main:app - -runtime_config: - python_version: 3 -# [END runtime] diff --git a/1-hello-world/main.py b/1-hello-world/main.py deleted file mode 100644 index 5b398d30..00000000 --- a/1-hello-world/main.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# [START app] -from flask import Flask - - -app = Flask(__name__) - - -@app.route('/') -def hello(): - """Return a friendly HTTP greeting.""" - return 'Hello World!' - - -if __name__ == '__main__': - app.run(host='127.0.0.1', port=8080) -# [END app] diff --git a/1-hello-world/requirements.txt b/1-hello-world/requirements.txt deleted file mode 100644 index a34d076b..00000000 --- a/1-hello-world/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -Flask==1.0.2 -gunicorn==19.9.0 diff --git a/1-hello-world/tox.ini b/1-hello-world/tox.ini deleted file mode 100644 index 82bf72e3..00000000 --- a/1-hello-world/tox.ini +++ /dev/null @@ -1,10 +0,0 @@ -[tox] -skipsdist = True -envlist = lint - -[testenv:lint] -deps = - flake8 - flake8-import-order -commands = - flake8 --exclude=env --import-order-style=google diff --git a/2-structured-data/app.yaml b/2-structured-data/app.yaml deleted file mode 100644 index 566e6a19..00000000 --- a/2-structured-data/app.yaml +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# This file specifies your Python application's runtime configuration. -# See https://cloud.google.com/appengine/docs/managed-vms/python/runtime -# for details. - -runtime: python -env: flex -entrypoint: gunicorn -b :$PORT main:app - -runtime_config: - python_version: 3 - -#[START cloudsql_settings] -beta_settings: - # If using Cloud SQL, uncomment and set this value to the Cloud SQL - # connection name, e.g. - # "project:region:cloudsql-instance" - # You must also update the values in config.py. - # - # cloud_sql_instances: "your-cloudsql-connection-name" -#[END cloudsql_settings] diff --git a/2-structured-data/bookshelf/__init__.py b/2-structured-data/bookshelf/__init__.py deleted file mode 100644 index 419c524c..00000000 --- a/2-structured-data/bookshelf/__init__.py +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging - -from flask import current_app, Flask, redirect, url_for - - -def create_app(config, debug=False, testing=False, config_overrides=None): - app = Flask(__name__) - app.config.from_object(config) - - app.debug = debug - app.testing = testing - - if config_overrides: - app.config.update(config_overrides) - - # Configure logging - if not app.testing: - logging.basicConfig(level=logging.INFO) - - # Setup the data model. - with app.app_context(): - model = get_model() - model.init_app(app) - - # Register the Bookshelf CRUD blueprint. - from .crud import crud - app.register_blueprint(crud, url_prefix='/books') - - # Add a default root route. - @app.route("/") - def index(): - return redirect(url_for('crud.list')) - - # Add an error handler. This is useful for debugging the live application, - # however, you should disable the output of the exception for production - # applications. - @app.errorhandler(500) - def server_error(e): - return """ - An internal error occurred:
{}
- See logs for full stacktrace. - """.format(e), 500 - - return app - - -def get_model(): - model_backend = current_app.config['DATA_BACKEND'] - if model_backend == 'cloudsql': - from . import model_cloudsql - model = model_cloudsql - elif model_backend == 'datastore': - from . import model_datastore - model = model_datastore - elif model_backend == 'mongodb': - from . import model_mongodb - model = model_mongodb - else: - raise ValueError( - "No appropriate databackend configured. " - "Please specify datastore, cloudsql, or mongodb") - - return model diff --git a/2-structured-data/bookshelf/crud.py b/2-structured-data/bookshelf/crud.py deleted file mode 100644 index baa3488d..00000000 --- a/2-structured-data/bookshelf/crud.py +++ /dev/null @@ -1,75 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from bookshelf import get_model -from flask import Blueprint, redirect, render_template, request, url_for - - -crud = Blueprint('crud', __name__) - - -# [START list] -@crud.route("/") -def list(): - token = request.args.get('page_token', None) - if token: - token = token.encode('utf-8') - - books, next_page_token = get_model().list(cursor=token) - - return render_template( - "list.html", - books=books, - next_page_token=next_page_token) -# [END list] - - -@crud.route('/') -def view(id): - book = get_model().read(id) - return render_template("view.html", book=book) - - -# [START add] -@crud.route('/add', methods=['GET', 'POST']) -def add(): - if request.method == 'POST': - data = request.form.to_dict(flat=True) - - book = get_model().create(data) - - return redirect(url_for('.view', id=book['id'])) - - return render_template("form.html", action="Add", book={}) -# [END add] - - -@crud.route('//edit', methods=['GET', 'POST']) -def edit(id): - book = get_model().read(id) - - if request.method == 'POST': - data = request.form.to_dict(flat=True) - - book = get_model().update(data, id) - - return redirect(url_for('.view', id=book['id'])) - - return render_template("form.html", action="Edit", book=book) - - -@crud.route('//delete') -def delete(id): - get_model().delete(id) - return redirect(url_for('.list')) diff --git a/2-structured-data/bookshelf/model_cloudsql.py b/2-structured-data/bookshelf/model_cloudsql.py deleted file mode 100644 index 39ab1bbb..00000000 --- a/2-structured-data/bookshelf/model_cloudsql.py +++ /dev/null @@ -1,117 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from flask import Flask -from flask_sqlalchemy import SQLAlchemy - - -builtin_list = list - - -db = SQLAlchemy() - - -def init_app(app): - # Disable track modifications, as it unnecessarily uses memory. - app.config.setdefault('SQLALCHEMY_TRACK_MODIFICATIONS', False) - db.init_app(app) - - -def from_sql(row): - """Translates a SQLAlchemy model instance into a dictionary""" - data = row.__dict__.copy() - data['id'] = row.id - data.pop('_sa_instance_state') - return data - - -# [START model] -class Book(db.Model): - __tablename__ = 'books' - - id = db.Column(db.Integer, primary_key=True) - title = db.Column(db.String(255)) - author = db.Column(db.String(255)) - publishedDate = db.Column(db.String(255)) - imageUrl = db.Column(db.String(255)) - description = db.Column(db.String(4096)) - createdBy = db.Column(db.String(255)) - createdById = db.Column(db.String(255)) - - def __repr__(self): - return " - - - Bookshelf - Python on Google Cloud Platform - - - - - - -
- {% block content %}{% endblock %} -
- {{user}} - - diff --git a/2-structured-data/bookshelf/templates/form.html b/2-structured-data/bookshelf/templates/form.html deleted file mode 100644 index af30bb17..00000000 --- a/2-structured-data/bookshelf/templates/form.html +++ /dev/null @@ -1,49 +0,0 @@ -{# -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -#} - -{# [START form] #} -{% extends "base.html" %} - -{% block content %} -

{{action}} book

- -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- - -
- -{% endblock %} -{# [END form] #} diff --git a/2-structured-data/bookshelf/templates/list.html b/2-structured-data/bookshelf/templates/list.html deleted file mode 100644 index 69beeeb3..00000000 --- a/2-structured-data/bookshelf/templates/list.html +++ /dev/null @@ -1,51 +0,0 @@ -{# -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -#} - -{% extends "base.html" %} - -{% block content %} - -

Books

- - - Add book - - -{% for book in books %} - -{% else %} -

No books found

-{% endfor %} - -{% if next_page_token %} - -{% endif %} - -{% endblock %} diff --git a/2-structured-data/bookshelf/templates/view.html b/2-structured-data/bookshelf/templates/view.html deleted file mode 100644 index 5f07c51a..00000000 --- a/2-structured-data/bookshelf/templates/view.html +++ /dev/null @@ -1,48 +0,0 @@ -{# -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -#} - -{% extends "base.html" %} - -{% block content %} - -

Book

- - - -
-
- -
-
-

- {{book.title}} - {{book.publishedDate}} -

-
By {{book.author|default('Unknown', True)}}
-

{{book.description}}

-
-
- -{% endblock %} diff --git a/2-structured-data/config.py b/2-structured-data/config.py deleted file mode 100644 index 660387d3..00000000 --- a/2-structured-data/config.py +++ /dev/null @@ -1,79 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -This file contains all of the configuration values for the application. -Update this file with the values for your specific Google Cloud project. -You can create and manage projects at https://console.developers.google.com -""" - -import os - -# The secret key is used by Flask to encrypt session cookies. -SECRET_KEY = 'secret' - -# There are three different ways to store the data in the application. -# You can choose 'datastore', 'cloudsql', or 'mongodb'. Be sure to -# configure the respective settings for the one you choose below. -# You do not have to configure the other data backends. If unsure, choose -# 'datastore' as it does not require any additional configuration. -DATA_BACKEND = 'datastore' - -# Google Cloud Project ID. This can be found on the 'Overview' page at -# https://console.developers.google.com -PROJECT_ID = 'your-project-id' - -# CloudSQL & SQLAlchemy configuration -# Replace the following values the respective values of your Cloud SQL -# instance. -CLOUDSQL_USER = 'root' -CLOUDSQL_PASSWORD = 'your-cloudsql-password' -CLOUDSQL_DATABASE = 'bookshelf' -# Set this value to the Cloud SQL connection name, e.g. -# "project:region:cloudsql-instance". -# You must also update the value in app.yaml. -CLOUDSQL_CONNECTION_NAME = 'your-cloudsql-connection-name' - -# The CloudSQL proxy is used locally to connect to the cloudsql instance. -# To start the proxy, use: -# -# $ cloud_sql_proxy -instances=your-connection-name=tcp:3306 -# -# Port 3306 is the standard MySQL port. If you need to use a different port, -# change the 3306 to a different port number. - -# Alternatively, you could use a local MySQL instance for testing. -LOCAL_SQLALCHEMY_DATABASE_URI = ( - 'mysql+pymysql://{user}:{password}@127.0.0.1:3306/{database}').format( - user=CLOUDSQL_USER, password=CLOUDSQL_PASSWORD, - database=CLOUDSQL_DATABASE) - -# When running on App Engine a unix socket is used to connect to the cloudsql -# instance. -LIVE_SQLALCHEMY_DATABASE_URI = ( - 'mysql+pymysql://{user}:{password}@localhost/{database}' - '?unix_socket=/cloudsql/{connection_name}').format( - user=CLOUDSQL_USER, password=CLOUDSQL_PASSWORD, - database=CLOUDSQL_DATABASE, connection_name=CLOUDSQL_CONNECTION_NAME) - -if os.environ.get('GAE_INSTANCE'): - SQLALCHEMY_DATABASE_URI = LIVE_SQLALCHEMY_DATABASE_URI -else: - SQLALCHEMY_DATABASE_URI = LOCAL_SQLALCHEMY_DATABASE_URI - -# Mongo configuration -# If using mongolab, the connection URI is available from the mongolab control -# panel. If self-hosting on compute engine, replace the values below. -MONGO_URI = \ - 'mongodb://user:password@host:27017/database' diff --git a/2-structured-data/main.py b/2-structured-data/main.py deleted file mode 100644 index d5697c6c..00000000 --- a/2-structured-data/main.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import bookshelf -import config - - -app = bookshelf.create_app(config) - - -# This is only used when running locally. When running live, gunicorn runs -# the application. -if __name__ == '__main__': - app.run(host='127.0.0.1', port=8080, debug=True) diff --git a/2-structured-data/requirements-dev.txt b/2-structured-data/requirements-dev.txt deleted file mode 100644 index 5bbc2e5d..00000000 --- a/2-structured-data/requirements-dev.txt +++ /dev/null @@ -1,6 +0,0 @@ -tox==3.5.3 -flake8==3.6.0 -flaky==3.4.0 -pytest==4.0.1 -pytest-cov==2.6.0 -retrying==1.3.3 diff --git a/2-structured-data/requirements.txt b/2-structured-data/requirements.txt deleted file mode 100644 index deaf011a..00000000 --- a/2-structured-data/requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -Flask>=1.0.0 -google-cloud-datastore==1.7.1 -gunicorn==19.9.0 -Flask-SQLAlchemy==2.3.2 -PyMySQL==0.9.2 -Flask-PyMongo>=2.0.0 -oauth2client==4.1.2 -PyMongo==3.7.2 -six==1.11.0 diff --git a/2-structured-data/tests/conftest.py b/2-structured-data/tests/conftest.py deleted file mode 100644 index 62c2a480..00000000 --- a/2-structured-data/tests/conftest.py +++ /dev/null @@ -1,91 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""conftest.py is used to define common test fixtures for pytest.""" - -import bookshelf -import config -from google.cloud.exceptions import ServiceUnavailable -from oauth2client.client import HttpAccessTokenRefreshError -import pytest -from retrying import retry - - -#@pytest.yield_fixture(params=['datastore', 'cloudsql', 'mongodb']) -@pytest.yield_fixture(params=['datastore', 'mongodb']) -def app(request): - """This fixtures provides a Flask app instance configured for testing. - - Because it's parametric, it will cause every test that uses this fixture - to run three times: one time for each backend (datastore, cloudsql, and - mongodb). - - It also ensures the tests run within a request context, allowing - any calls to flask.request, flask.current_app, etc. to work.""" - app = bookshelf.create_app( - config, - testing=True, - config_overrides={ - 'DATA_BACKEND': request.param - }) - - with app.test_request_context(): - yield app - - -@pytest.yield_fixture -def model(monkeypatch, app): - """This fixture provides a modified version of the app's model that tracks - all created items and deletes them at the end of the test. - - Any tests that directly or indirectly interact with the database should use - this to ensure that resources are properly cleaned up. - - Monkeypatch is provided by pytest and used to patch the model's create - method. - - The app fixture is needed to provide the configuration and context needed - to get the proper model object. - """ - model = bookshelf.get_model() - - # Ensure no books exist before running. This typically helps if tests - # somehow left the database in a bad state. - delete_all_books(model) - - yield model - - # Delete all books that we created during tests. - delete_all_books(model) - - -# The backend data stores can sometimes be flaky. It's useful to retry this -# a few times before giving up. -@retry( - stop_max_attempt_number=3, - wait_exponential_multiplier=100, - wait_exponential_max=2000) -def delete_all_books(model): - while True: - books, _ = model.list(limit=50) - if not books: - break - for book in books: - model.delete(book['id']) - - -def flaky_filter(info, *args): - """Used by flaky to determine when to re-run a test case.""" - _, e, _ = info - return isinstance(e, (ServiceUnavailable, HttpAccessTokenRefreshError)) diff --git a/2-structured-data/tests/test_crud.py b/2-structured-data/tests/test_crud.py deleted file mode 100644 index c0d2f40f..00000000 --- a/2-structured-data/tests/test_crud.py +++ /dev/null @@ -1,88 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import re - -from conftest import flaky_filter -from flaky import flaky -import pytest - - -# Mark all test cases in this class as flaky, so that if errors occur they -# can be retried. This is useful when databases are temporarily unavailable. -@flaky(rerun_filter=flaky_filter) -# Tell pytest to use both the app and model fixtures for all test cases. -# This ensures that configuration is properly applied and that all database -# resources created during tests are cleaned up. These fixtures are defined -# in conftest.py -@pytest.mark.usefixtures('app', 'model') -class TestCrudActions(object): - - def test_list(self, app, model): - for i in range(1, 12): - model.create({'title': u'Book {0}'.format(i)}) - - with app.test_client() as c: - rv = c.get('/books/') - - assert rv.status == '200 OK' - - body = rv.data.decode('utf-8') - assert 'Book 1' in body, "Should show books" - assert len(re.findall('

Book', body)) == 10, ( - "Should not show more than 10 books") - assert 'More' in body, "Should have more than one page" - - def test_add(self, app): - data = { - 'title': 'Test Book', - 'author': 'Test Author', - 'publishedDate': 'Test Date Published', - 'description': 'Test Description' - } - - with app.test_client() as c: - rv = c.post('/books/add', data=data, follow_redirects=True) - - assert rv.status == '200 OK' - body = rv.data.decode('utf-8') - assert 'Test Book' in body - assert 'Test Author' in body - assert 'Test Date Published' in body - assert 'Test Description' in body - - def test_edit(self, app, model): - existing = model.create({'title': "Temp Title"}) - - with app.test_client() as c: - rv = c.post( - '/books/%s/edit' % existing['id'], - data={'title': 'Updated Title'}, - follow_redirects=True) - - assert rv.status == '200 OK' - body = rv.data.decode('utf-8') - assert 'Updated Title' in body - assert 'Temp Title' not in body - - def test_delete(self, app, model): - existing = model.create({'title': "Temp Title"}) - - with app.test_client() as c: - rv = c.get( - '/books/%s/delete' % existing['id'], - follow_redirects=True) - - assert rv.status == '200 OK' - assert not model.read(existing['id']) diff --git a/2-structured-data/tox.ini b/2-structured-data/tox.ini deleted file mode 100644 index 40c86660..00000000 --- a/2-structured-data/tox.ini +++ /dev/null @@ -1,19 +0,0 @@ -[tox] -skipsdist = True -envlist = lint,py27,py36 - -[testenv] -deps = - -rrequirements.txt - -rrequirements-dev.txt -commands = - py.test --cov=bookshelf --no-success-flaky-report {posargs} tests -passenv = GOOGLE_APPLICATION_CREDENTIALS DATASTORE_HOST -setenv = PYTHONPATH={toxinidir} - -[testenv:lint] -deps = - flake8 - flake8-import-order -commands = - flake8 --import-order-style=google bookshelf tests diff --git a/3-binary-data/app.yaml b/3-binary-data/app.yaml deleted file mode 100644 index d0de9391..00000000 --- a/3-binary-data/app.yaml +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# This file specifies your Python application's runtime configuration. -# See https://cloud.google.com/appengine/docs/managed-vms/config for details. - -runtime: python -env: flex -entrypoint: gunicorn -b :$PORT main:app - -runtime_config: - python_version: 3 - -beta_settings: - # If using Cloud SQL, uncomment and set this value to the Cloud SQL - # connection name, e.g. - # "project:region:cloudsql-instance" - # You must also update the values in config.py. - # - # cloud_sql_instances: "your-cloudsql-connection-name" diff --git a/3-binary-data/bookshelf/__init__.py b/3-binary-data/bookshelf/__init__.py deleted file mode 100644 index 419c524c..00000000 --- a/3-binary-data/bookshelf/__init__.py +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging - -from flask import current_app, Flask, redirect, url_for - - -def create_app(config, debug=False, testing=False, config_overrides=None): - app = Flask(__name__) - app.config.from_object(config) - - app.debug = debug - app.testing = testing - - if config_overrides: - app.config.update(config_overrides) - - # Configure logging - if not app.testing: - logging.basicConfig(level=logging.INFO) - - # Setup the data model. - with app.app_context(): - model = get_model() - model.init_app(app) - - # Register the Bookshelf CRUD blueprint. - from .crud import crud - app.register_blueprint(crud, url_prefix='/books') - - # Add a default root route. - @app.route("/") - def index(): - return redirect(url_for('crud.list')) - - # Add an error handler. This is useful for debugging the live application, - # however, you should disable the output of the exception for production - # applications. - @app.errorhandler(500) - def server_error(e): - return """ - An internal error occurred:
{}
- See logs for full stacktrace. - """.format(e), 500 - - return app - - -def get_model(): - model_backend = current_app.config['DATA_BACKEND'] - if model_backend == 'cloudsql': - from . import model_cloudsql - model = model_cloudsql - elif model_backend == 'datastore': - from . import model_datastore - model = model_datastore - elif model_backend == 'mongodb': - from . import model_mongodb - model = model_mongodb - else: - raise ValueError( - "No appropriate databackend configured. " - "Please specify datastore, cloudsql, or mongodb") - - return model diff --git a/3-binary-data/bookshelf/crud.py b/3-binary-data/bookshelf/crud.py deleted file mode 100644 index 6686798c..00000000 --- a/3-binary-data/bookshelf/crud.py +++ /dev/null @@ -1,109 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from bookshelf import get_model, storage -from flask import Blueprint, current_app, redirect, render_template, request, \ - url_for - - -crud = Blueprint('crud', __name__) - - -# [START upload_image_file] -def upload_image_file(file): - """ - Upload the user-uploaded file to Google Cloud Storage and retrieve its - publicly-accessible URL. - """ - if not file: - return None - - public_url = storage.upload_file( - file.read(), - file.filename, - file.content_type - ) - - current_app.logger.info( - "Uploaded file %s as %s.", file.filename, public_url) - - return public_url -# [END upload_image_file] - - -@crud.route("/") -def list(): - token = request.args.get('page_token', None) - if token: - token = token.encode('utf-8') - - books, next_page_token = get_model().list(cursor=token) - - return render_template( - "list.html", - books=books, - next_page_token=next_page_token) - - -@crud.route('/') -def view(id): - book = get_model().read(id) - return render_template("view.html", book=book) - - -@crud.route('/add', methods=['GET', 'POST']) -def add(): - if request.method == 'POST': - data = request.form.to_dict(flat=True) - - # If an image was uploaded, update the data to point to the new image. - # [START image_url] - image_url = upload_image_file(request.files.get('image')) - # [END image_url] - - # [START image_url2] - if image_url: - data['imageUrl'] = image_url - # [END image_url2] - - book = get_model().create(data) - - return redirect(url_for('.view', id=book['id'])) - - return render_template("form.html", action="Add", book={}) - - -@crud.route('//edit', methods=['GET', 'POST']) -def edit(id): - book = get_model().read(id) - - if request.method == 'POST': - data = request.form.to_dict(flat=True) - - image_url = upload_image_file(request.files.get('image')) - - if image_url: - data['imageUrl'] = image_url - - book = get_model().update(data, id) - - return redirect(url_for('.view', id=book['id'])) - - return render_template("form.html", action="Edit", book=book) - - -@crud.route('//delete') -def delete(id): - get_model().delete(id) - return redirect(url_for('.list')) diff --git a/3-binary-data/bookshelf/model_cloudsql.py b/3-binary-data/bookshelf/model_cloudsql.py deleted file mode 100644 index 96eb190a..00000000 --- a/3-binary-data/bookshelf/model_cloudsql.py +++ /dev/null @@ -1,107 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from flask import Flask -from flask_sqlalchemy import SQLAlchemy - - -builtin_list = list - - -db = SQLAlchemy() - - -def init_app(app): - # Disable track modifications, as it unnecessarily uses memory. - app.config.setdefault('SQLALCHEMY_TRACK_MODIFICATIONS', False) - db.init_app(app) - - -def from_sql(row): - """Translates a SQLAlchemy model instance into a dictionary""" - data = row.__dict__.copy() - data['id'] = row.id - data.pop('_sa_instance_state') - return data - - -class Book(db.Model): - __tablename__ = 'books' - - id = db.Column(db.Integer, primary_key=True) - title = db.Column(db.String(255)) - author = db.Column(db.String(255)) - publishedDate = db.Column(db.String(255)) - imageUrl = db.Column(db.String(255)) - description = db.Column(db.String(4096)) - createdBy = db.Column(db.String(255)) - createdById = db.Column(db.String(255)) - - def __repr__(self): - return " - - - Bookshelf - Python on Google Cloud Platform - - - - - - -
- {% block content %}{% endblock %} -
- {{user}} - - diff --git a/3-binary-data/bookshelf/templates/form.html b/3-binary-data/bookshelf/templates/form.html deleted file mode 100644 index 6d5e4d1f..00000000 --- a/3-binary-data/bookshelf/templates/form.html +++ /dev/null @@ -1,59 +0,0 @@ -{# -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -#} - -{# [START form] #} -{% extends "base.html" %} - -{% block content %} -

{{action}} book

- -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- - - - -
- -{% endblock %} -{# [END form] #} diff --git a/3-binary-data/bookshelf/templates/list.html b/3-binary-data/bookshelf/templates/list.html deleted file mode 100644 index 3362f0e2..00000000 --- a/3-binary-data/bookshelf/templates/list.html +++ /dev/null @@ -1,55 +0,0 @@ -{# -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -#} - -{% extends "base.html" %} - -{% block content %} - -

Books

- - - Add book - - -{% for book in books %} - -{% else %} -

No books found

-{% endfor %} - -{% if next_page_token %} - -{% endif %} - -{% endblock %} diff --git a/3-binary-data/bookshelf/templates/view.html b/3-binary-data/bookshelf/templates/view.html deleted file mode 100644 index 1e509ff0..00000000 --- a/3-binary-data/bookshelf/templates/view.html +++ /dev/null @@ -1,54 +0,0 @@ -{# -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -#} - -{% extends "base.html" %} - -{% block content %} - -

Book

- - - -
- {# [START book_image] #} -
- {% if book.imageUrl %} - - {% else %} - - {% endif %} -
- {# [END book_image] #} -
-

- {{book.title}} - {{book.publishedDate}} -

-
By {{book.author|default('Unknown', True)}}
-

{{book.description}}

-
-
- -{% endblock %} diff --git a/3-binary-data/config.py b/3-binary-data/config.py deleted file mode 100644 index 7d397967..00000000 --- a/3-binary-data/config.py +++ /dev/null @@ -1,95 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -This file contains all of the configuration values for the application. -Update this file with the values for your specific Google Cloud project. -You can create and manage projects at https://console.developers.google.com -""" - -import os - -# The secret key is used by Flask to encrypt session cookies. -SECRET_KEY = 'secret' - -# There are three different ways to store the data in the application. -# You can choose 'datastore', 'cloudsql', or 'mongodb'. Be sure to -# configure the respective settings for the one you choose below. -# You do not have to configure the other data backends. If unsure, choose -# 'datastore' as it does not require any additional configuration. -DATA_BACKEND = 'datastore' - -# Google Cloud Project ID. This can be found on the 'Overview' page at -# https://console.developers.google.com -PROJECT_ID = 'your-project-id' - -# CloudSQL & SQLAlchemy configuration -# Replace the following values the respective values of your Cloud SQL -# instance. -CLOUDSQL_USER = 'root' -CLOUDSQL_PASSWORD = 'your-cloudsql-password' -CLOUDSQL_DATABASE = 'bookshelf' -# Set this value to the Cloud SQL connection name, e.g. -# "project:region:cloudsql-instance". -# You must also update the value in app.yaml. -CLOUDSQL_CONNECTION_NAME = 'your-cloudsql-connection-name' - -# The CloudSQL proxy is used locally to connect to the cloudsql instance. -# To start the proxy, use: -# -# $ cloud_sql_proxy -instances=your-connection-name=tcp:3306 -# -# Port 3306 is the standard MySQL port. If you need to use a different port, -# change the 3306 to a different port number. - -# Alternatively, you could use a local MySQL instance for testing. -LOCAL_SQLALCHEMY_DATABASE_URI = ( - 'mysql+pymysql://{user}:{password}@127.0.0.1:3306/{database}').format( - user=CLOUDSQL_USER, password=CLOUDSQL_PASSWORD, - database=CLOUDSQL_DATABASE) - -# When running on App Engine a unix socket is used to connect to the cloudsql -# instance. -LIVE_SQLALCHEMY_DATABASE_URI = ( - 'mysql+pymysql://{user}:{password}@localhost/{database}' - '?unix_socket=/cloudsql/{connection_name}').format( - user=CLOUDSQL_USER, password=CLOUDSQL_PASSWORD, - database=CLOUDSQL_DATABASE, connection_name=CLOUDSQL_CONNECTION_NAME) - -if os.environ.get('GAE_INSTANCE'): - SQLALCHEMY_DATABASE_URI = LIVE_SQLALCHEMY_DATABASE_URI -else: - SQLALCHEMY_DATABASE_URI = LOCAL_SQLALCHEMY_DATABASE_URI - -# Mongo configuration -# If using mongolab, the connection URI is available from the mongolab control -# panel. If self-hosting on compute engine, replace the values below. -MONGO_URI = 'mongodb://user:password@host:27017/database' - -# Google Cloud Storage and upload settings. -# Typically, you'll name your bucket the same as your project. To create a -# bucket: -# -# $ gsutil mb gs:// -# -# You also need to make sure that the default ACL is set to public-read, -# otherwise users will not be able to see their upload images: -# -# $ gsutil defacl set public-read gs:// -# -# You can adjust the max content length and allow extensions settings to allow -# larger or more varied file types if desired. -CLOUD_STORAGE_BUCKET = 'your-bucket-name' -MAX_CONTENT_LENGTH = 8 * 1024 * 1024 -ALLOWED_EXTENSIONS = set(['png', 'jpg', 'jpeg', 'gif']) diff --git a/3-binary-data/main.py b/3-binary-data/main.py deleted file mode 100644 index d5697c6c..00000000 --- a/3-binary-data/main.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import bookshelf -import config - - -app = bookshelf.create_app(config) - - -# This is only used when running locally. When running live, gunicorn runs -# the application. -if __name__ == '__main__': - app.run(host='127.0.0.1', port=8080, debug=True) diff --git a/3-binary-data/requirements-dev.txt b/3-binary-data/requirements-dev.txt deleted file mode 100644 index 5bbc2e5d..00000000 --- a/3-binary-data/requirements-dev.txt +++ /dev/null @@ -1,6 +0,0 @@ -tox==3.5.3 -flake8==3.6.0 -flaky==3.4.0 -pytest==4.0.1 -pytest-cov==2.6.0 -retrying==1.3.3 diff --git a/3-binary-data/requirements.txt b/3-binary-data/requirements.txt deleted file mode 100644 index fa332f42..00000000 --- a/3-binary-data/requirements.txt +++ /dev/null @@ -1,10 +0,0 @@ -Flask>=1.0.0 -google-cloud-datastore==1.7.1 -google-cloud-storage==1.13.0 -gunicorn==19.9.0 -oauth2client==4.1.3 -Flask-SQLAlchemy==2.3.2 -PyMySQL==0.9.2 -Flask-PyMongo>=2.0.0 -PyMongo==3.7.2 -six==1.11.0 diff --git a/3-binary-data/tests/conftest.py b/3-binary-data/tests/conftest.py deleted file mode 100644 index 62c2a480..00000000 --- a/3-binary-data/tests/conftest.py +++ /dev/null @@ -1,91 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""conftest.py is used to define common test fixtures for pytest.""" - -import bookshelf -import config -from google.cloud.exceptions import ServiceUnavailable -from oauth2client.client import HttpAccessTokenRefreshError -import pytest -from retrying import retry - - -#@pytest.yield_fixture(params=['datastore', 'cloudsql', 'mongodb']) -@pytest.yield_fixture(params=['datastore', 'mongodb']) -def app(request): - """This fixtures provides a Flask app instance configured for testing. - - Because it's parametric, it will cause every test that uses this fixture - to run three times: one time for each backend (datastore, cloudsql, and - mongodb). - - It also ensures the tests run within a request context, allowing - any calls to flask.request, flask.current_app, etc. to work.""" - app = bookshelf.create_app( - config, - testing=True, - config_overrides={ - 'DATA_BACKEND': request.param - }) - - with app.test_request_context(): - yield app - - -@pytest.yield_fixture -def model(monkeypatch, app): - """This fixture provides a modified version of the app's model that tracks - all created items and deletes them at the end of the test. - - Any tests that directly or indirectly interact with the database should use - this to ensure that resources are properly cleaned up. - - Monkeypatch is provided by pytest and used to patch the model's create - method. - - The app fixture is needed to provide the configuration and context needed - to get the proper model object. - """ - model = bookshelf.get_model() - - # Ensure no books exist before running. This typically helps if tests - # somehow left the database in a bad state. - delete_all_books(model) - - yield model - - # Delete all books that we created during tests. - delete_all_books(model) - - -# The backend data stores can sometimes be flaky. It's useful to retry this -# a few times before giving up. -@retry( - stop_max_attempt_number=3, - wait_exponential_multiplier=100, - wait_exponential_max=2000) -def delete_all_books(model): - while True: - books, _ = model.list(limit=50) - if not books: - break - for book in books: - model.delete(book['id']) - - -def flaky_filter(info, *args): - """Used by flaky to determine when to re-run a test case.""" - _, e, _ = info - return isinstance(e, (ServiceUnavailable, HttpAccessTokenRefreshError)) diff --git a/3-binary-data/tests/test_crud.py b/3-binary-data/tests/test_crud.py deleted file mode 100644 index b09e224b..00000000 --- a/3-binary-data/tests/test_crud.py +++ /dev/null @@ -1,88 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import re - -from conftest import flaky_filter -from flaky import flaky -import pytest - - -# Mark all test cases in this class as flaky, so that if errors occur they -# can be retried. This is useful when databases are temporarily unavailable. -@flaky(rerun_filter=flaky_filter) -# Tell pytest to use both the app and model fixtures for all test cases. -# This ensures that configuration is properly applied and that all database -# resources created during tests are cleaned up. These fixtures are defined -# in conftest.py -@pytest.mark.usefixtures('app', 'model') -class TestCrudActions(object): - - def test_list(self, app, model): - for i in range(1, 12): - model.create({'title': u'Book {0}'.format(i)}) - - with app.test_client() as c: - rv = c.get('/books/') - - assert rv.status == '200 OK' - - body = rv.data.decode('utf-8') - assert 'Book 1' in body, "Should show books" - assert len(re.findall('

Book', body)) <= 10, ( - "Should not show more than 10 books") - assert 'More' in body, "Should have more than one page" - - def test_add(self, app): - data = { - 'title': 'Test Book', - 'author': 'Test Author', - 'publishedDate': 'Test Date Published', - 'description': 'Test Description' - } - - with app.test_client() as c: - rv = c.post('/books/add', data=data, follow_redirects=True) - - assert rv.status == '200 OK' - body = rv.data.decode('utf-8') - assert 'Test Book' in body - assert 'Test Author' in body - assert 'Test Date Published' in body - assert 'Test Description' in body - - def test_edit(self, app, model): - existing = model.create({'title': "Temp Title"}) - - with app.test_client() as c: - rv = c.post( - '/books/%s/edit' % existing['id'], - data={'title': 'Updated Title'}, - follow_redirects=True) - - assert rv.status == '200 OK' - body = rv.data.decode('utf-8') - assert 'Updated Title' in body - assert 'Temp Title' not in body - - def test_delete(self, app, model): - existing = model.create({'title': "Temp Title"}) - - with app.test_client() as c: - rv = c.get( - '/books/%s/delete' % existing['id'], - follow_redirects=True) - - assert rv.status == '200 OK' - assert not model.read(existing['id']) diff --git a/3-binary-data/tests/test_storage.py b/3-binary-data/tests/test_storage.py deleted file mode 100644 index 531c8827..00000000 --- a/3-binary-data/tests/test_storage.py +++ /dev/null @@ -1,70 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import re - -from conftest import flaky_filter -from flaky import flaky -import httplib2 -import pytest -from six import BytesIO - - -# Mark all test cases in this class as flaky, so that if errors occur they -# can be retried. This is useful when databases are temporarily unavailable. -@flaky(rerun_filter=flaky_filter) -# Tell pytest to use both the app and model fixtures for all test cases. -# This ensures that configuration is properly applied and that all database -# resources created during tests are cleaned up. These fixtures are defined -# in conftest.py -@pytest.mark.usefixtures('app', 'model') -class TestStorage(object): - - def test_upload_image(self, app): - data = { - 'title': 'Test Book', - 'author': 'Test Author', - 'publishedDate': 'Test Date Published', - 'description': 'Test Description', - 'image': (BytesIO(b'hello world'), 'hello.jpg') - } - - with app.test_client() as c: - rv = c.post('/books/add', data=data, follow_redirects=True) - - assert rv.status == '200 OK' - body = rv.data.decode('utf-8') - - img_tag = re.search(''), - '1337h4x0r.php') - } - - with app.test_client() as c: - rv = c.post('/books/add', data=data, follow_redirects=True) - - # check we weren't pwned - assert rv.status == '400 BAD REQUEST' diff --git a/3-binary-data/tox.ini b/3-binary-data/tox.ini deleted file mode 100644 index 40c86660..00000000 --- a/3-binary-data/tox.ini +++ /dev/null @@ -1,19 +0,0 @@ -[tox] -skipsdist = True -envlist = lint,py27,py36 - -[testenv] -deps = - -rrequirements.txt - -rrequirements-dev.txt -commands = - py.test --cov=bookshelf --no-success-flaky-report {posargs} tests -passenv = GOOGLE_APPLICATION_CREDENTIALS DATASTORE_HOST -setenv = PYTHONPATH={toxinidir} - -[testenv:lint] -deps = - flake8 - flake8-import-order -commands = - flake8 --import-order-style=google bookshelf tests diff --git a/4-auth/app.yaml b/4-auth/app.yaml deleted file mode 100644 index aa5ddf46..00000000 --- a/4-auth/app.yaml +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# This file specifies your Python application's runtime configuration. -# See https://cloud.google.com/appengine/docs/managed-vms/python/runtime -# for details. - -runtime: python -env: flex -entrypoint: gunicorn -b :$PORT main:app - -runtime_config: - python_version: 3 - -beta_settings: - # If using Cloud SQL, uncomment and set this value to the Cloud SQL - # connection name, e.g. - # "project:region:cloudsql-instance" - # You must also update the values in config.py. - # - # cloud_sql_instances: "your-cloudsql-connection-name" diff --git a/4-auth/bookshelf/__init__.py b/4-auth/bookshelf/__init__.py deleted file mode 100644 index f3f4b515..00000000 --- a/4-auth/bookshelf/__init__.py +++ /dev/null @@ -1,125 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import json -import logging - -from flask import current_app, Flask, redirect, request, session, url_for -import httplib2 -# [START include] -from oauth2client.contrib.flask_util import UserOAuth2 - - -oauth2 = UserOAuth2() -# [END include] - - -def create_app(config, debug=False, testing=False, config_overrides=None): - app = Flask(__name__) - app.config.from_object(config) - - app.debug = debug - app.testing = testing - - if config_overrides: - app.config.update(config_overrides) - - # Configure logging - if not app.testing: - logging.basicConfig(level=logging.INFO) - - # Setup the data model. - with app.app_context(): - model = get_model() - model.init_app(app) - - # [START init_app] - # Initalize the OAuth2 helper. - oauth2.init_app( - app, - scopes=['email', 'profile'], - authorize_callback=_request_user_info) - # [END init_app] - - # [START logout] - # Add a logout handler. - @app.route('/logout') - def logout(): - # Delete the user's profile and the credentials stored by oauth2. - del session['profile'] - session.modified = True - oauth2.storage.delete() - return redirect(request.referrer or '/') - # [END logout] - - # Register the Bookshelf CRUD blueprint. - from .crud import crud - app.register_blueprint(crud, url_prefix='/books') - - # Add a default root route. - @app.route("/") - def index(): - return redirect(url_for('crud.list')) - - # Add an error handler. This is useful for debugging the live application, - # however, you should disable the output of the exception for production - # applications. - @app.errorhandler(500) - def server_error(e): - return """ - An internal error occurred:
{}
- See logs for full stacktrace. - """.format(e), 500 - - return app - - -def get_model(): - model_backend = current_app.config['DATA_BACKEND'] - if model_backend == 'cloudsql': - from . import model_cloudsql - model = model_cloudsql - elif model_backend == 'datastore': - from . import model_datastore - model = model_datastore - elif model_backend == 'mongodb': - from . import model_mongodb - model = model_mongodb - else: - raise ValueError( - "No appropriate databackend configured. " - "Please specify datastore, cloudsql, or mongodb") - - return model - - -# [START request_user_info] -def _request_user_info(credentials): - """ - Makes an HTTP request to the Google OAuth2 API to retrieve the user's basic - profile information, including full name and photo, and stores it in the - Flask session. - """ - http = httplib2.Http() - credentials.authorize(http) - resp, content = http.request( - 'https://www.googleapis.com/oauth2/v3/userinfo') - - if resp.status != 200: - current_app.logger.error( - "Error while obtaining user profile: \n%s: %s", resp, content) - return None - session['profile'] = json.loads(content.decode('utf-8')) - -# [END request_user_info] diff --git a/4-auth/bookshelf/crud.py b/4-auth/bookshelf/crud.py deleted file mode 100644 index dd5c6291..00000000 --- a/4-auth/bookshelf/crud.py +++ /dev/null @@ -1,129 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from bookshelf import get_model, oauth2, storage -from flask import Blueprint, current_app, redirect, render_template, request, \ - session, url_for - - -crud = Blueprint('crud', __name__) - - -def upload_image_file(file): - """ - Upload the user-uploaded file to Google Cloud Storage and retrieve its - publicly-accessible URL. - """ - if not file: - return None - - public_url = storage.upload_file( - file.read(), - file.filename, - file.content_type - ) - - current_app.logger.info( - "Uploaded file %s as %s.", file.filename, public_url) - - return public_url - - -@crud.route("/") -def list(): - token = request.args.get('page_token', None) - if token: - token = token.encode('utf-8') - - books, next_page_token = get_model().list(cursor=token) - - return render_template( - "list.html", - books=books, - next_page_token=next_page_token) - - -# [START list_mine] -@crud.route("/mine") -@oauth2.required -def list_mine(): - token = request.args.get('page_token', None) - if token: - token = token.encode('utf-8') - - books, next_page_token = get_model().list_by_user( - user_id=session['profile']['email'], - cursor=token) - - return render_template( - "list.html", - books=books, - next_page_token=next_page_token) -# [END list_mine] - - -@crud.route('/') -def view(id): - book = get_model().read(id) - return render_template("view.html", book=book) - - -# [START add] -@crud.route('/add', methods=['GET', 'POST']) -def add(): - if request.method == 'POST': - data = request.form.to_dict(flat=True) - - # If an image was uploaded, update the data to point to the new image. - image_url = upload_image_file(request.files.get('image')) - - if image_url: - data['imageUrl'] = image_url - - # If the user is logged in, associate their profile with the new book. - if 'profile' in session: - data['createdBy'] = session['profile']['name'] - data['createdById'] = session['profile']['email'] - - book = get_model().create(data) - - return redirect(url_for('.view', id=book['id'])) - - return render_template("form.html", action="Add", book={}) -# [END add] - - -@crud.route('//edit', methods=['GET', 'POST']) -def edit(id): - book = get_model().read(id) - - if request.method == 'POST': - data = request.form.to_dict(flat=True) - - image_url = upload_image_file(request.files.get('image')) - - if image_url: - data['imageUrl'] = image_url - - book = get_model().update(data, id) - - return redirect(url_for('.view', id=book['id'])) - - return render_template("form.html", action="Edit", book=book) - - -@crud.route('//delete') -def delete(id): - get_model().delete(id) - return redirect(url_for('.list')) diff --git a/4-auth/bookshelf/model_cloudsql.py b/4-auth/bookshelf/model_cloudsql.py deleted file mode 100644 index c9456fcd..00000000 --- a/4-auth/bookshelf/model_cloudsql.py +++ /dev/null @@ -1,121 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from flask import Flask -from flask_sqlalchemy import SQLAlchemy - - -builtin_list = list - - -db = SQLAlchemy() - - -def init_app(app): - # Disable track modifications, as it unnecessarily uses memory. - app.config.setdefault('SQLALCHEMY_TRACK_MODIFICATIONS', False) - db.init_app(app) - - -def from_sql(row): - """Translates a SQLAlchemy model instance into a dictionary""" - data = row.__dict__.copy() - data['id'] = row.id - data.pop('_sa_instance_state') - return data - - -class Book(db.Model): - __tablename__ = 'books' - - id = db.Column(db.Integer, primary_key=True) - title = db.Column(db.String(255)) - author = db.Column(db.String(255)) - publishedDate = db.Column(db.String(255)) - imageUrl = db.Column(db.String(255)) - description = db.Column(db.String(4096)) - createdBy = db.Column(db.String(255)) - createdById = db.Column(db.String(255)) - - def __repr__(self): - return " - - - Bookshelf - Python on Google Cloud Platform - - - - - - -
- {% block content %}{% endblock %} -
- {{user}} - - diff --git a/4-auth/bookshelf/templates/form.html b/4-auth/bookshelf/templates/form.html deleted file mode 100644 index 2de32a29..00000000 --- a/4-auth/bookshelf/templates/form.html +++ /dev/null @@ -1,67 +0,0 @@ -{# -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -#} - -{% extends "base.html" %} - -{% block content %} -

{{action}} book

- -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- - - - - - - - -
- -{% endblock %} diff --git a/4-auth/bookshelf/templates/list.html b/4-auth/bookshelf/templates/list.html deleted file mode 100644 index 3362f0e2..00000000 --- a/4-auth/bookshelf/templates/list.html +++ /dev/null @@ -1,55 +0,0 @@ -{# -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -#} - -{% extends "base.html" %} - -{% block content %} - -

Books

- - - Add book - - -{% for book in books %} - -{% else %} -

No books found

-{% endfor %} - -{% if next_page_token %} - -{% endif %} - -{% endblock %} diff --git a/4-auth/bookshelf/templates/view.html b/4-auth/bookshelf/templates/view.html deleted file mode 100644 index cfa9138e..00000000 --- a/4-auth/bookshelf/templates/view.html +++ /dev/null @@ -1,53 +0,0 @@ -{# -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -#} - -{% extends "base.html" %} - -{% block content %} - -

Book

- - - -
-
- {% if book.imageUrl %} - - {% else %} - - {% endif %} -
-
-

- {{book.title}} - {{book.publishedDate}} -

-
By {{book.author|default('Unknown', True)}}
-

{{book.description}}

- Added by {{book.get('createdBy')|default('Anonymous', True)}} -
-
- -{% endblock %} diff --git a/4-auth/config.py b/4-auth/config.py deleted file mode 100644 index 767efbd1..00000000 --- a/4-auth/config.py +++ /dev/null @@ -1,112 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -This file contains all of the configuration values for the application. -Update this file with the values for your specific Google Cloud project. -You can create and manage projects at https://console.developers.google.com -""" - -import os - -# The secret key is used by Flask to encrypt session cookies. -# [START secret_key] -SECRET_KEY = 'secret' -# [END secret_key] - -# There are three different ways to store the data in the application. -# You can choose 'datastore', 'cloudsql', or 'mongodb'. Be sure to -# configure the respective settings for the one you choose below. -# You do not have to configure the other data backends. If unsure, choose -# 'datastore' as it does not require any additional configuration. -DATA_BACKEND = 'datastore' - -# Google Cloud Project ID. This can be found on the 'Overview' page at -# https://console.developers.google.com -PROJECT_ID = 'your-project-id' - -# CloudSQL & SQLAlchemy configuration -# Replace the following values the respective values of your Cloud SQL -# instance. -CLOUDSQL_USER = 'root' -CLOUDSQL_PASSWORD = 'your-cloudsql-password' -CLOUDSQL_DATABASE = 'bookshelf' -# Set this value to the Cloud SQL connection name, e.g. -# "project:region:cloudsql-instance". -# You must also update the value in app.yaml. -CLOUDSQL_CONNECTION_NAME = 'your-cloudsql-connection-name' - -# The CloudSQL proxy is used locally to connect to the cloudsql instance. -# To start the proxy, use: -# -# $ cloud_sql_proxy -instances=your-connection-name=tcp:3306 -# -# Port 3306 is the standard MySQL port. If you need to use a different port, -# change the 3306 to a different port number. - -# Alternatively, you could use a local MySQL instance for testing. -LOCAL_SQLALCHEMY_DATABASE_URI = ( - 'mysql+pymysql://{user}:{password}@127.0.0.1:3306/{database}').format( - user=CLOUDSQL_USER, password=CLOUDSQL_PASSWORD, - database=CLOUDSQL_DATABASE) - -# When running on App Engine a unix socket is used to connect to the cloudsql -# instance. -LIVE_SQLALCHEMY_DATABASE_URI = ( - 'mysql+pymysql://{user}:{password}@localhost/{database}' - '?unix_socket=/cloudsql/{connection_name}').format( - user=CLOUDSQL_USER, password=CLOUDSQL_PASSWORD, - database=CLOUDSQL_DATABASE, connection_name=CLOUDSQL_CONNECTION_NAME) - -if os.environ.get('GAE_INSTANCE'): - SQLALCHEMY_DATABASE_URI = LIVE_SQLALCHEMY_DATABASE_URI -else: - SQLALCHEMY_DATABASE_URI = LOCAL_SQLALCHEMY_DATABASE_URI - -# Mongo configuration -# If using mongolab, the connection URI is available from the mongolab control -# panel. If self-hosting on compute engine, replace the values below. -MONGO_URI = 'mongodb://user:password@host:27017/database' - -# Google Cloud Storage and upload settings. -# Typically, you'll name your bucket the same as your project. To create a -# bucket: -# -# $ gsutil mb gs:// -# -# You also need to make sure that the default ACL is set to public-read, -# otherwise users will not be able to see their upload images: -# -# $ gsutil defacl set public-read gs:// -# -# You can adjust the max content length and allow extensions settings to allow -# larger or more varied file types if desired. -CLOUD_STORAGE_BUCKET = 'your-bucket-name' -MAX_CONTENT_LENGTH = 8 * 1024 * 1024 -ALLOWED_EXTENSIONS = set(['png', 'jpg', 'jpeg', 'gif']) - -# OAuth2 configuration. -# This can be generated from the Google Developers Console at -# https://console.developers.google.com/project/_/apiui/credential. -# Note that you will need to add all URLs that your application uses as -# authorized redirect URIs. For example, typically you would add the following: -# -# * http://localhost:8080/oauth2callback -# * https://.appspot.com/oauth2callback. -# -# If you receive a invalid redirect URI error review you settings to ensure -# that the current URI is allowed. -GOOGLE_OAUTH2_CLIENT_ID = \ - 'your-client-id' -GOOGLE_OAUTH2_CLIENT_SECRET = 'your-client-secret' diff --git a/4-auth/main.py b/4-auth/main.py deleted file mode 100644 index d5697c6c..00000000 --- a/4-auth/main.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import bookshelf -import config - - -app = bookshelf.create_app(config) - - -# This is only used when running locally. When running live, gunicorn runs -# the application. -if __name__ == '__main__': - app.run(host='127.0.0.1', port=8080, debug=True) diff --git a/4-auth/requirements-dev.txt b/4-auth/requirements-dev.txt deleted file mode 100644 index 282373c0..00000000 --- a/4-auth/requirements-dev.txt +++ /dev/null @@ -1,7 +0,0 @@ -tox==3.5.3 -flake8==3.6.0 -flaky==3.4.0 -pytest==4.0.1 -pytest-cov==2.6.0 -retrying==1.3.3 -mock==2.0.0 diff --git a/4-auth/requirements.txt b/4-auth/requirements.txt deleted file mode 100644 index fa332f42..00000000 --- a/4-auth/requirements.txt +++ /dev/null @@ -1,10 +0,0 @@ -Flask>=1.0.0 -google-cloud-datastore==1.7.1 -google-cloud-storage==1.13.0 -gunicorn==19.9.0 -oauth2client==4.1.3 -Flask-SQLAlchemy==2.3.2 -PyMySQL==0.9.2 -Flask-PyMongo>=2.0.0 -PyMongo==3.7.2 -six==1.11.0 diff --git a/4-auth/tests/conftest.py b/4-auth/tests/conftest.py deleted file mode 100644 index 62c2a480..00000000 --- a/4-auth/tests/conftest.py +++ /dev/null @@ -1,91 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""conftest.py is used to define common test fixtures for pytest.""" - -import bookshelf -import config -from google.cloud.exceptions import ServiceUnavailable -from oauth2client.client import HttpAccessTokenRefreshError -import pytest -from retrying import retry - - -#@pytest.yield_fixture(params=['datastore', 'cloudsql', 'mongodb']) -@pytest.yield_fixture(params=['datastore', 'mongodb']) -def app(request): - """This fixtures provides a Flask app instance configured for testing. - - Because it's parametric, it will cause every test that uses this fixture - to run three times: one time for each backend (datastore, cloudsql, and - mongodb). - - It also ensures the tests run within a request context, allowing - any calls to flask.request, flask.current_app, etc. to work.""" - app = bookshelf.create_app( - config, - testing=True, - config_overrides={ - 'DATA_BACKEND': request.param - }) - - with app.test_request_context(): - yield app - - -@pytest.yield_fixture -def model(monkeypatch, app): - """This fixture provides a modified version of the app's model that tracks - all created items and deletes them at the end of the test. - - Any tests that directly or indirectly interact with the database should use - this to ensure that resources are properly cleaned up. - - Monkeypatch is provided by pytest and used to patch the model's create - method. - - The app fixture is needed to provide the configuration and context needed - to get the proper model object. - """ - model = bookshelf.get_model() - - # Ensure no books exist before running. This typically helps if tests - # somehow left the database in a bad state. - delete_all_books(model) - - yield model - - # Delete all books that we created during tests. - delete_all_books(model) - - -# The backend data stores can sometimes be flaky. It's useful to retry this -# a few times before giving up. -@retry( - stop_max_attempt_number=3, - wait_exponential_multiplier=100, - wait_exponential_max=2000) -def delete_all_books(model): - while True: - books, _ = model.list(limit=50) - if not books: - break - for book in books: - model.delete(book['id']) - - -def flaky_filter(info, *args): - """Used by flaky to determine when to re-run a test case.""" - _, e, _ = info - return isinstance(e, (ServiceUnavailable, HttpAccessTokenRefreshError)) diff --git a/4-auth/tests/test_auth.py b/4-auth/tests/test_auth.py deleted file mode 100644 index 491de9e0..00000000 --- a/4-auth/tests/test_auth.py +++ /dev/null @@ -1,133 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import contextlib - -import bookshelf -from conftest import flaky_filter -from flaky import flaky -import mock -from oauth2client.client import OAuth2Credentials -import pytest - - -@pytest.fixture -def client_with_credentials(app): - """This fixture provides a Flask app test client that has a session - pre-configured with use credentials.""" - credentials = OAuth2Credentials( - 'access_token', - 'client_id', - 'client_secret', - 'refresh_token', - '3600', - None, - 'Test', - id_token={'sub': '123', 'email': 'user@example.com'}, - scopes=('email', 'profile')) - - @contextlib.contextmanager - def inner(): - with app.test_client() as client: - with client.session_transaction() as session: - session['profile'] = {'email': 'abc@example.com', 'name': 'Test User'} - session['google_oauth2_credentials'] = credentials.to_json() - yield client - - return inner - - -# Mark all test cases in this class as flaky, so that if errors occur they -# can be retried. This is useful when databases are temporarily unavailable. -@flaky(rerun_filter=flaky_filter) -# Tell pytest to use both the app and model fixtures for all test cases. -# This ensures that configuration is properly applied and that all database -# resources created during tests are cleaned up. These fixtures are defined -# in conftest.py -@pytest.mark.usefixtures('app', 'model') -class TestAuth(object): - def test_not_logged_in(self, app): - with app.test_client() as c: - rv = c.get('/books/') - - assert rv.status == '200 OK' - body = rv.data.decode('utf-8') - assert 'Login' in body - - def test_logged_in(self, client_with_credentials): - with client_with_credentials() as c: - rv = c.get('/books/') - - assert rv.status == '200 OK' - body = rv.data.decode('utf-8') - assert 'Test User' in body - - def test_add_anonymous(self, app): - data = { - 'title': 'Test Book', - } - - with app.test_client() as c: - rv = c.post('/books/add', data=data, follow_redirects=True) - - assert rv.status == '200 OK' - body = rv.data.decode('utf-8') - assert 'Test Book' in body - assert 'Added by Anonymous' in body - - def test_add_logged_in(self, client_with_credentials): - data = { - 'title': 'Test Book', - } - - with client_with_credentials() as c: - rv = c.post('/books/add', data=data, follow_redirects=True) - - assert rv.status == '200 OK' - body = rv.data.decode('utf-8') - assert 'Test Book' in body - assert 'Added by Test User' in body - - def test_mine(self, model, client_with_credentials): - # Create two books, one created by the logged in user and one - # created by another user. - model.create({ - 'title': 'Book 1', - 'createdById': 'abc@example.com' - }) - - model.create({ - 'title': 'Book 2', - 'createdById': 'def@example.com' - }) - - # Check the "My Books" page and make sure only one of the books - # appears. - with client_with_credentials() as c: - rv = c.get('/books/mine') - - assert rv.status == '200 OK' - body = rv.data.decode('utf-8') - assert 'Book 1' in body - assert 'Book 2' not in body - - @mock.patch("httplib2.Http") - def test_request_user_info(self, HttpMock): - httpObj = mock.MagicMock() - responseMock = mock.MagicMock(status=200) - httpObj.request = mock.MagicMock( - return_value=(responseMock, b'{"name": "bill"}')) - HttpMock.return_value = httpObj - credentials = mock.MagicMock() - bookshelf._request_user_info(credentials) diff --git a/4-auth/tests/test_crud.py b/4-auth/tests/test_crud.py deleted file mode 100644 index d3ac58dc..00000000 --- a/4-auth/tests/test_crud.py +++ /dev/null @@ -1,90 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import re -import time - -from conftest import flaky_filter -from flaky import flaky -import pytest - - -# Mark all test cases in this class as flaky, so that if errors occur they -# can be retried. This is useful when databases are temporarily unavailable. -@flaky(rerun_filter=flaky_filter) -# Tell pytest to use both the app and model fixtures for all test cases. -# This ensures that configuration is properly applied and that all database -# resources created during tests are cleaned up. These fixtures are defined -# in conftest.py -@pytest.mark.usefixtures('app', 'model') -class TestCrudActions(object): - - def test_list(self, app, model): - for i in range(1, 12): - model.create({'title': u'Book {0}'.format(i)}) - time.sleep(2) # Wait for data to propogate - - with app.test_client() as c: - rv = c.get('/books/') - - assert rv.status == '200 OK' - - body = rv.data.decode('utf-8') - assert 'Book 1' in body, "Should show books" - assert len(re.findall('

Book', body)) == 10, ( - "Should not show more than 10 books") - assert 'More' in body, "Should have more than one page" - - def test_add(self, app): - data = { - 'title': 'Test Book', - 'author': 'Test Author', - 'publishedDate': 'Test Date Published', - 'description': 'Test Description' - } - - with app.test_client() as c: - rv = c.post('/books/add', data=data, follow_redirects=True) - - assert rv.status == '200 OK' - body = rv.data.decode('utf-8') - assert 'Test Book' in body - assert 'Test Author' in body - assert 'Test Date Published' in body - assert 'Test Description' in body - - def test_edit(self, app, model): - existing = model.create({'title': "Temp Title"}) - - with app.test_client() as c: - rv = c.post( - '/books/%s/edit' % existing['id'], - data={'title': 'Updated Title'}, - follow_redirects=True) - - assert rv.status == '200 OK' - body = rv.data.decode('utf-8') - assert 'Updated Title' in body - assert 'Temp Title' not in body - - def test_delete(self, app, model): - existing = model.create({'title': "Temp Title"}) - - with app.test_client() as c: - rv = c.get( - '/books/%s/delete' % existing['id'], - follow_redirects=True) - - assert rv.status == '200 OK' - assert not model.read(existing['id']) diff --git a/4-auth/tests/test_storage.py b/4-auth/tests/test_storage.py deleted file mode 100644 index 531c8827..00000000 --- a/4-auth/tests/test_storage.py +++ /dev/null @@ -1,70 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import re - -from conftest import flaky_filter -from flaky import flaky -import httplib2 -import pytest -from six import BytesIO - - -# Mark all test cases in this class as flaky, so that if errors occur they -# can be retried. This is useful when databases are temporarily unavailable. -@flaky(rerun_filter=flaky_filter) -# Tell pytest to use both the app and model fixtures for all test cases. -# This ensures that configuration is properly applied and that all database -# resources created during tests are cleaned up. These fixtures are defined -# in conftest.py -@pytest.mark.usefixtures('app', 'model') -class TestStorage(object): - - def test_upload_image(self, app): - data = { - 'title': 'Test Book', - 'author': 'Test Author', - 'publishedDate': 'Test Date Published', - 'description': 'Test Description', - 'image': (BytesIO(b'hello world'), 'hello.jpg') - } - - with app.test_client() as c: - rv = c.post('/books/add', data=data, follow_redirects=True) - - assert rv.status == '200 OK' - body = rv.data.decode('utf-8') - - img_tag = re.search(''), - '1337h4x0r.php') - } - - with app.test_client() as c: - rv = c.post('/books/add', data=data, follow_redirects=True) - - # check we weren't pwned - assert rv.status == '400 BAD REQUEST' diff --git a/4-auth/tox.ini b/4-auth/tox.ini deleted file mode 100644 index 40c86660..00000000 --- a/4-auth/tox.ini +++ /dev/null @@ -1,19 +0,0 @@ -[tox] -skipsdist = True -envlist = lint,py27,py36 - -[testenv] -deps = - -rrequirements.txt - -rrequirements-dev.txt -commands = - py.test --cov=bookshelf --no-success-flaky-report {posargs} tests -passenv = GOOGLE_APPLICATION_CREDENTIALS DATASTORE_HOST -setenv = PYTHONPATH={toxinidir} - -[testenv:lint] -deps = - flake8 - flake8-import-order -commands = - flake8 --import-order-style=google bookshelf tests diff --git a/5-logging/app.yaml b/5-logging/app.yaml deleted file mode 100644 index aa5ddf46..00000000 --- a/5-logging/app.yaml +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# This file specifies your Python application's runtime configuration. -# See https://cloud.google.com/appengine/docs/managed-vms/python/runtime -# for details. - -runtime: python -env: flex -entrypoint: gunicorn -b :$PORT main:app - -runtime_config: - python_version: 3 - -beta_settings: - # If using Cloud SQL, uncomment and set this value to the Cloud SQL - # connection name, e.g. - # "project:region:cloudsql-instance" - # You must also update the values in config.py. - # - # cloud_sql_instances: "your-cloudsql-connection-name" diff --git a/5-logging/bookshelf/__init__.py b/5-logging/bookshelf/__init__.py deleted file mode 100644 index 2289afd4..00000000 --- a/5-logging/bookshelf/__init__.py +++ /dev/null @@ -1,126 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import json -import logging - -from flask import current_app, Flask, redirect, request, session, url_for -from google.cloud import error_reporting -import google.cloud.logging -import httplib2 -from oauth2client.contrib.flask_util import UserOAuth2 - - -oauth2 = UserOAuth2() - - -def create_app(config, debug=False, testing=False, config_overrides=None): - app = Flask(__name__) - app.config.from_object(config) - - app.debug = debug - app.testing = testing - - if config_overrides: - app.config.update(config_overrides) - - # [START setup_logging] - if not app.testing: - client = google.cloud.logging.Client(app.config['PROJECT_ID']) - # Attaches a Google Stackdriver logging handler to the root logger - client.setup_logging(logging.INFO) - # [END setup_logging] - - # Setup the data model. - with app.app_context(): - model = get_model() - model.init_app(app) - - # Initalize the OAuth2 helper. - oauth2.init_app( - app, - scopes=['email', 'profile'], - authorize_callback=_request_user_info) - - # Add a logout handler. - @app.route('/logout') - def logout(): - # Delete the user's profile and the credentials stored by oauth2. - del session['profile'] - session.modified = True - oauth2.storage.delete() - return redirect(request.referrer or '/') - - # Register the Bookshelf CRUD blueprint. - from .crud import crud - app.register_blueprint(crud, url_prefix='/books') - - # Add a default root route. - @app.route("/") - def index(): - return redirect(url_for('crud.list')) - - # Add an error handler that reports exceptions to Stackdriver Error - # Reporting. Note that this error handler is only used when debug - # is False - # [START setup_error_reporting] - @app.errorhandler(500) - def server_error(e): - client = error_reporting.Client(app.config['PROJECT_ID']) - client.report_exception( - http_context=error_reporting.build_flask_context(request)) - return """ - An internal error occurred. - """, 500 - # [END setup_error_reporting] - - return app - - -def get_model(): - model_backend = current_app.config['DATA_BACKEND'] - if model_backend == 'cloudsql': - from . import model_cloudsql - model = model_cloudsql - elif model_backend == 'datastore': - from . import model_datastore - model = model_datastore - elif model_backend == 'mongodb': - from . import model_mongodb - model = model_mongodb - else: - raise ValueError( - "No appropriate databackend configured. " - "Please specify datastore, cloudsql, or mongodb") - - return model - - -def _request_user_info(credentials): - """ - Makes an HTTP request to the Google OAuth2 API to retrieve the user's basic - profile information, including full name and photo, and stores it in the - Flask session. - """ - http = httplib2.Http() - credentials.authorize(http) - resp, content = http.request( - 'https://www.googleapis.com/oauth2/v3/userinfo') - - if resp.status != 200: - current_app.logger.error( - "Error while obtaining user profile: \n%s: %s", resp, content) - return None - - session['profile'] = json.loads(content.decode('utf-8')) diff --git a/5-logging/bookshelf/crud.py b/5-logging/bookshelf/crud.py deleted file mode 100644 index 834cb3d5..00000000 --- a/5-logging/bookshelf/crud.py +++ /dev/null @@ -1,125 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from bookshelf import get_model, oauth2, storage -from flask import Blueprint, current_app, redirect, render_template, request, \ - session, url_for - - -crud = Blueprint('crud', __name__) - - -def upload_image_file(file): - """ - Upload the user-uploaded file to Google Cloud Storage and retrieve its - publicly-accessible URL. - """ - if not file: - return None - - public_url = storage.upload_file( - file.read(), - file.filename, - file.content_type - ) - - current_app.logger.info( - "Uploaded file %s as %s.", file.filename, public_url) - - return public_url - - -@crud.route("/") -def list(): - token = request.args.get('page_token', None) - if token: - token = token.encode('utf-8') - - books, next_page_token = get_model().list(cursor=token) - - return render_template( - "list.html", - books=books, - next_page_token=next_page_token) - - -@crud.route("/mine") -@oauth2.required -def list_mine(): - token = request.args.get('page_token', None) - if token: - token = token.encode('utf-8') - - books, next_page_token = get_model().list_by_user( - user_id=session['profile']['email'], - cursor=token) - - return render_template( - "list.html", - books=books, - next_page_token=next_page_token) - - -@crud.route('/') -def view(id): - book = get_model().read(id) - return render_template("view.html", book=book) - - -@crud.route('/add', methods=['GET', 'POST']) -def add(): - if request.method == 'POST': - data = request.form.to_dict(flat=True) - - # If an image was uploaded, update the data to point to the new image. - image_url = upload_image_file(request.files.get('image')) - - if image_url: - data['imageUrl'] = image_url - - # If the user is logged in, associate their profile with the new book. - if 'profile' in session: - data['createdBy'] = session['profile']['name'] - data['createdById'] = session['profile']['email'] - - book = get_model().create(data) - - return redirect(url_for('.view', id=book['id'])) - - return render_template("form.html", action="Add", book={}) - - -@crud.route('//edit', methods=['GET', 'POST']) -def edit(id): - book = get_model().read(id) - - if request.method == 'POST': - data = request.form.to_dict(flat=True) - - image_url = upload_image_file(request.files.get('image')) - - if image_url: - data['imageUrl'] = image_url - - book = get_model().update(data, id) - - return redirect(url_for('.view', id=book['id'])) - - return render_template("form.html", action="Edit", book=book) - - -@crud.route('//delete') -def delete(id): - get_model().delete(id) - return redirect(url_for('.list')) diff --git a/5-logging/bookshelf/model_cloudsql.py b/5-logging/bookshelf/model_cloudsql.py deleted file mode 100644 index d84207d2..00000000 --- a/5-logging/bookshelf/model_cloudsql.py +++ /dev/null @@ -1,119 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from flask import Flask -from flask_sqlalchemy import SQLAlchemy - - -builtin_list = list - - -db = SQLAlchemy() - - -def init_app(app): - # Disable track modifications, as it unnecessarily uses memory. - app.config.setdefault('SQLALCHEMY_TRACK_MODIFICATIONS', False) - db.init_app(app) - - -def from_sql(row): - """Translates a SQLAlchemy model instance into a dictionary""" - data = row.__dict__.copy() - data['id'] = row.id - data.pop('_sa_instance_state') - return data - - -class Book(db.Model): - __tablename__ = 'books' - - id = db.Column(db.Integer, primary_key=True) - title = db.Column(db.String(255)) - author = db.Column(db.String(255)) - publishedDate = db.Column(db.String(255)) - imageUrl = db.Column(db.String(255)) - description = db.Column(db.String(4096)) - createdBy = db.Column(db.String(255)) - createdById = db.Column(db.String(255)) - - def __repr__(self): - return " - - - Bookshelf - Python on Google Cloud Platform - - - - - - -
- {% block content %}{% endblock %} -
- {{user}} - - diff --git a/5-logging/bookshelf/templates/form.html b/5-logging/bookshelf/templates/form.html deleted file mode 100644 index 2de32a29..00000000 --- a/5-logging/bookshelf/templates/form.html +++ /dev/null @@ -1,67 +0,0 @@ -{# -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -#} - -{% extends "base.html" %} - -{% block content %} -

{{action}} book

- -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- - - - - - - - -
- -{% endblock %} diff --git a/5-logging/bookshelf/templates/list.html b/5-logging/bookshelf/templates/list.html deleted file mode 100644 index 3362f0e2..00000000 --- a/5-logging/bookshelf/templates/list.html +++ /dev/null @@ -1,55 +0,0 @@ -{# -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -#} - -{% extends "base.html" %} - -{% block content %} - -

Books

- - - Add book - - -{% for book in books %} - -{% else %} -

No books found

-{% endfor %} - -{% if next_page_token %} - -{% endif %} - -{% endblock %} diff --git a/5-logging/bookshelf/templates/view.html b/5-logging/bookshelf/templates/view.html deleted file mode 100644 index e654e8ab..00000000 --- a/5-logging/bookshelf/templates/view.html +++ /dev/null @@ -1,53 +0,0 @@ -{# -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -#} - -{% extends "base.html" %} - -{% block content %} - -

Book

- - - -
-
- {% if book.imageUrl %} - - {% else %} - - {% endif %} -
-
-

- {{book.title}} - {{book.publishedDate}} -

-
By {{book.author|default('Unknown', True)}}
-

{{book.description}}

- Added by {{book.createdBy|default('Anonymous', True)}} -
-
- -{% endblock %} diff --git a/5-logging/config.py b/5-logging/config.py deleted file mode 100644 index b64eecd2..00000000 --- a/5-logging/config.py +++ /dev/null @@ -1,110 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -This file contains all of the configuration values for the application. -Update this file with the values for your specific Google Cloud project. -You can create and manage projects at https://console.developers.google.com -""" - -import os - -# The secret key is used by Flask to encrypt session cookies. -SECRET_KEY = 'secret' - -# There are three different ways to store the data in the application. -# You can choose 'datastore', 'cloudsql', or 'mongodb'. Be sure to -# configure the respective settings for the one you choose below. -# You do not have to configure the other data backends. If unsure, choose -# 'datastore' as it does not require any additional configuration. -DATA_BACKEND = 'datastore' - -# Google Cloud Project ID. This can be found on the 'Overview' page at -# https://console.developers.google.com -PROJECT_ID = 'your-project-id' - -# CloudSQL & SQLAlchemy configuration -# Replace the following values the respective values of your Cloud SQL -# instance. -CLOUDSQL_USER = 'root' -CLOUDSQL_PASSWORD = 'your-cloudsql-password' -CLOUDSQL_DATABASE = 'bookshelf' -# Set this value to the Cloud SQL connection name, e.g. -# "project:region:cloudsql-instance". -# You must also update the value in app.yaml. -CLOUDSQL_CONNECTION_NAME = 'your-cloudsql-connection-name' - -# The CloudSQL proxy is used locally to connect to the cloudsql instance. -# To start the proxy, use: -# -# $ cloud_sql_proxy -instances=your-connection-name=tcp:3306 -# -# Port 3306 is the standard MySQL port. If you need to use a different port, -# change the 3306 to a different port number. - -# Alternatively, you could use a local MySQL instance for testing. -LOCAL_SQLALCHEMY_DATABASE_URI = ( - 'mysql+pymysql://{user}:{password}@127.0.0.1:3306/{database}').format( - user=CLOUDSQL_USER, password=CLOUDSQL_PASSWORD, - database=CLOUDSQL_DATABASE) - -# When running on App Engine a unix socket is used to connect to the cloudsql -# instance. -LIVE_SQLALCHEMY_DATABASE_URI = ( - 'mysql+pymysql://{user}:{password}@localhost/{database}' - '?unix_socket=/cloudsql/{connection_name}').format( - user=CLOUDSQL_USER, password=CLOUDSQL_PASSWORD, - database=CLOUDSQL_DATABASE, connection_name=CLOUDSQL_CONNECTION_NAME) - -if os.environ.get('GAE_INSTANCE'): - SQLALCHEMY_DATABASE_URI = LIVE_SQLALCHEMY_DATABASE_URI -else: - SQLALCHEMY_DATABASE_URI = LOCAL_SQLALCHEMY_DATABASE_URI - -# Mongo configuration -# If using mongolab, the connection URI is available from the mongolab control -# panel. If self-hosting on compute engine, replace the values below. -MONGO_URI = 'mongodb://user:password@host:27017/database' - -# Google Cloud Storage and upload settings. -# Typically, you'll name your bucket the same as your project. To create a -# bucket: -# -# $ gsutil mb gs:// -# -# You also need to make sure that the default ACL is set to public-read, -# otherwise users will not be able to see their upload images: -# -# $ gsutil defacl set public-read gs:// -# -# You can adjust the max content length and allow extensions settings to allow -# larger or more varied file types if desired. -CLOUD_STORAGE_BUCKET = 'your-bucket-name' -MAX_CONTENT_LENGTH = 8 * 1024 * 1024 -ALLOWED_EXTENSIONS = set(['png', 'jpg', 'jpeg', 'gif']) - -# OAuth2 configuration. -# This can be generated from the Google Developers Console at -# https://console.developers.google.com/project/_/apiui/credential. -# Note that you will need to add all URLs that your application uses as -# authorized redirect URIs. For example, typically you would add the following: -# -# * http://localhost:8080/oauth2callback -# * https://.appspot.com/oauth2callback. -# -# If you receive a invalid redirect URI error review you settings to ensure -# that the current URI is allowed. -GOOGLE_OAUTH2_CLIENT_ID = \ - 'your-client-id' -GOOGLE_OAUTH2_CLIENT_SECRET = 'your-client-secret' diff --git a/5-logging/main.py b/5-logging/main.py deleted file mode 100644 index d5697c6c..00000000 --- a/5-logging/main.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import bookshelf -import config - - -app = bookshelf.create_app(config) - - -# This is only used when running locally. When running live, gunicorn runs -# the application. -if __name__ == '__main__': - app.run(host='127.0.0.1', port=8080, debug=True) diff --git a/5-logging/requirements-dev.txt b/5-logging/requirements-dev.txt deleted file mode 100644 index 282373c0..00000000 --- a/5-logging/requirements-dev.txt +++ /dev/null @@ -1,7 +0,0 @@ -tox==3.5.3 -flake8==3.6.0 -flaky==3.4.0 -pytest==4.0.1 -pytest-cov==2.6.0 -retrying==1.3.3 -mock==2.0.0 diff --git a/5-logging/requirements.txt b/5-logging/requirements.txt deleted file mode 100644 index 32ff3033..00000000 --- a/5-logging/requirements.txt +++ /dev/null @@ -1,12 +0,0 @@ -Flask>=1.0.0 -google-cloud-datastore==1.7.1 -google-cloud-storage==1.13.0 -google-cloud-logging==1.8.0 -google-cloud-error_reporting==0.30.0 -gunicorn==19.9.0 -oauth2client==4.1.3 -Flask-SQLAlchemy==2.3.2 -PyMySQL==0.9.2 -Flask-PyMongo>=2.0.0 -PyMongo==3.7.2 -six==1.11.0 diff --git a/5-logging/tests/conftest.py b/5-logging/tests/conftest.py deleted file mode 100644 index 62c2a480..00000000 --- a/5-logging/tests/conftest.py +++ /dev/null @@ -1,91 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""conftest.py is used to define common test fixtures for pytest.""" - -import bookshelf -import config -from google.cloud.exceptions import ServiceUnavailable -from oauth2client.client import HttpAccessTokenRefreshError -import pytest -from retrying import retry - - -#@pytest.yield_fixture(params=['datastore', 'cloudsql', 'mongodb']) -@pytest.yield_fixture(params=['datastore', 'mongodb']) -def app(request): - """This fixtures provides a Flask app instance configured for testing. - - Because it's parametric, it will cause every test that uses this fixture - to run three times: one time for each backend (datastore, cloudsql, and - mongodb). - - It also ensures the tests run within a request context, allowing - any calls to flask.request, flask.current_app, etc. to work.""" - app = bookshelf.create_app( - config, - testing=True, - config_overrides={ - 'DATA_BACKEND': request.param - }) - - with app.test_request_context(): - yield app - - -@pytest.yield_fixture -def model(monkeypatch, app): - """This fixture provides a modified version of the app's model that tracks - all created items and deletes them at the end of the test. - - Any tests that directly or indirectly interact with the database should use - this to ensure that resources are properly cleaned up. - - Monkeypatch is provided by pytest and used to patch the model's create - method. - - The app fixture is needed to provide the configuration and context needed - to get the proper model object. - """ - model = bookshelf.get_model() - - # Ensure no books exist before running. This typically helps if tests - # somehow left the database in a bad state. - delete_all_books(model) - - yield model - - # Delete all books that we created during tests. - delete_all_books(model) - - -# The backend data stores can sometimes be flaky. It's useful to retry this -# a few times before giving up. -@retry( - stop_max_attempt_number=3, - wait_exponential_multiplier=100, - wait_exponential_max=2000) -def delete_all_books(model): - while True: - books, _ = model.list(limit=50) - if not books: - break - for book in books: - model.delete(book['id']) - - -def flaky_filter(info, *args): - """Used by flaky to determine when to re-run a test case.""" - _, e, _ = info - return isinstance(e, (ServiceUnavailable, HttpAccessTokenRefreshError)) diff --git a/5-logging/tests/test_auth.py b/5-logging/tests/test_auth.py deleted file mode 100644 index 491de9e0..00000000 --- a/5-logging/tests/test_auth.py +++ /dev/null @@ -1,133 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import contextlib - -import bookshelf -from conftest import flaky_filter -from flaky import flaky -import mock -from oauth2client.client import OAuth2Credentials -import pytest - - -@pytest.fixture -def client_with_credentials(app): - """This fixture provides a Flask app test client that has a session - pre-configured with use credentials.""" - credentials = OAuth2Credentials( - 'access_token', - 'client_id', - 'client_secret', - 'refresh_token', - '3600', - None, - 'Test', - id_token={'sub': '123', 'email': 'user@example.com'}, - scopes=('email', 'profile')) - - @contextlib.contextmanager - def inner(): - with app.test_client() as client: - with client.session_transaction() as session: - session['profile'] = {'email': 'abc@example.com', 'name': 'Test User'} - session['google_oauth2_credentials'] = credentials.to_json() - yield client - - return inner - - -# Mark all test cases in this class as flaky, so that if errors occur they -# can be retried. This is useful when databases are temporarily unavailable. -@flaky(rerun_filter=flaky_filter) -# Tell pytest to use both the app and model fixtures for all test cases. -# This ensures that configuration is properly applied and that all database -# resources created during tests are cleaned up. These fixtures are defined -# in conftest.py -@pytest.mark.usefixtures('app', 'model') -class TestAuth(object): - def test_not_logged_in(self, app): - with app.test_client() as c: - rv = c.get('/books/') - - assert rv.status == '200 OK' - body = rv.data.decode('utf-8') - assert 'Login' in body - - def test_logged_in(self, client_with_credentials): - with client_with_credentials() as c: - rv = c.get('/books/') - - assert rv.status == '200 OK' - body = rv.data.decode('utf-8') - assert 'Test User' in body - - def test_add_anonymous(self, app): - data = { - 'title': 'Test Book', - } - - with app.test_client() as c: - rv = c.post('/books/add', data=data, follow_redirects=True) - - assert rv.status == '200 OK' - body = rv.data.decode('utf-8') - assert 'Test Book' in body - assert 'Added by Anonymous' in body - - def test_add_logged_in(self, client_with_credentials): - data = { - 'title': 'Test Book', - } - - with client_with_credentials() as c: - rv = c.post('/books/add', data=data, follow_redirects=True) - - assert rv.status == '200 OK' - body = rv.data.decode('utf-8') - assert 'Test Book' in body - assert 'Added by Test User' in body - - def test_mine(self, model, client_with_credentials): - # Create two books, one created by the logged in user and one - # created by another user. - model.create({ - 'title': 'Book 1', - 'createdById': 'abc@example.com' - }) - - model.create({ - 'title': 'Book 2', - 'createdById': 'def@example.com' - }) - - # Check the "My Books" page and make sure only one of the books - # appears. - with client_with_credentials() as c: - rv = c.get('/books/mine') - - assert rv.status == '200 OK' - body = rv.data.decode('utf-8') - assert 'Book 1' in body - assert 'Book 2' not in body - - @mock.patch("httplib2.Http") - def test_request_user_info(self, HttpMock): - httpObj = mock.MagicMock() - responseMock = mock.MagicMock(status=200) - httpObj.request = mock.MagicMock( - return_value=(responseMock, b'{"name": "bill"}')) - HttpMock.return_value = httpObj - credentials = mock.MagicMock() - bookshelf._request_user_info(credentials) diff --git a/5-logging/tests/test_crud.py b/5-logging/tests/test_crud.py deleted file mode 100644 index c0d2f40f..00000000 --- a/5-logging/tests/test_crud.py +++ /dev/null @@ -1,88 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import re - -from conftest import flaky_filter -from flaky import flaky -import pytest - - -# Mark all test cases in this class as flaky, so that if errors occur they -# can be retried. This is useful when databases are temporarily unavailable. -@flaky(rerun_filter=flaky_filter) -# Tell pytest to use both the app and model fixtures for all test cases. -# This ensures that configuration is properly applied and that all database -# resources created during tests are cleaned up. These fixtures are defined -# in conftest.py -@pytest.mark.usefixtures('app', 'model') -class TestCrudActions(object): - - def test_list(self, app, model): - for i in range(1, 12): - model.create({'title': u'Book {0}'.format(i)}) - - with app.test_client() as c: - rv = c.get('/books/') - - assert rv.status == '200 OK' - - body = rv.data.decode('utf-8') - assert 'Book 1' in body, "Should show books" - assert len(re.findall('

Book', body)) == 10, ( - "Should not show more than 10 books") - assert 'More' in body, "Should have more than one page" - - def test_add(self, app): - data = { - 'title': 'Test Book', - 'author': 'Test Author', - 'publishedDate': 'Test Date Published', - 'description': 'Test Description' - } - - with app.test_client() as c: - rv = c.post('/books/add', data=data, follow_redirects=True) - - assert rv.status == '200 OK' - body = rv.data.decode('utf-8') - assert 'Test Book' in body - assert 'Test Author' in body - assert 'Test Date Published' in body - assert 'Test Description' in body - - def test_edit(self, app, model): - existing = model.create({'title': "Temp Title"}) - - with app.test_client() as c: - rv = c.post( - '/books/%s/edit' % existing['id'], - data={'title': 'Updated Title'}, - follow_redirects=True) - - assert rv.status == '200 OK' - body = rv.data.decode('utf-8') - assert 'Updated Title' in body - assert 'Temp Title' not in body - - def test_delete(self, app, model): - existing = model.create({'title': "Temp Title"}) - - with app.test_client() as c: - rv = c.get( - '/books/%s/delete' % existing['id'], - follow_redirects=True) - - assert rv.status == '200 OK' - assert not model.read(existing['id']) diff --git a/5-logging/tests/test_storage.py b/5-logging/tests/test_storage.py deleted file mode 100644 index 531c8827..00000000 --- a/5-logging/tests/test_storage.py +++ /dev/null @@ -1,70 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import re - -from conftest import flaky_filter -from flaky import flaky -import httplib2 -import pytest -from six import BytesIO - - -# Mark all test cases in this class as flaky, so that if errors occur they -# can be retried. This is useful when databases are temporarily unavailable. -@flaky(rerun_filter=flaky_filter) -# Tell pytest to use both the app and model fixtures for all test cases. -# This ensures that configuration is properly applied and that all database -# resources created during tests are cleaned up. These fixtures are defined -# in conftest.py -@pytest.mark.usefixtures('app', 'model') -class TestStorage(object): - - def test_upload_image(self, app): - data = { - 'title': 'Test Book', - 'author': 'Test Author', - 'publishedDate': 'Test Date Published', - 'description': 'Test Description', - 'image': (BytesIO(b'hello world'), 'hello.jpg') - } - - with app.test_client() as c: - rv = c.post('/books/add', data=data, follow_redirects=True) - - assert rv.status == '200 OK' - body = rv.data.decode('utf-8') - - img_tag = re.search(''), - '1337h4x0r.php') - } - - with app.test_client() as c: - rv = c.post('/books/add', data=data, follow_redirects=True) - - # check we weren't pwned - assert rv.status == '400 BAD REQUEST' diff --git a/5-logging/tox.ini b/5-logging/tox.ini deleted file mode 100644 index 40c86660..00000000 --- a/5-logging/tox.ini +++ /dev/null @@ -1,19 +0,0 @@ -[tox] -skipsdist = True -envlist = lint,py27,py36 - -[testenv] -deps = - -rrequirements.txt - -rrequirements-dev.txt -commands = - py.test --cov=bookshelf --no-success-flaky-report {posargs} tests -passenv = GOOGLE_APPLICATION_CREDENTIALS DATASTORE_HOST -setenv = PYTHONPATH={toxinidir} - -[testenv:lint] -deps = - flake8 - flake8-import-order -commands = - flake8 --import-order-style=google bookshelf tests diff --git a/6-pubsub/app.yaml b/6-pubsub/app.yaml deleted file mode 100644 index 89284aa0..00000000 --- a/6-pubsub/app.yaml +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# This file specifies your Python application's runtime configuration. -# See https://cloud.google.com/appengine/docs/managed-vms/python/runtime -# for details. - -runtime: python -env: flex - -# [START entrypoint] -# Instead of using gunicorn directly, we'll use Honcho. Honcho is a python port -# of the Foreman process manager. For the default service, only the -# frontend process is needed. -entrypoint: honcho start -f /app/procfile bookshelf -# [END entrypoint] - -runtime_config: - python_version: 3 - -beta_settings: - # If using Cloud SQL, uncomment and set this value to the Cloud SQL - # connection name, e.g. - # "project:region:cloudsql-instance" - # You must also update the values in config.py. - # - # cloud_sql_instances: "your-cloudsql-connection-name" diff --git a/6-pubsub/bookshelf/__init__.py b/6-pubsub/bookshelf/__init__.py deleted file mode 100644 index a40b4913..00000000 --- a/6-pubsub/bookshelf/__init__.py +++ /dev/null @@ -1,122 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import json -import logging - -from flask import current_app, Flask, redirect, request, session, url_for -from google.cloud import error_reporting -import google.cloud.logging -import httplib2 -from oauth2client.contrib.flask_util import UserOAuth2 - - -oauth2 = UserOAuth2() - - -def create_app(config, debug=False, testing=False, config_overrides=None): - app = Flask(__name__) - app.config.from_object(config) - - app.debug = debug - app.testing = testing - - if config_overrides: - app.config.update(config_overrides) - - # Configure logging - if not app.testing: - client = google.cloud.logging.Client(app.config['PROJECT_ID']) - # Attaches a Google Stackdriver logging handler to the root logger - client.setup_logging(logging.INFO) - # Setup the data model. - with app.app_context(): - model = get_model() - model.init_app(app) - - # Initalize the OAuth2 helper. - oauth2.init_app( - app, - scopes=['email', 'profile'], - authorize_callback=_request_user_info) - - # Add a logout handler. - @app.route('/logout') - def logout(): - # Delete the user's profile and the credentials stored by oauth2. - del session['profile'] - session.modified = True - oauth2.storage.delete() - return redirect(request.referrer or '/') - - # Register the Bookshelf CRUD blueprint. - from .crud import crud - app.register_blueprint(crud, url_prefix='/books') - - # Add a default root route. - @app.route("/") - def index(): - return redirect(url_for('crud.list')) - - # Add an error handler that reports exceptions to Stackdriver Error - # Reporting. Note that this error handler is only used when debug - # is False - @app.errorhandler(500) - def server_error(e): - client = error_reporting.Client(app.config['PROJECT_ID']) - client.report_exception( - http_context=error_reporting.build_flask_context(request)) - return """ - An internal error occurred. - """, 500 - - return app - - -def get_model(): - model_backend = current_app.config['DATA_BACKEND'] - if model_backend == 'cloudsql': - from . import model_cloudsql - model = model_cloudsql - elif model_backend == 'datastore': - from . import model_datastore - model = model_datastore - elif model_backend == 'mongodb': - from . import model_mongodb - model = model_mongodb - else: - raise ValueError( - "No appropriate databackend configured. " - "Please specify datastore, cloudsql, or mongodb") - - return model - - -def _request_user_info(credentials): - """ - Makes an HTTP request to the Google OAuth2 API to retrieve the user's basic - profile information, including full name and photo, and stores it in the - Flask session. - """ - http = httplib2.Http() - credentials.authorize(http) - resp, content = http.request( - 'https://www.googleapis.com/oauth2/v3/userinfo') - - if resp.status != 200: - current_app.logger.error( - "Error while obtaining user profile: \n%s: %s", resp, content) - return None - - session['profile'] = json.loads(content.decode('utf-8')) diff --git a/6-pubsub/bookshelf/crud.py b/6-pubsub/bookshelf/crud.py deleted file mode 100644 index e2f4b429..00000000 --- a/6-pubsub/bookshelf/crud.py +++ /dev/null @@ -1,133 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from bookshelf import get_model, oauth2, storage, tasks -from flask import Blueprint, current_app, redirect, render_template, request, \ - session, url_for - - -crud = Blueprint('crud', __name__) - - -def upload_image_file(file): - """ - Upload the user-uploaded file to Google Cloud Storage and retrieve its - publicly-accessible URL. - """ - if not file: - return None - - public_url = storage.upload_file( - file.read(), - file.filename, - file.content_type - ) - - current_app.logger.info( - "Uploaded file %s as %s.", file.filename, public_url) - - return public_url - - -@crud.route("/") -def list(): - token = request.args.get('page_token', None) - if token: - token = token.encode('utf-8') - - books, next_page_token = get_model().list(cursor=token) - - return render_template( - "list.html", - books=books, - next_page_token=next_page_token) - - -@crud.route("/mine") -@oauth2.required -def list_mine(): - token = request.args.get('page_token', None) - if token: - token = token.encode('utf-8') - - books, next_page_token = get_model().list_by_user( - user_id=session['profile']['email'], - cursor=token) - - return render_template( - "list.html", - books=books, - next_page_token=next_page_token) - - -@crud.route('/') -def view(id): - book = get_model().read(id) - return render_template("view.html", book=book) - - -@crud.route('/add', methods=['GET', 'POST']) -def add(): - if request.method == 'POST': - data = request.form.to_dict(flat=True) - - # If an image was uploaded, update the data to point to the new image. - image_url = upload_image_file(request.files.get('image')) - - if image_url: - data['imageUrl'] = image_url - - # If the user is logged in, associate their profile with the new book. - if 'profile' in session: - data['createdBy'] = session['profile']['name'] - data['createdById'] = session['profile']['email'] - - book = get_model().create(data) - - # [START enqueue] - q = tasks.get_books_queue() - q.enqueue(tasks.process_book, book['id']) - # [END enqueue] - - return redirect(url_for('.view', id=book['id'])) - - return render_template("form.html", action="Add", book={}) - - -@crud.route('//edit', methods=['GET', 'POST']) -def edit(id): - book = get_model().read(id) - - if request.method == 'POST': - data = request.form.to_dict(flat=True) - - image_url = upload_image_file(request.files.get('image')) - - if image_url: - data['imageUrl'] = image_url - - book = get_model().update(data, id) - - q = tasks.get_books_queue() - q.enqueue(tasks.process_book, book['id']) - - return redirect(url_for('.view', id=book['id'])) - - return render_template("form.html", action="Edit", book=book) - - -@crud.route('//delete') -def delete(id): - get_model().delete(id) - return redirect(url_for('.list')) diff --git a/6-pubsub/bookshelf/model_cloudsql.py b/6-pubsub/bookshelf/model_cloudsql.py deleted file mode 100644 index d84207d2..00000000 --- a/6-pubsub/bookshelf/model_cloudsql.py +++ /dev/null @@ -1,119 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from flask import Flask -from flask_sqlalchemy import SQLAlchemy - - -builtin_list = list - - -db = SQLAlchemy() - - -def init_app(app): - # Disable track modifications, as it unnecessarily uses memory. - app.config.setdefault('SQLALCHEMY_TRACK_MODIFICATIONS', False) - db.init_app(app) - - -def from_sql(row): - """Translates a SQLAlchemy model instance into a dictionary""" - data = row.__dict__.copy() - data['id'] = row.id - data.pop('_sa_instance_state') - return data - - -class Book(db.Model): - __tablename__ = 'books' - - id = db.Column(db.Integer, primary_key=True) - title = db.Column(db.String(255)) - author = db.Column(db.String(255)) - publishedDate = db.Column(db.String(255)) - imageUrl = db.Column(db.String(255)) - description = db.Column(db.String(4096)) - createdBy = db.Column(db.String(255)) - createdById = db.Column(db.String(255)) - - def __repr__(self): - return " - - - Bookshelf - Python on Google Cloud Platform - - - - - - -
- {% block content %}{% endblock %} -
- {{user}} - - diff --git a/6-pubsub/bookshelf/templates/form.html b/6-pubsub/bookshelf/templates/form.html deleted file mode 100644 index 2de32a29..00000000 --- a/6-pubsub/bookshelf/templates/form.html +++ /dev/null @@ -1,67 +0,0 @@ -{# -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -#} - -{% extends "base.html" %} - -{% block content %} -

{{action}} book

- -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- - - - - - - - -
- -{% endblock %} diff --git a/6-pubsub/bookshelf/templates/list.html b/6-pubsub/bookshelf/templates/list.html deleted file mode 100644 index 3362f0e2..00000000 --- a/6-pubsub/bookshelf/templates/list.html +++ /dev/null @@ -1,55 +0,0 @@ -{# -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -#} - -{% extends "base.html" %} - -{% block content %} - -

Books

- - - Add book - - -{% for book in books %} - -{% else %} -

No books found

-{% endfor %} - -{% if next_page_token %} - -{% endif %} - -{% endblock %} diff --git a/6-pubsub/bookshelf/templates/view.html b/6-pubsub/bookshelf/templates/view.html deleted file mode 100644 index e654e8ab..00000000 --- a/6-pubsub/bookshelf/templates/view.html +++ /dev/null @@ -1,53 +0,0 @@ -{# -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -#} - -{% extends "base.html" %} - -{% block content %} - -

Book

- - - -
-
- {% if book.imageUrl %} - - {% else %} - - {% endif %} -
-
-

- {{book.title}} - {{book.publishedDate}} -

-
By {{book.author|default('Unknown', True)}}
-

{{book.description}}

- Added by {{book.createdBy|default('Anonymous', True)}} -
-
- -{% endblock %} diff --git a/6-pubsub/config.py b/6-pubsub/config.py deleted file mode 100644 index d40ef9bc..00000000 --- a/6-pubsub/config.py +++ /dev/null @@ -1,113 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -This file contains all of the configuration values for the application. -Update this file with the values for your specific Google Cloud project. -You can create and manage projects at https://console.developers.google.com -""" - -import os - -# The secret key is used by Flask to encrypt session cookies. -SECRET_KEY = 'secret' - -# There are three different ways to store the data in the application. -# You can choose 'datastore', 'cloudsql', or 'mongodb'. Be sure to -# configure the respective settings for the one you choose below. -# You do not have to configure the other data backends. If unsure, choose -# 'datastore' as it does not require any additional configuration. -DATA_BACKEND = 'datastore' - -# Google Cloud Project ID. This can be found on the 'Overview' page at -# https://console.developers.google.com -PROJECT_ID = 'your-project-id' - -# Cloud Datastore dataset id, this is the same as your project id. -DATASTORE_DATASET_ID = PROJECT_ID - -# CloudSQL & SQLAlchemy configuration -# Replace the following values the respective values of your Cloud SQL -# instance. -CLOUDSQL_USER = 'root' -CLOUDSQL_PASSWORD = 'your-cloudsql-password' -CLOUDSQL_DATABASE = 'bookshelf' -# Set this value to the Cloud SQL connection name, e.g. -# "project:region:cloudsql-instance". -# You must also update the value in app.yaml. -CLOUDSQL_CONNECTION_NAME = 'your-cloudsql-connection-name' - -# The CloudSQL proxy is used locally to connect to the cloudsql instance. -# To start the proxy, use: -# -# $ cloud_sql_proxy -instances=your-connection-name=tcp:3306 -# -# Port 3306 is the standard MySQL port. If you need to use a different port, -# change the 3306 to a different port number. - -# Alternatively, you could use a local MySQL instance for testing. -LOCAL_SQLALCHEMY_DATABASE_URI = ( - 'mysql+pymysql://{user}:{password}@127.0.0.1:3306/{database}').format( - user=CLOUDSQL_USER, password=CLOUDSQL_PASSWORD, - database=CLOUDSQL_DATABASE) - -# When running on App Engine a unix socket is used to connect to the cloudsql -# instance. -LIVE_SQLALCHEMY_DATABASE_URI = ( - 'mysql+pymysql://{user}:{password}@localhost/{database}' - '?unix_socket=/cloudsql/{connection_name}').format( - user=CLOUDSQL_USER, password=CLOUDSQL_PASSWORD, - database=CLOUDSQL_DATABASE, connection_name=CLOUDSQL_CONNECTION_NAME) - -if os.environ.get('GAE_INSTANCE'): - SQLALCHEMY_DATABASE_URI = LIVE_SQLALCHEMY_DATABASE_URI -else: - SQLALCHEMY_DATABASE_URI = LOCAL_SQLALCHEMY_DATABASE_URI - -# Mongo configuration -# If using mongolab, the connection URI is available from the mongolab control -# panel. If self-hosting on compute engine, replace the values below. -MONGO_URI = 'mongodb://user:password@host:27017/database' - -# Google Cloud Storage and upload settings. -# Typically, you'll name your bucket the same as your project. To create a -# bucket: -# -# $ gsutil mb gs:// -# -# You also need to make sure that the default ACL is set to public-read, -# otherwise users will not be able to see their upload images: -# -# $ gsutil defacl set public-read gs:// -# -# You can adjust the max content length and allow extensions settings to allow -# larger or more varied file types if desired. -CLOUD_STORAGE_BUCKET = 'your-bucket-name' -MAX_CONTENT_LENGTH = 8 * 1024 * 1024 -ALLOWED_EXTENSIONS = set(['png', 'jpg', 'jpeg', 'gif']) - -# OAuth2 configuration. -# This can be generated from the Google Developers Console at -# https://console.developers.google.com/project/_/apiui/credential. -# Note that you will need to add all URLs that your application uses as -# authorized redirect URIs. For example, typically you would add the following: -# -# * http://localhost:8080/oauth2callback -# * https://.appspot.com/oauth2callback. -# -# If you receive a invalid redirect URI error review you settings to ensure -# that the current URI is allowed. -GOOGLE_OAUTH2_CLIENT_ID = \ - 'your-client-id' -GOOGLE_OAUTH2_CLIENT_SECRET = 'your-client-secret' diff --git a/6-pubsub/main.py b/6-pubsub/main.py deleted file mode 100644 index 73d3a303..00000000 --- a/6-pubsub/main.py +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import bookshelf -import config - - -app = bookshelf.create_app(config) - - -# [START books_queue] -# Make the queue available at the top-level, this allows you to run -# `psqworker main.books_queue`. We have to use the app's context because -# it contains all the configuration for plugins. -# If you were using another task queue, such as celery or rq, you can use this -# section to configure your queues to work with Flask. -with app.app_context(): - books_queue = bookshelf.tasks.get_books_queue() -# [END books_queue] - - -# This is only used when running locally. When running live, gunicorn runs -# the application. -if __name__ == '__main__': - app.run(host='127.0.0.1', port=8080, debug=True) diff --git a/6-pubsub/monitor.py b/6-pubsub/monitor.py deleted file mode 100644 index 24c4fe63..00000000 --- a/6-pubsub/monitor.py +++ /dev/null @@ -1,56 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# [START monitor] -import os -import sys - -from flask import Flask - - -# The app checks this file for the PID of the process to monitor. -PID_FILE = None - - -# Create app to handle health checks and monitor the queue worker. This will -# run alongside the worker, see procfile. -monitor_app = Flask(__name__) - - -# The health check reads the PID file created by psqworker and checks the proc -# filesystem to see if the worker is running. This same pattern can be used for -# rq and celery. -@monitor_app.route('/_ah/health') -def health(): - if not os.path.exists(PID_FILE): - return 'Worker pid not found', 503 - - with open(PID_FILE, 'r') as pidfile: - pid = pidfile.read() - - if not os.path.exists('/proc/{}'.format(pid)): - return 'Worker not running', 503 - - return 'healthy', 200 - - -@monitor_app.route('/') -def index(): - return health() - - -if __name__ == '__main__': - PID_FILE = sys.argv[1] - monitor_app.run('0.0.0.0', 8080) -# [END monitor] diff --git a/6-pubsub/procfile b/6-pubsub/procfile deleted file mode 100644 index 0c4fcec5..00000000 --- a/6-pubsub/procfile +++ /dev/null @@ -1,3 +0,0 @@ -bookshelf: gunicorn -b 0.0.0.0:$PORT main:app -worker: psqworker --pid /tmp/psq.pid main.books_queue -monitor: python monitor.py /tmp/psq.pid diff --git a/6-pubsub/requirements-dev.txt b/6-pubsub/requirements-dev.txt deleted file mode 100644 index 7e4b9ace..00000000 --- a/6-pubsub/requirements-dev.txt +++ /dev/null @@ -1,9 +0,0 @@ -tox==3.5.3 -flake8==3.6.0 -flaky==3.4.0 -mock==2.0.0 -pytest==4.0.1 -pytest-cov==2.6.0 -BeautifulSoup4==4.6.3 -requests==2.20.1 -retrying==1.3.3 diff --git a/6-pubsub/requirements.txt b/6-pubsub/requirements.txt deleted file mode 100644 index 9e6e136a..00000000 --- a/6-pubsub/requirements.txt +++ /dev/null @@ -1,16 +0,0 @@ -Flask>=1.0.0 -google-cloud-datastore==1.7.1 -google-cloud-storage==1.13.0 -google-cloud-pubsub==0.39.1 -google-cloud-logging==1.8.0 -google-cloud-error_reporting==0.30.0 -gunicorn==19.9.0 -oauth2client==4.1.3 -Flask-SQLAlchemy==2.3.2 -PyMySQL==0.9.2 -Flask-PyMongo>=2.0.0 -PyMongo==3.7.2 -six==1.11.0 -requests[security]==2.20.1 -honcho==1.0.1 -psq==0.7.0 diff --git a/6-pubsub/tests/conftest.py b/6-pubsub/tests/conftest.py deleted file mode 100644 index 62c2a480..00000000 --- a/6-pubsub/tests/conftest.py +++ /dev/null @@ -1,91 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""conftest.py is used to define common test fixtures for pytest.""" - -import bookshelf -import config -from google.cloud.exceptions import ServiceUnavailable -from oauth2client.client import HttpAccessTokenRefreshError -import pytest -from retrying import retry - - -#@pytest.yield_fixture(params=['datastore', 'cloudsql', 'mongodb']) -@pytest.yield_fixture(params=['datastore', 'mongodb']) -def app(request): - """This fixtures provides a Flask app instance configured for testing. - - Because it's parametric, it will cause every test that uses this fixture - to run three times: one time for each backend (datastore, cloudsql, and - mongodb). - - It also ensures the tests run within a request context, allowing - any calls to flask.request, flask.current_app, etc. to work.""" - app = bookshelf.create_app( - config, - testing=True, - config_overrides={ - 'DATA_BACKEND': request.param - }) - - with app.test_request_context(): - yield app - - -@pytest.yield_fixture -def model(monkeypatch, app): - """This fixture provides a modified version of the app's model that tracks - all created items and deletes them at the end of the test. - - Any tests that directly or indirectly interact with the database should use - this to ensure that resources are properly cleaned up. - - Monkeypatch is provided by pytest and used to patch the model's create - method. - - The app fixture is needed to provide the configuration and context needed - to get the proper model object. - """ - model = bookshelf.get_model() - - # Ensure no books exist before running. This typically helps if tests - # somehow left the database in a bad state. - delete_all_books(model) - - yield model - - # Delete all books that we created during tests. - delete_all_books(model) - - -# The backend data stores can sometimes be flaky. It's useful to retry this -# a few times before giving up. -@retry( - stop_max_attempt_number=3, - wait_exponential_multiplier=100, - wait_exponential_max=2000) -def delete_all_books(model): - while True: - books, _ = model.list(limit=50) - if not books: - break - for book in books: - model.delete(book['id']) - - -def flaky_filter(info, *args): - """Used by flaky to determine when to re-run a test case.""" - _, e, _ = info - return isinstance(e, (ServiceUnavailable, HttpAccessTokenRefreshError)) diff --git a/6-pubsub/tests/test_auth.py b/6-pubsub/tests/test_auth.py deleted file mode 100644 index 491de9e0..00000000 --- a/6-pubsub/tests/test_auth.py +++ /dev/null @@ -1,133 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import contextlib - -import bookshelf -from conftest import flaky_filter -from flaky import flaky -import mock -from oauth2client.client import OAuth2Credentials -import pytest - - -@pytest.fixture -def client_with_credentials(app): - """This fixture provides a Flask app test client that has a session - pre-configured with use credentials.""" - credentials = OAuth2Credentials( - 'access_token', - 'client_id', - 'client_secret', - 'refresh_token', - '3600', - None, - 'Test', - id_token={'sub': '123', 'email': 'user@example.com'}, - scopes=('email', 'profile')) - - @contextlib.contextmanager - def inner(): - with app.test_client() as client: - with client.session_transaction() as session: - session['profile'] = {'email': 'abc@example.com', 'name': 'Test User'} - session['google_oauth2_credentials'] = credentials.to_json() - yield client - - return inner - - -# Mark all test cases in this class as flaky, so that if errors occur they -# can be retried. This is useful when databases are temporarily unavailable. -@flaky(rerun_filter=flaky_filter) -# Tell pytest to use both the app and model fixtures for all test cases. -# This ensures that configuration is properly applied and that all database -# resources created during tests are cleaned up. These fixtures are defined -# in conftest.py -@pytest.mark.usefixtures('app', 'model') -class TestAuth(object): - def test_not_logged_in(self, app): - with app.test_client() as c: - rv = c.get('/books/') - - assert rv.status == '200 OK' - body = rv.data.decode('utf-8') - assert 'Login' in body - - def test_logged_in(self, client_with_credentials): - with client_with_credentials() as c: - rv = c.get('/books/') - - assert rv.status == '200 OK' - body = rv.data.decode('utf-8') - assert 'Test User' in body - - def test_add_anonymous(self, app): - data = { - 'title': 'Test Book', - } - - with app.test_client() as c: - rv = c.post('/books/add', data=data, follow_redirects=True) - - assert rv.status == '200 OK' - body = rv.data.decode('utf-8') - assert 'Test Book' in body - assert 'Added by Anonymous' in body - - def test_add_logged_in(self, client_with_credentials): - data = { - 'title': 'Test Book', - } - - with client_with_credentials() as c: - rv = c.post('/books/add', data=data, follow_redirects=True) - - assert rv.status == '200 OK' - body = rv.data.decode('utf-8') - assert 'Test Book' in body - assert 'Added by Test User' in body - - def test_mine(self, model, client_with_credentials): - # Create two books, one created by the logged in user and one - # created by another user. - model.create({ - 'title': 'Book 1', - 'createdById': 'abc@example.com' - }) - - model.create({ - 'title': 'Book 2', - 'createdById': 'def@example.com' - }) - - # Check the "My Books" page and make sure only one of the books - # appears. - with client_with_credentials() as c: - rv = c.get('/books/mine') - - assert rv.status == '200 OK' - body = rv.data.decode('utf-8') - assert 'Book 1' in body - assert 'Book 2' not in body - - @mock.patch("httplib2.Http") - def test_request_user_info(self, HttpMock): - httpObj = mock.MagicMock() - responseMock = mock.MagicMock(status=200) - httpObj.request = mock.MagicMock( - return_value=(responseMock, b'{"name": "bill"}')) - HttpMock.return_value = httpObj - credentials = mock.MagicMock() - bookshelf._request_user_info(credentials) diff --git a/6-pubsub/tests/test_crud.py b/6-pubsub/tests/test_crud.py deleted file mode 100644 index c0d2f40f..00000000 --- a/6-pubsub/tests/test_crud.py +++ /dev/null @@ -1,88 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import re - -from conftest import flaky_filter -from flaky import flaky -import pytest - - -# Mark all test cases in this class as flaky, so that if errors occur they -# can be retried. This is useful when databases are temporarily unavailable. -@flaky(rerun_filter=flaky_filter) -# Tell pytest to use both the app and model fixtures for all test cases. -# This ensures that configuration is properly applied and that all database -# resources created during tests are cleaned up. These fixtures are defined -# in conftest.py -@pytest.mark.usefixtures('app', 'model') -class TestCrudActions(object): - - def test_list(self, app, model): - for i in range(1, 12): - model.create({'title': u'Book {0}'.format(i)}) - - with app.test_client() as c: - rv = c.get('/books/') - - assert rv.status == '200 OK' - - body = rv.data.decode('utf-8') - assert 'Book 1' in body, "Should show books" - assert len(re.findall('

Book', body)) == 10, ( - "Should not show more than 10 books") - assert 'More' in body, "Should have more than one page" - - def test_add(self, app): - data = { - 'title': 'Test Book', - 'author': 'Test Author', - 'publishedDate': 'Test Date Published', - 'description': 'Test Description' - } - - with app.test_client() as c: - rv = c.post('/books/add', data=data, follow_redirects=True) - - assert rv.status == '200 OK' - body = rv.data.decode('utf-8') - assert 'Test Book' in body - assert 'Test Author' in body - assert 'Test Date Published' in body - assert 'Test Description' in body - - def test_edit(self, app, model): - existing = model.create({'title': "Temp Title"}) - - with app.test_client() as c: - rv = c.post( - '/books/%s/edit' % existing['id'], - data={'title': 'Updated Title'}, - follow_redirects=True) - - assert rv.status == '200 OK' - body = rv.data.decode('utf-8') - assert 'Updated Title' in body - assert 'Temp Title' not in body - - def test_delete(self, app, model): - existing = model.create({'title': "Temp Title"}) - - with app.test_client() as c: - rv = c.get( - '/books/%s/delete' % existing['id'], - follow_redirects=True) - - assert rv.status == '200 OK' - assert not model.read(existing['id']) diff --git a/6-pubsub/tests/test_end_to_end.py b/6-pubsub/tests/test_end_to_end.py deleted file mode 100644 index df80e8a0..00000000 --- a/6-pubsub/tests/test_end_to_end.py +++ /dev/null @@ -1,80 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -import re - -from bs4 import BeautifulSoup -import pytest -import requests -from retrying import retry - - -@pytest.mark.e2e -def test_end_to_end(): - """Tests designed to be run against live environments. - - Unlike the integration tests in the other packages, these tests are - designed to be run against fully-functional live environments. - - To run locally, start both main.py and psq_worker main.books_queue and - run this file. - - It can be run against a live environment by setting the E2E_URL - environment variables before running the tests: - - E2E_URL=http://your-app-id.appspot.com \ - nosetests tests/test_end_to_end.py - """ - - base_url = os.environ.get('E2E_URL', 'http://localhost:8080') - - book_data = { - 'title': 'a confederacy of dunces', - } - - response = requests.post(base_url + '/books/add', data=book_data) - - # There was a 302, so get the book's URL from the redirect. - book_url = response.request.url - book_id = book_url.rsplit('/', 1).pop() - - # Use retry because it will take some indeterminate time for the pub/sub - # message to be processed. - @retry(wait_exponential_multiplier=5000, stop_max_attempt_number=12) - def check_for_updated_data(): - # Check that the book's information was updated. - response = requests.get(book_url) - assert response.status_code == 200 - - soup = BeautifulSoup(response.text, 'html.parser') - - title = soup.find('h4', 'book-title').contents[0].strip() - assert re.search(r'A Confederacy of Dunces', title, re.I) - - author = soup.find('h5', 'book-author').string - assert re.search(r'John Kennedy Toole', author, re.I) - - description = soup.find('p', 'book-description').string - assert re.search(r'Ignatius', description, re.I) - - image_src = soup.find('img', 'book-image')['src'] - image = requests.get(image_src) - assert image.status_code == 200 - - try: - check_for_updated_data() - finally: - # Delete the book we created. - requests.get(base_url + '/books/{}/delete'.format(book_id)) diff --git a/6-pubsub/tests/test_storage.py b/6-pubsub/tests/test_storage.py deleted file mode 100644 index 531c8827..00000000 --- a/6-pubsub/tests/test_storage.py +++ /dev/null @@ -1,70 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import re - -from conftest import flaky_filter -from flaky import flaky -import httplib2 -import pytest -from six import BytesIO - - -# Mark all test cases in this class as flaky, so that if errors occur they -# can be retried. This is useful when databases are temporarily unavailable. -@flaky(rerun_filter=flaky_filter) -# Tell pytest to use both the app and model fixtures for all test cases. -# This ensures that configuration is properly applied and that all database -# resources created during tests are cleaned up. These fixtures are defined -# in conftest.py -@pytest.mark.usefixtures('app', 'model') -class TestStorage(object): - - def test_upload_image(self, app): - data = { - 'title': 'Test Book', - 'author': 'Test Author', - 'publishedDate': 'Test Date Published', - 'description': 'Test Description', - 'image': (BytesIO(b'hello world'), 'hello.jpg') - } - - with app.test_client() as c: - rv = c.post('/books/add', data=data, follow_redirects=True) - - assert rv.status == '200 OK' - body = rv.data.decode('utf-8') - - img_tag = re.search(''), - '1337h4x0r.php') - } - - with app.test_client() as c: - rv = c.post('/books/add', data=data, follow_redirects=True) - - # check we weren't pwned - assert rv.status == '400 BAD REQUEST' diff --git a/6-pubsub/tox.ini b/6-pubsub/tox.ini deleted file mode 100644 index 8a04f077..00000000 --- a/6-pubsub/tox.ini +++ /dev/null @@ -1,30 +0,0 @@ -[tox] -skipsdist = True -envlist = lint,py27,py36 - -[testenv] -deps = - -rrequirements.txt - -rrequirements-dev.txt -commands = - py.test --cov=bookshelf --no-success-flaky-report -m "not e2e" {posargs: tests} -passenv = GOOGLE_APPLICATION_CREDENTIALS DATASTORE_HOST E2E_URL -setenv = PYTHONPATH={toxinidir} - - -[testenv:py27-e2e] -basepython = python2.7 -commands = - py.test --no-success-flaky-report -m "e2e" {posargs: tests} - -[testenv:py36-e2e] -basepython = python3.6 -commands = - py.test --no-success-flaky-report -m "e2e" {posargs: tests} - -[testenv:lint] -deps = - flake8 - flake8-import-order -commands = - flake8 --import-order-style=google bookshelf tests diff --git a/6-pubsub/worker.yaml b/6-pubsub/worker.yaml deleted file mode 100644 index 01ac8a4c..00000000 --- a/6-pubsub/worker.yaml +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# This file specifies your Python application's runtime configuration. -# See https://cloud.google.com/appengine/docs/managed-vms/python/runtime -# for details. - -# [START worker] -service: worker -runtime: python -env: flex - -# Instead of using gunicorn directly, we'll use Honcho. Honcho is a python port -# of the Foreman process manager. For the worker service, both the queue worker -# and the monitor process are needed. -entrypoint: honcho start -f /app/procfile worker monitor - -runtime_config: - python_version: 3 - -beta_settings: - # If using Cloud SQL, uncomment and set this value to the Cloud SQL - # connection name, e.g. - # "project:region:cloudsql-instance" - # You must also update the values in config.py. - # - # cloud_sql_instances: "your-cloudsql-connection-name" -# [END worker] - diff --git a/7-gce/bookshelf/__init__.py b/7-gce/bookshelf/__init__.py deleted file mode 100644 index 6cfda7fe..00000000 --- a/7-gce/bookshelf/__init__.py +++ /dev/null @@ -1,131 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import json -import logging - -from flask import current_app, Flask, redirect, request, session, url_for -from google.cloud import error_reporting -import google.cloud.logging -import httplib2 -from oauth2client.contrib.flask_util import UserOAuth2 - - -oauth2 = UserOAuth2() - - -def create_app(config, debug=False, testing=False, config_overrides=None): - app = Flask(__name__) - app.config.from_object(config) - - app.debug = debug - app.testing = testing - - if config_overrides: - app.config.update(config_overrides) - - # Configure logging - if not app.testing: - client = google.cloud.logging.Client(app.config['PROJECT_ID']) - # Attaches a Google Stackdriver logging handler to the root logger - client.setup_logging(logging.INFO) - - # Setup the data model. - with app.app_context(): - model = get_model() - model.init_app(app) - - # Create a health check handler. Health checks are used when running on - # Google Compute Engine by the load balancer to determine which instances - # can serve traffic. Google App Engine also uses health checking, but - # accepts any non-500 response as healthy. - @app.route('/_ah/health') - def health_check(): - return 'ok', 200 - - # Initalize the OAuth2 helper. - oauth2.init_app( - app, - scopes=['email', 'profile'], - authorize_callback=_request_user_info) - - # Add a logout handler. - @app.route('/logout') - def logout(): - # Delete the user's profile and the credentials stored by oauth2. - del session['profile'] - session.modified = True - oauth2.storage.delete() - return redirect(request.referrer or '/') - - # Register the Bookshelf CRUD blueprint. - from .crud import crud - app.register_blueprint(crud, url_prefix='/books') - - # Add a default root route. - @app.route("/") - def index(): - return redirect(url_for('crud.list')) - - # Add an error handler that reports exceptions to Stackdriver Error - # Reporting. Note that this error handler is only used when Debug - # is False - @app.errorhandler(500) - def server_error(e): - client = error_reporting.Client(app.config['PROJECT_ID']) - client.report_exception( - http_context=error_reporting.build_flask_context(request)) - return """ - An internal error occurred. - """, 500 - - return app - - -def get_model(): - model_backend = current_app.config['DATA_BACKEND'] - if model_backend == 'cloudsql': - from . import model_cloudsql - model = model_cloudsql - elif model_backend == 'datastore': - from . import model_datastore - model = model_datastore - elif model_backend == 'mongodb': - from . import model_mongodb - model = model_mongodb - else: - raise ValueError( - "No appropriate databackend configured. " - "Please specify datastore, cloudsql, or mongodb") - - return model - - -def _request_user_info(credentials): - """ - Makes an HTTP request to the Google OAuth2 API to retrieve the user's basic - profile information, including full name and photo, and stores it in the - Flask session. - """ - http = httplib2.Http() - credentials.authorize(http) - resp, content = http.request( - 'https://www.googleapis.com/oauth2/v3/userinfo') - - if resp.status != 200: - current_app.logger.error( - "Error while obtaining user profile: \n%s: %s", resp, content) - return None - - session['profile'] = json.loads(content.decode('utf-8')) diff --git a/7-gce/bookshelf/crud.py b/7-gce/bookshelf/crud.py deleted file mode 100644 index b85e5d6a..00000000 --- a/7-gce/bookshelf/crud.py +++ /dev/null @@ -1,131 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from bookshelf import get_model, oauth2, storage, tasks -from flask import Blueprint, current_app, redirect, render_template, request, \ - session, url_for - - -crud = Blueprint('crud', __name__) - - -def upload_image_file(file): - """ - Upload the user-uploaded file to Google Cloud Storage and retrieve its - publicly-accessible URL. - """ - if not file: - return None - - public_url = storage.upload_file( - file.read(), - file.filename, - file.content_type - ) - - current_app.logger.info( - "Uploaded file %s as %s.", file.filename, public_url) - - return public_url - - -@crud.route("/") -def list(): - token = request.args.get('page_token', None) - if token: - token = token.encode('utf-8') - - books, next_page_token = get_model().list(cursor=token) - - return render_template( - "list.html", - books=books, - next_page_token=next_page_token) - - -@crud.route("/mine") -@oauth2.required -def list_mine(): - token = request.args.get('page_token', None) - if token: - token = token.encode('utf-8') - - books, next_page_token = get_model().list_by_user( - user_id=session['profile']['email'], - cursor=token) - - return render_template( - "list.html", - books=books, - next_page_token=next_page_token) - - -@crud.route('/') -def view(id): - book = get_model().read(id) - return render_template("view.html", book=book) - - -@crud.route('/add', methods=['GET', 'POST']) -def add(): - if request.method == 'POST': - data = request.form.to_dict(flat=True) - - # If an image was uploaded, update the data to point to the new image. - image_url = upload_image_file(request.files.get('image')) - - if image_url: - data['imageUrl'] = image_url - - # If the user is logged in, associate their profile with the new book. - if 'profile' in session: - data['createdBy'] = session['profile']['name'] - data['createdById'] = session['profile']['email'] - - book = get_model().create(data) - - q = tasks.get_books_queue() - q.enqueue(tasks.process_book, book['id']) - - return redirect(url_for('.view', id=book['id'])) - - return render_template("form.html", action="Add", book={}) - - -@crud.route('//edit', methods=['GET', 'POST']) -def edit(id): - book = get_model().read(id) - - if request.method == 'POST': - data = request.form.to_dict(flat=True) - - image_url = upload_image_file(request.files.get('image')) - - if image_url: - data['imageUrl'] = image_url - - book = get_model().update(data, id) - - q = tasks.get_books_queue() - q.enqueue(tasks.process_book, book['id']) - - return redirect(url_for('.view', id=book['id'])) - - return render_template("form.html", action="Edit", book=book) - - -@crud.route('//delete') -def delete(id): - get_model().delete(id) - return redirect(url_for('.list')) diff --git a/7-gce/bookshelf/model_cloudsql.py b/7-gce/bookshelf/model_cloudsql.py deleted file mode 100644 index d84207d2..00000000 --- a/7-gce/bookshelf/model_cloudsql.py +++ /dev/null @@ -1,119 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from flask import Flask -from flask_sqlalchemy import SQLAlchemy - - -builtin_list = list - - -db = SQLAlchemy() - - -def init_app(app): - # Disable track modifications, as it unnecessarily uses memory. - app.config.setdefault('SQLALCHEMY_TRACK_MODIFICATIONS', False) - db.init_app(app) - - -def from_sql(row): - """Translates a SQLAlchemy model instance into a dictionary""" - data = row.__dict__.copy() - data['id'] = row.id - data.pop('_sa_instance_state') - return data - - -class Book(db.Model): - __tablename__ = 'books' - - id = db.Column(db.Integer, primary_key=True) - title = db.Column(db.String(255)) - author = db.Column(db.String(255)) - publishedDate = db.Column(db.String(255)) - imageUrl = db.Column(db.String(255)) - description = db.Column(db.String(4096)) - createdBy = db.Column(db.String(255)) - createdById = db.Column(db.String(255)) - - def __repr__(self): - return " - - - Bookshelf - Python on Google Cloud Platform - - - - - - -
- {% block content %}{% endblock %} -
- {{user}} - - diff --git a/7-gce/bookshelf/templates/form.html b/7-gce/bookshelf/templates/form.html deleted file mode 100644 index 2de32a29..00000000 --- a/7-gce/bookshelf/templates/form.html +++ /dev/null @@ -1,67 +0,0 @@ -{# -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -#} - -{% extends "base.html" %} - -{% block content %} -

{{action}} book

- -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- - - - - - - - -
- -{% endblock %} diff --git a/7-gce/bookshelf/templates/list.html b/7-gce/bookshelf/templates/list.html deleted file mode 100644 index 3362f0e2..00000000 --- a/7-gce/bookshelf/templates/list.html +++ /dev/null @@ -1,55 +0,0 @@ -{# -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -#} - -{% extends "base.html" %} - -{% block content %} - -

Books

- - - Add book - - -{% for book in books %} - -{% else %} -

No books found

-{% endfor %} - -{% if next_page_token %} - -{% endif %} - -{% endblock %} diff --git a/7-gce/bookshelf/templates/view.html b/7-gce/bookshelf/templates/view.html deleted file mode 100644 index e654e8ab..00000000 --- a/7-gce/bookshelf/templates/view.html +++ /dev/null @@ -1,53 +0,0 @@ -{# -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -#} - -{% extends "base.html" %} - -{% block content %} - -

Book

- - - -
-
- {% if book.imageUrl %} - - {% else %} - - {% endif %} -
-
-

- {{book.title}} - {{book.publishedDate}} -

-
By {{book.author|default('Unknown', True)}}
-

{{book.description}}

- Added by {{book.createdBy|default('Anonymous', True)}} -
-
- -{% endblock %} diff --git a/7-gce/config.py b/7-gce/config.py deleted file mode 100644 index b64eecd2..00000000 --- a/7-gce/config.py +++ /dev/null @@ -1,110 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -This file contains all of the configuration values for the application. -Update this file with the values for your specific Google Cloud project. -You can create and manage projects at https://console.developers.google.com -""" - -import os - -# The secret key is used by Flask to encrypt session cookies. -SECRET_KEY = 'secret' - -# There are three different ways to store the data in the application. -# You can choose 'datastore', 'cloudsql', or 'mongodb'. Be sure to -# configure the respective settings for the one you choose below. -# You do not have to configure the other data backends. If unsure, choose -# 'datastore' as it does not require any additional configuration. -DATA_BACKEND = 'datastore' - -# Google Cloud Project ID. This can be found on the 'Overview' page at -# https://console.developers.google.com -PROJECT_ID = 'your-project-id' - -# CloudSQL & SQLAlchemy configuration -# Replace the following values the respective values of your Cloud SQL -# instance. -CLOUDSQL_USER = 'root' -CLOUDSQL_PASSWORD = 'your-cloudsql-password' -CLOUDSQL_DATABASE = 'bookshelf' -# Set this value to the Cloud SQL connection name, e.g. -# "project:region:cloudsql-instance". -# You must also update the value in app.yaml. -CLOUDSQL_CONNECTION_NAME = 'your-cloudsql-connection-name' - -# The CloudSQL proxy is used locally to connect to the cloudsql instance. -# To start the proxy, use: -# -# $ cloud_sql_proxy -instances=your-connection-name=tcp:3306 -# -# Port 3306 is the standard MySQL port. If you need to use a different port, -# change the 3306 to a different port number. - -# Alternatively, you could use a local MySQL instance for testing. -LOCAL_SQLALCHEMY_DATABASE_URI = ( - 'mysql+pymysql://{user}:{password}@127.0.0.1:3306/{database}').format( - user=CLOUDSQL_USER, password=CLOUDSQL_PASSWORD, - database=CLOUDSQL_DATABASE) - -# When running on App Engine a unix socket is used to connect to the cloudsql -# instance. -LIVE_SQLALCHEMY_DATABASE_URI = ( - 'mysql+pymysql://{user}:{password}@localhost/{database}' - '?unix_socket=/cloudsql/{connection_name}').format( - user=CLOUDSQL_USER, password=CLOUDSQL_PASSWORD, - database=CLOUDSQL_DATABASE, connection_name=CLOUDSQL_CONNECTION_NAME) - -if os.environ.get('GAE_INSTANCE'): - SQLALCHEMY_DATABASE_URI = LIVE_SQLALCHEMY_DATABASE_URI -else: - SQLALCHEMY_DATABASE_URI = LOCAL_SQLALCHEMY_DATABASE_URI - -# Mongo configuration -# If using mongolab, the connection URI is available from the mongolab control -# panel. If self-hosting on compute engine, replace the values below. -MONGO_URI = 'mongodb://user:password@host:27017/database' - -# Google Cloud Storage and upload settings. -# Typically, you'll name your bucket the same as your project. To create a -# bucket: -# -# $ gsutil mb gs:// -# -# You also need to make sure that the default ACL is set to public-read, -# otherwise users will not be able to see their upload images: -# -# $ gsutil defacl set public-read gs:// -# -# You can adjust the max content length and allow extensions settings to allow -# larger or more varied file types if desired. -CLOUD_STORAGE_BUCKET = 'your-bucket-name' -MAX_CONTENT_LENGTH = 8 * 1024 * 1024 -ALLOWED_EXTENSIONS = set(['png', 'jpg', 'jpeg', 'gif']) - -# OAuth2 configuration. -# This can be generated from the Google Developers Console at -# https://console.developers.google.com/project/_/apiui/credential. -# Note that you will need to add all URLs that your application uses as -# authorized redirect URIs. For example, typically you would add the following: -# -# * http://localhost:8080/oauth2callback -# * https://.appspot.com/oauth2callback. -# -# If you receive a invalid redirect URI error review you settings to ensure -# that the current URI is allowed. -GOOGLE_OAUTH2_CLIENT_ID = \ - 'your-client-id' -GOOGLE_OAUTH2_CLIENT_SECRET = 'your-client-secret' diff --git a/7-gce/gce/deploy.sh b/7-gce/gce/deploy.sh deleted file mode 100755 index 0d7482b4..00000000 --- a/7-gce/gce/deploy.sh +++ /dev/null @@ -1,159 +0,0 @@ -#! /bin/bash -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -set -ex - -ZONE=us-central1-f - -GROUP=frontend-group -TEMPLATE=$GROUP-tmpl -MACHINE_TYPE=f1-micro -IMAGE_FAMILY=debian-9 -IMAGE_PROJECT=debian-cloud -STARTUP_SCRIPT=startup-script.sh -SCOPES="userinfo-email,cloud-platform" -TAGS=http-server - -MIN_INSTANCES=1 -MAX_INSTANCES=10 -TARGET_UTILIZATION=0.6 - -SERVICE=my-app-service - -# -# Instance group setup -# - -# First we have to create an instance template. -# This template will be used by the instance group -# to create new instances. - -# [START create_template] -gcloud compute instance-templates create $TEMPLATE \ - --image-family $IMAGE_FAMILY \ - --image-project $IMAGE_PROJECT \ - --machine-type $MACHINE_TYPE \ - --scopes $SCOPES \ - --metadata-from-file startup-script=$STARTUP_SCRIPT \ - --tags $TAGS -# [END create_template] - -# Create the managed instance group. - -# [START create_group] -gcloud compute instance-groups managed \ - create $GROUP \ - --base-instance-name $GROUP \ - --size $MIN_INSTANCES \ - --template $TEMPLATE \ - --zone $ZONE -# [END create_group] - -# [START create_named_port] -gcloud compute instance-groups managed set-named-ports \ - $GROUP \ - --named-ports http:8080 \ - --zone $ZONE -# [END create_named_port] - -# -# Load Balancer Setup -# - -# A complete HTTP load balancer is structured as follows: -# -# 1) A global forwarding rule directs incoming requests to a target HTTP proxy. -# 2) The target HTTP proxy checks each request against a URL map to determine the -# appropriate backend service for the request. -# 3) The backend service directs each request to an appropriate backend based on -# serving capacity, zone, and instance health of its attached backends. The -# health of each backend instance is verified using either a health check. -# -# We'll create these resources in reverse order: -# service, health check, backend service, url map, proxy. - -# Create a health check -# The load balancer will use this check to keep track of which instances to send traffic to. -# Note that health checks will not cause the load balancer to shutdown any instances. - -# [START create_health_check] -gcloud compute http-health-checks create ah-health-check \ - --request-path /_ah/health \ - --port 8080 -# [END create_health_check] - -# Create a backend service, associate it with the health check and instance group. -# The backend service serves as a target for load balancing. - -# [START create_backend_service] -gcloud compute backend-services create $SERVICE \ - --http-health-checks ah-health-check \ - --global -# [END create_backend_service] - -# [START add_backend_service] -gcloud compute backend-services add-backend $SERVICE \ - --instance-group $GROUP \ - --instance-group-zone $ZONE \ - --global -# [END add_backend_service] - -# Create a URL map and web Proxy. The URL map will send all requests to the -# backend service defined above. - -# [START create_url_map] -gcloud compute url-maps create $SERVICE-map \ - --default-service $SERVICE -# [END create_url_map] - -# [START create_http_proxy] -gcloud compute target-http-proxies create $SERVICE-proxy \ - --url-map $SERVICE-map -# [END create_http_proxy] - -# Create a global forwarding rule to send all traffic to our proxy - -# [START create_forwarding_rule] -gcloud compute forwarding-rules create $SERVICE-http-rule \ - --global \ - --target-http-proxy $SERVICE-proxy \ - --ports=80 -# [END create_forwarding_rule] - -# -# Autoscaler configuration -# -# [START set_autoscaling] -gcloud compute instance-groups managed set-autoscaling \ - $GROUP \ - --max-num-replicas $MAX_INSTANCES \ - --target-load-balancing-utilization $TARGET_UTILIZATION \ - --zone $ZONE -# [END set_autoscaling] - -# [START create_firewall] -# Check if the firewall rule has been created in previous steps of the documentation -if gcloud compute firewall-rules list --filter="name~'default-allow-http-8080'" \ - --format="table(name)" | grep -q 'NAME'; then - echo "Firewall rule default-allow-http-8080 already exists." -else - gcloud compute firewall-rules create default-allow-http-8080 \ - --allow tcp:8080 \ - --source-ranges 0.0.0.0/0 \ - --target-tags http-server \ - --description "Allow port 8080 access to http-server" -fi - -# [END create_firewall] diff --git a/7-gce/gce/deployment_manager/bookshelf.jinja b/7-gce/gce/deployment_manager/bookshelf.jinja deleted file mode 100644 index 8a91dc0f..00000000 --- a/7-gce/gce/deployment_manager/bookshelf.jinja +++ /dev/null @@ -1,168 +0,0 @@ -{# -Copyright 2016 Google Inc. All rights reserved. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -#} - -{# [START all] #} - -{# [START env] #} -{% set NAME = "bookshelf-" + env["deployment"] %} -{% set SERVICE = "bookshelf-" + env["deployment"] + "-frontend" %} -{# [END env] #} - -# -# Instance group setup -# - -# First we have to create an instance template. -# This template will be used by the instance group -# to create new instances. -resources: -- name : {{ NAME }} - type: compute.v1.instanceTemplate - properties: - properties: - tags: - items: - - http-server - disks: - - boot: True - type: PERSISTENT - initializeParams: - sourceImage: {{ properties['machine-image'] }} - diskSizeGb: 10 - diskType: pd-ssd - machineType: {{ properties['machine-type'] }} - serviceAccounts: - - email: default - scopes: {{ properties['scopes'] }} - metadata: - items: - - key: startup-script -{# [START startup] #} - value: | -{{imports['startup-script']|indent(14, true)}} -{# [END startup] #} - networkInterfaces: - - network: global/networks/default - accessConfigs: - - type: ONE_TO_ONE_NAT - name: External NAT - -# Creates the managed instance group. This is responsible for creating -# new instances using the instance template, as well as providing a named -# port the backend service can target -- name: {{ NAME }}-frontend-group - type: compute.v1.instanceGroupManager - properties: - instanceTemplate: $(ref.{{ NAME }}.selfLink) - baseInstanceName: frontend-group - targetSize: 3 - zone: {{ properties['zone'] }} - namedPorts: - - name: http - port: 8080 - - - -# Load Balancer Setup -# - -# A complete HTTP load balancer is structured as follows: -# -# 1) A global forwarding rule directs incoming requests to a target HTTP proxy. -# 2) The target HTTP proxy checks each request against a URL map to determine the -# appropriate backend service for the request. -# 3) The backend service directs each request to an appropriate backend based on -# serving capacity, zone, and instance health of its attached backends. The -# health of each backend instance is verified using either a health check. -# -# We'll create these resources in reverse order: -# service, health check, backend service, url map, proxy. - -# Create a health check -# The load balancer will use this check to keep track of which instances to send traffic to. -# Note that health checks will not cause the load balancer to shutdown any instances. -- name: {{ NAME }}-health-check - type: compute.v1.httpHealthCheck - properties: - requestPath: /_ah/health - port: 8080 - -# Create a backend service, associate it with the health check and instance group. -# The backend service serves as a target for load balancing. -- name: {{ SERVICE }} - type: compute.v1.backendService - properties: - healthChecks: - - $(ref.{{ NAME }}-health-check.selfLink) - portName: http - backends: -{# [START reference] #} - - group: $(ref.{{ NAME }}-frontend-group.instanceGroup) - zone: {{ properties['zone'] }} -{# [END reference] #} - -# Create a URL map and web Proxy. The URL map will send all requests to the -# backend service defined above. -- name: {{ SERVICE }}-map - type: compute.v1.urlMap - properties: - defaultService: $(ref.{{ SERVICE }}.selfLink) - -# This is the actual proxy which uses the URL map to route traffic -# to the backend service -- name: {{ SERVICE }}-proxy - type: compute.v1.targetHttpProxy - properties: - urlMap: $(ref.{{ SERVICE }}-map.selfLink) - -# This is the global forwarding rule which creates an external IP to -# target the http poxy -- name: {{ SERVICE }}-http-rule - type: compute.v1.globalForwardingRule - properties: - target: $(ref.{{ SERVICE }}-proxy.selfLink) - portRange: 80 - -# Creates an autoscaler resource (note that when using the gcloud CLI, -# autoscaling is set as a configuration of the managed instance group -# but autoscaler is a resource so in deployment manager we explicitly -# define it -- name: {{ NAME }}-autoscaler - type: compute.v1.autoscaler - properties: - zone: {{ properties['zone'] }} - target: $(ref.{{ NAME }}-frontend-group.selfLink) - autoscalingPolicy: -{# [START properties] #} - minNumReplicas: {{ properties['min-instances'] }} - maxNumReplicas: {{ properties['max-instances'] }} - loadBalancingUtilization: - utilizationTarget: {{ properties['target-utilization'] }} -{# [END properties] #} - -# Firewall rule that allows traffic to GCE instances with the -# http server tag we created -- name: {{ NAME }}-allow-http - type: compute.v1.firewall - properties: - allowed: - - IPProtocol: tcp - ports: - - 8080 - sourceRanges: - - 0.0.0.0/0 - targetTags: - - http-server - description: "Allow port 8080 access to http-server" - -{# [END all] #} diff --git a/7-gce/gce/deployment_manager/bookshelf.jinja.schema b/7-gce/gce/deployment_manager/bookshelf.jinja.schema deleted file mode 100644 index 526deef6..00000000 --- a/7-gce/gce/deployment_manager/bookshelf.jinja.schema +++ /dev/null @@ -1,59 +0,0 @@ -# Copyright 2016 Google Inc. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# [START all] - -info: - title: Bookshelf GCE Deploy - author: Google Inc. - description: Creates a GCE Deployment - -imports: -- name: startup-script - path: ../startup-script.sh - -required: -- zone -- machine-type -- min-instances -- max-instances -- scopes - -properties: - zone: - description: Zone to create the resources in. - type: string - machine-type: - description: Type of machine to use - type: string - machine-image: - description: The OS image to use on the machines - type: string - min-instances: - description: The minimum number of VMs the autoscaler will create - type: integer - max-instances: - description: The maximum number of VMs the autoscaler will create - type: integer - target-utilization: - description: The target CPU usage for the autoscaler to base its scaling on - type: number - scopes: - description: A list of scopes to create the VM with - type: array - minItems: 1 - items: - type: string - -# [END all] diff --git a/7-gce/gce/deployment_manager/config.yaml b/7-gce/gce/deployment_manager/config.yaml deleted file mode 100644 index 901b55fb..00000000 --- a/7-gce/gce/deployment_manager/config.yaml +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright 2015 Google Inc. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# [START all] -imports: -- name: bookshelf.jinja - path: ./bookshelf.jinja - -resources: -- name: bookshelf - type: bookshelf.jinja - properties: - zone: us-central1-f - machine-type: n1-standard-1 - machine-image: https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/family/debian-9 - min-instances: 1 - max-instances: 10 - target-utilization: 0.6 - scopes: - - https://www.googleapis.com/auth/cloud-platform - -# [END all] diff --git a/7-gce/gce/startup-script.sh b/7-gce/gce/startup-script.sh deleted file mode 100644 index 6f05e7ce..00000000 --- a/7-gce/gce/startup-script.sh +++ /dev/null @@ -1,76 +0,0 @@ -#! /bin/bash -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# [START startup] -set -v - -# Talk to the metadata server to get the project id -PROJECTID=$(curl -s "http://metadata.google.internal/computeMetadata/v1/project/project-id" -H "Metadata-Flavor: Google") - -# Install logging monitor. The monitor will automatically pickup logs sent to -# syslog. -# [START logging] -curl -s "https://storage.googleapis.com/signals-agents/logging/google-fluentd-install.sh" | bash -service google-fluentd restart & -# [END logging] - -# Install dependencies from apt -apt-get update -apt-get install -yq \ - git build-essential supervisor python python-dev python-pip libffi-dev \ - libssl-dev - -# Create a pythonapp user. The application will run as this user. -useradd -m -d /home/pythonapp pythonapp - -# pip from apt is out of date, so make it update itself and install virtualenv. -pip install --upgrade pip virtualenv - -# Get the source code from the Google Cloud Repository -# git requires $HOME and it's not set during the startup script. -export HOME=/root -git config --global credential.helper gcloud.sh -git clone https://source.developers.google.com/p/$PROJECTID/r/[YOUR_REPO_NAME] /opt/app - -# Install app dependencies -virtualenv -p python3 /opt/app/7-gce/env -source /opt/app/7-gce/env/bin/activate -/opt/app/7-gce/env/bin/pip install -r /opt/app/7-gce/requirements.txt - -# Make sure the pythonapp user owns the application code -chown -R pythonapp:pythonapp /opt/app - -# Configure supervisor to start gunicorn inside of our virtualenv and run the -# application. -cat >/etc/supervisor/conf.d/python-app.conf << EOF -[program:pythonapp] -directory=/opt/app/7-gce -command=/opt/app/7-gce/env/bin/honcho start -f ./procfile worker bookshelf -autostart=true -autorestart=true -user=pythonapp -# Environment variables ensure that the application runs inside of the -# configured virtualenv. -environment=VIRTUAL_ENV="/opt/app/7-gce/env",PATH="/opt/app/7-gce/env/bin",\ - HOME="/home/pythonapp",USER="pythonapp" -stdout_logfile=syslog -stderr_logfile=syslog -EOF - -supervisorctl reread -supervisorctl update - -# Application should now be running under supervisor -# [END startup] diff --git a/7-gce/gce/teardown.sh b/7-gce/gce/teardown.sh deleted file mode 100755 index 8f123a10..00000000 --- a/7-gce/gce/teardown.sh +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -#! /bin/bash - -set -x - -ZONE=us-central1-f -gcloud config set compute/zone $ZONE - -GROUP=frontend-group -TEMPLATE=$GROUP-tmpl -SERVICE=my-app-service - -gcloud compute instance-groups managed stop-autoscaling $GROUP --zone $ZONE - -gcloud compute forwarding-rules delete $SERVICE-http-rule --global --quiet - -gcloud compute target-http-proxies delete $SERVICE-proxy --quiet - -gcloud compute url-maps delete $SERVICE-map --quiet - -gcloud compute backend-services delete $SERVICE --global --quiet - -gcloud compute http-health-checks delete ah-health-check --quiet - -gcloud compute instance-groups managed delete $GROUP --quiet - -gcloud compute instance-templates delete $TEMPLATE --quiet - -gcloud compute firewall-rules delete default-allow-http-8080 --quiet diff --git a/7-gce/main.py b/7-gce/main.py deleted file mode 100644 index 3975fab6..00000000 --- a/7-gce/main.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import bookshelf -import config - - -app = bookshelf.create_app(config) - - -# Make the queue available at the top-level, this allows you to run -# `psqworker main.books_queue`. We have to use the app's context because -# it contains all the configuration for plugins. -# If you were using another task queue, such as celery or rq, you can use this -# section to configure your queues to work with Flask. -with app.app_context(): - books_queue = bookshelf.tasks.get_books_queue() - - -# This is only used when running locally. When running live, gunicorn runs -# the application. -if __name__ == '__main__': - app.run(host='127.0.0.1', port=8080, debug=True) diff --git a/7-gce/monitor.py b/7-gce/monitor.py deleted file mode 100644 index ecbd098b..00000000 --- a/7-gce/monitor.py +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -import sys - -from flask import Flask - - -# The app checks this file for the PID of the process to monitor. -PID_FILE = None - - -# Create app to handle health checks and monitor the queue worker. This will -# run alongside the worker, see procfile. -monitor_app = Flask(__name__) - - -# The health check reads the PID file created by psqworker and checks the proc -# filesystem to see if the worker is running. This same pattern can be used for -# rq and celery. -@monitor_app.route('/_ah/health') -def health(): - if not os.path.exists(PID_FILE): - return 'Worker pid not found', 503 - - with open(PID_FILE, 'r') as pidfile: - pid = pidfile.read() - - if not os.path.exists('/proc/{}'.format(pid)): - return 'Worker not running', 503 - - return 'healthy', 200 - - -@monitor_app.route('/') -def index(): - return health() - - -if __name__ == '__main__': - PID_FILE = sys.argv[1] - monitor_app.run('0.0.0.0', 8080) diff --git a/7-gce/procfile b/7-gce/procfile deleted file mode 100644 index 0c4fcec5..00000000 --- a/7-gce/procfile +++ /dev/null @@ -1,3 +0,0 @@ -bookshelf: gunicorn -b 0.0.0.0:$PORT main:app -worker: psqworker --pid /tmp/psq.pid main.books_queue -monitor: python monitor.py /tmp/psq.pid diff --git a/7-gce/requirements-dev.txt b/7-gce/requirements-dev.txt deleted file mode 100644 index 7e4b9ace..00000000 --- a/7-gce/requirements-dev.txt +++ /dev/null @@ -1,9 +0,0 @@ -tox==3.5.3 -flake8==3.6.0 -flaky==3.4.0 -mock==2.0.0 -pytest==4.0.1 -pytest-cov==2.6.0 -BeautifulSoup4==4.6.3 -requests==2.20.1 -retrying==1.3.3 diff --git a/7-gce/requirements.txt b/7-gce/requirements.txt deleted file mode 100644 index 94c7387b..00000000 --- a/7-gce/requirements.txt +++ /dev/null @@ -1,15 +0,0 @@ -Flask>=1.0.0 -google-cloud-datastore==1.7.1 -google-cloud-storage==1.13.0 -google-cloud-logging==1.8.0 -google-cloud-error_reporting==0.30.0 -gunicorn==19.9.0 -oauth2client==4.1.3 -Flask-SQLAlchemy==2.3.2 -PyMySQL==0.9.2 -Flask-PyMongo>=2.0.0 -PyMongo==3.7.2 -six==1.11.0 -requests[security]==2.20.1 -honcho==1.0.1 -psq==0.7.0 diff --git a/7-gce/tests/conftest.py b/7-gce/tests/conftest.py deleted file mode 100644 index 62c2a480..00000000 --- a/7-gce/tests/conftest.py +++ /dev/null @@ -1,91 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""conftest.py is used to define common test fixtures for pytest.""" - -import bookshelf -import config -from google.cloud.exceptions import ServiceUnavailable -from oauth2client.client import HttpAccessTokenRefreshError -import pytest -from retrying import retry - - -#@pytest.yield_fixture(params=['datastore', 'cloudsql', 'mongodb']) -@pytest.yield_fixture(params=['datastore', 'mongodb']) -def app(request): - """This fixtures provides a Flask app instance configured for testing. - - Because it's parametric, it will cause every test that uses this fixture - to run three times: one time for each backend (datastore, cloudsql, and - mongodb). - - It also ensures the tests run within a request context, allowing - any calls to flask.request, flask.current_app, etc. to work.""" - app = bookshelf.create_app( - config, - testing=True, - config_overrides={ - 'DATA_BACKEND': request.param - }) - - with app.test_request_context(): - yield app - - -@pytest.yield_fixture -def model(monkeypatch, app): - """This fixture provides a modified version of the app's model that tracks - all created items and deletes them at the end of the test. - - Any tests that directly or indirectly interact with the database should use - this to ensure that resources are properly cleaned up. - - Monkeypatch is provided by pytest and used to patch the model's create - method. - - The app fixture is needed to provide the configuration and context needed - to get the proper model object. - """ - model = bookshelf.get_model() - - # Ensure no books exist before running. This typically helps if tests - # somehow left the database in a bad state. - delete_all_books(model) - - yield model - - # Delete all books that we created during tests. - delete_all_books(model) - - -# The backend data stores can sometimes be flaky. It's useful to retry this -# a few times before giving up. -@retry( - stop_max_attempt_number=3, - wait_exponential_multiplier=100, - wait_exponential_max=2000) -def delete_all_books(model): - while True: - books, _ = model.list(limit=50) - if not books: - break - for book in books: - model.delete(book['id']) - - -def flaky_filter(info, *args): - """Used by flaky to determine when to re-run a test case.""" - _, e, _ = info - return isinstance(e, (ServiceUnavailable, HttpAccessTokenRefreshError)) diff --git a/7-gce/tests/test_auth.py b/7-gce/tests/test_auth.py deleted file mode 100644 index 72da036e..00000000 --- a/7-gce/tests/test_auth.py +++ /dev/null @@ -1,133 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import contextlib - -import bookshelf -from conftest import flaky_filter -from flaky import flaky -import mock -from oauth2client.client import OAuth2Credentials -import pytest - - -@pytest.fixture -def client_with_credentials(app): - """This fixture provides a Flask app test client that has a session - pre-configured with use credentials.""" - credentials = OAuth2Credentials( - 'access_token', - 'client_id', - 'client_secret', - 'refresh_token', - '3600', - None, - 'Test', - id_token={'sub': '123', 'email': 'user@example.com'}, - scopes=('email', 'profile')) - - @contextlib.contextmanager - def inner(): - with app.test_client() as client: - with client.session_transaction() as session: - session['profile'] = {'email': 'abc@example.com', 'name': 'Test User'} - session['google_oauth2_credentials'] = credentials.to_json() - yield client - - return inner - - -# Mark all test cases in this class as flaky, so that if errors occur they -# can be retried. This is useful when databases are temporarily unavailable. -@flaky(rerun_filter=flaky_filter) -# Tell pytest to use both the app and model fixtures for all test cases. -# This ensures that configuration is properly applied and that all database -# resources created during tests are cleaned up. These fixtures are defined -# in conftest.py -@pytest.mark.usefixtures('app', 'model') -class TestAuth(object): - def test_not_logged_in(self, app): - with app.test_client() as c: - rv = c.get('/books/mine') - - assert rv.status < '400' - body = rv.data.decode('utf-8') - assert 'Redirecting' in body - - def test_logged_in(self, client_with_credentials): - with client_with_credentials() as c: - rv = c.get('/books/mine') - - assert rv.status < '400' - body = rv.data.decode('utf-8') - assert 'Redirecting' not in body - - def test_add_anonymous(self, app): - data = { - 'title': 'Test Book', - } - - with app.test_client() as c: - rv = c.post('/books/add', data=data, follow_redirects=True) - - assert rv.status == '200 OK' - body = rv.data.decode('utf-8') - assert 'Test Book' in body - assert 'Added by Anonymous' in body - - def test_add_logged_in(self, client_with_credentials): - data = { - 'title': 'Test Book', - } - - with client_with_credentials() as c: - rv = c.post('/books/add', data=data, follow_redirects=True) - - assert rv.status == '200 OK' - body = rv.data.decode('utf-8') - assert 'Test Book' in body - assert 'Added by Test User' in body - - def test_mine(self, model, client_with_credentials): - # Create two books, one created by the logged in user and one - # created by another user. - model.create({ - 'title': 'Book 1', - 'createdById': 'abc@example.com' - }) - - model.create({ - 'title': 'Book 2', - 'createdById': 'def@example.com' - }) - - # Check the "My Books" page and make sure only one of the books - # appears. - with client_with_credentials() as c: - rv = c.get('/books/mine') - - assert rv.status == '200 OK' - body = rv.data.decode('utf-8') - assert 'Book 1' in body - assert 'Book 2' not in body - - @mock.patch("httplib2.Http") - def test_request_user_info(self, HttpMock): - httpObj = mock.MagicMock() - responseMock = mock.MagicMock(status=200) - httpObj.request = mock.MagicMock( - return_value=(responseMock, b'{"name": "bill"}')) - HttpMock.return_value = httpObj - credentials = mock.MagicMock() - bookshelf._request_user_info(credentials) diff --git a/7-gce/tests/test_crud.py b/7-gce/tests/test_crud.py deleted file mode 100644 index c0d2f40f..00000000 --- a/7-gce/tests/test_crud.py +++ /dev/null @@ -1,88 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import re - -from conftest import flaky_filter -from flaky import flaky -import pytest - - -# Mark all test cases in this class as flaky, so that if errors occur they -# can be retried. This is useful when databases are temporarily unavailable. -@flaky(rerun_filter=flaky_filter) -# Tell pytest to use both the app and model fixtures for all test cases. -# This ensures that configuration is properly applied and that all database -# resources created during tests are cleaned up. These fixtures are defined -# in conftest.py -@pytest.mark.usefixtures('app', 'model') -class TestCrudActions(object): - - def test_list(self, app, model): - for i in range(1, 12): - model.create({'title': u'Book {0}'.format(i)}) - - with app.test_client() as c: - rv = c.get('/books/') - - assert rv.status == '200 OK' - - body = rv.data.decode('utf-8') - assert 'Book 1' in body, "Should show books" - assert len(re.findall('

Book', body)) == 10, ( - "Should not show more than 10 books") - assert 'More' in body, "Should have more than one page" - - def test_add(self, app): - data = { - 'title': 'Test Book', - 'author': 'Test Author', - 'publishedDate': 'Test Date Published', - 'description': 'Test Description' - } - - with app.test_client() as c: - rv = c.post('/books/add', data=data, follow_redirects=True) - - assert rv.status == '200 OK' - body = rv.data.decode('utf-8') - assert 'Test Book' in body - assert 'Test Author' in body - assert 'Test Date Published' in body - assert 'Test Description' in body - - def test_edit(self, app, model): - existing = model.create({'title': "Temp Title"}) - - with app.test_client() as c: - rv = c.post( - '/books/%s/edit' % existing['id'], - data={'title': 'Updated Title'}, - follow_redirects=True) - - assert rv.status == '200 OK' - body = rv.data.decode('utf-8') - assert 'Updated Title' in body - assert 'Temp Title' not in body - - def test_delete(self, app, model): - existing = model.create({'title': "Temp Title"}) - - with app.test_client() as c: - rv = c.get( - '/books/%s/delete' % existing['id'], - follow_redirects=True) - - assert rv.status == '200 OK' - assert not model.read(existing['id']) diff --git a/7-gce/tests/test_end_to_end.py b/7-gce/tests/test_end_to_end.py deleted file mode 100644 index df80e8a0..00000000 --- a/7-gce/tests/test_end_to_end.py +++ /dev/null @@ -1,80 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -import re - -from bs4 import BeautifulSoup -import pytest -import requests -from retrying import retry - - -@pytest.mark.e2e -def test_end_to_end(): - """Tests designed to be run against live environments. - - Unlike the integration tests in the other packages, these tests are - designed to be run against fully-functional live environments. - - To run locally, start both main.py and psq_worker main.books_queue and - run this file. - - It can be run against a live environment by setting the E2E_URL - environment variables before running the tests: - - E2E_URL=http://your-app-id.appspot.com \ - nosetests tests/test_end_to_end.py - """ - - base_url = os.environ.get('E2E_URL', 'http://localhost:8080') - - book_data = { - 'title': 'a confederacy of dunces', - } - - response = requests.post(base_url + '/books/add', data=book_data) - - # There was a 302, so get the book's URL from the redirect. - book_url = response.request.url - book_id = book_url.rsplit('/', 1).pop() - - # Use retry because it will take some indeterminate time for the pub/sub - # message to be processed. - @retry(wait_exponential_multiplier=5000, stop_max_attempt_number=12) - def check_for_updated_data(): - # Check that the book's information was updated. - response = requests.get(book_url) - assert response.status_code == 200 - - soup = BeautifulSoup(response.text, 'html.parser') - - title = soup.find('h4', 'book-title').contents[0].strip() - assert re.search(r'A Confederacy of Dunces', title, re.I) - - author = soup.find('h5', 'book-author').string - assert re.search(r'John Kennedy Toole', author, re.I) - - description = soup.find('p', 'book-description').string - assert re.search(r'Ignatius', description, re.I) - - image_src = soup.find('img', 'book-image')['src'] - image = requests.get(image_src) - assert image.status_code == 200 - - try: - check_for_updated_data() - finally: - # Delete the book we created. - requests.get(base_url + '/books/{}/delete'.format(book_id)) diff --git a/7-gce/tests/test_storage.py b/7-gce/tests/test_storage.py deleted file mode 100644 index 531c8827..00000000 --- a/7-gce/tests/test_storage.py +++ /dev/null @@ -1,70 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import re - -from conftest import flaky_filter -from flaky import flaky -import httplib2 -import pytest -from six import BytesIO - - -# Mark all test cases in this class as flaky, so that if errors occur they -# can be retried. This is useful when databases are temporarily unavailable. -@flaky(rerun_filter=flaky_filter) -# Tell pytest to use both the app and model fixtures for all test cases. -# This ensures that configuration is properly applied and that all database -# resources created during tests are cleaned up. These fixtures are defined -# in conftest.py -@pytest.mark.usefixtures('app', 'model') -class TestStorage(object): - - def test_upload_image(self, app): - data = { - 'title': 'Test Book', - 'author': 'Test Author', - 'publishedDate': 'Test Date Published', - 'description': 'Test Description', - 'image': (BytesIO(b'hello world'), 'hello.jpg') - } - - with app.test_client() as c: - rv = c.post('/books/add', data=data, follow_redirects=True) - - assert rv.status == '200 OK' - body = rv.data.decode('utf-8') - - img_tag = re.search(''), - '1337h4x0r.php') - } - - with app.test_client() as c: - rv = c.post('/books/add', data=data, follow_redirects=True) - - # check we weren't pwned - assert rv.status == '400 BAD REQUEST' diff --git a/7-gce/tox.ini b/7-gce/tox.ini deleted file mode 100644 index 8a04f077..00000000 --- a/7-gce/tox.ini +++ /dev/null @@ -1,30 +0,0 @@ -[tox] -skipsdist = True -envlist = lint,py27,py36 - -[testenv] -deps = - -rrequirements.txt - -rrequirements-dev.txt -commands = - py.test --cov=bookshelf --no-success-flaky-report -m "not e2e" {posargs: tests} -passenv = GOOGLE_APPLICATION_CREDENTIALS DATASTORE_HOST E2E_URL -setenv = PYTHONPATH={toxinidir} - - -[testenv:py27-e2e] -basepython = python2.7 -commands = - py.test --no-success-flaky-report -m "e2e" {posargs: tests} - -[testenv:py36-e2e] -basepython = python3.6 -commands = - py.test --no-success-flaky-report -m "e2e" {posargs: tests} - -[testenv:lint] -deps = - flake8 - flake8-import-order -commands = - flake8 --import-order-style=google bookshelf tests diff --git a/noxfile.py b/noxfile.py index 75e2431b..858c8c91 100644 --- a/noxfile.py +++ b/noxfile.py @@ -9,13 +9,6 @@ DIRS = [ # Hello world doesn't have system tests, just a lint test which will be # covered by the global lint here. - # '1-hello-world', - '2-structured-data', - '3-binary-data', - '4-auth', - '5-logging', - '6-pubsub', - '7-gce', 'background/app', 'background/function', 'sessions', diff --git a/optional-kubernetes-engine/.dockerignore b/optional-kubernetes-engine/.dockerignore deleted file mode 100644 index fdc976d4..00000000 --- a/optional-kubernetes-engine/.dockerignore +++ /dev/null @@ -1,17 +0,0 @@ -__pycache__ -*.pyc -*.pyo -*.pyd -.Python -env -pip-log.txt -pip-delete-this-directory.txt -.tox -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*,cover -*.log -.git diff --git a/optional-kubernetes-engine/Dockerfile b/optional-kubernetes-engine/Dockerfile deleted file mode 100644 index 69f9d260..00000000 --- a/optional-kubernetes-engine/Dockerfile +++ /dev/null @@ -1,27 +0,0 @@ -# The Google App Engine python runtime is Debian Jessie with Python installed -# and various os-level packages to allow installation of popular Python -# libraries. The source is on github at: -# https://github.com/GoogleCloudPlatform/python-docker -FROM gcr.io/google-appengine/python - -# Create a virtualenv for the application dependencies. -# If you want to use Python 2, add the -p python2.7 flag. -RUN virtualenv -p python3.4 /env - -# Set virtualenv environment variables. This is equivalent to running -# source /env/bin/activate. This ensures the application is executed within -# the context of the virtualenv and will have access to its dependencies. -ENV VIRTUAL_ENV /env -ENV PATH /env/bin:$PATH - -# Install dependencies. -ADD requirements.txt /app/requirements.txt -RUN pip install -r /app/requirements.txt - -# Add application code. -ADD . /app - -# Instead of using gunicorn directly, we'll use Honcho. Honcho is a python port -# of the Foreman process manager. $PROCESSES is set in the pod manifest -# to control which processes Honcho will start. -CMD honcho start -f /app/procfile $PROCESSES diff --git a/optional-kubernetes-engine/Makefile b/optional-kubernetes-engine/Makefile deleted file mode 100644 index f275ddf4..00000000 --- a/optional-kubernetes-engine/Makefile +++ /dev/null @@ -1,50 +0,0 @@ -GCLOUD_PROJECT:=$(shell gcloud config list project --format="value(core.project)") - -.PHONY: all -all: deploy - -.PHONY: create-cluster -create-cluster: - gcloud container clusters create bookshelf \ - --scopes "cloud-platform" \ - --num-nodes 2 - gcloud container clusters get-credentials bookshelf - -.PHONY: create-bucket -create-bucket: - gsutil mb gs://$(GCLOUD_PROJECT) - gsutil defacl set public-read gs://$(GCLOUD_PROJECT) - -.PHONY: build -build: - docker build -t gcr.io/$(GCLOUD_PROJECT)/bookshelf . - -.PHONY: push -push: build - gcloud docker -- push gcr.io/$(GCLOUD_PROJECT)/bookshelf - -.PHONY: template -template: - sed -i ".tmpl" "s/\[GCLOUD_PROJECT\]/$(GCLOUD_PROJECT)/g" bookshelf-frontend.yaml - sed -i ".tmpl" "s/\[GCLOUD_PROJECT\]/$(GCLOUD_PROJECT)/g" bookshelf-worker.yaml - -.PHONY: create-service -create-service: - kubectl create -f bookshelf-service.yaml - -.PHONY: deploy-frontend -deploy-frontend: push template - kubectl create -f bookshelf-frontend.yaml - -.PHONY: deploy-worker -deploy-worker: push template - kubectl create -f bookshelf-worker.yaml - -.PHONY: deploy -deploy: deploy-frontend deploy-worker create-service - -.PHONY: delete -delete: - -kubectl delete -f bookshelf-service.yaml - -kubectl delete -f bookshelf-worker.yaml - -kubectl delete -f bookshelf-frontend.yaml diff --git a/optional-kubernetes-engine/README.md b/optional-kubernetes-engine/README.md deleted file mode 100644 index e3c17236..00000000 --- a/optional-kubernetes-engine/README.md +++ /dev/null @@ -1,97 +0,0 @@ -# Deploy Bookshelf to Google Kubernetes Engine - -This optional tutorial will walk you through how to deploy the Bookshelf sample application to [Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/). This tutorial is also applicable to [Kubernetes](http://kubernetes.io/) outside of Google Kubernetes Engine, but may require additional steps for external load balancing. - -## Pre-requisites - -1. Create a project in the [Google Cloud Platform Console](https://console.cloud.google.com). - -2. [Enable billing](https://console.cloud.google.com/project/_/settings) for your project. - -3. [Enable APIs](https://console.cloud.google.com/flows/enableapi?apiid=datastore,pubsub,storage_api,logging,plus) for your project. The provided link will enable all necessary APIs, but if you wish to do so manually you will need Datastore, Pub/Sub, Storage, and Logging. - -4. Install the [Google Cloud SDK](https://cloud.google.com/sdk) - - $ curl https://sdk.cloud.google.com | bash - $ gcloud init - -5. Install [Docker](https://www.docker.com/). - -## Create a cluster - -Create a cluster for the bookshelf application: - - gcloud container clusters create bookshelf \ - --scopes "cloud-platform" \ - --num-nodes 2 - gcloud container clusters get-credentials bookshelf - -The scopes specified in the `--scopes` argument allows nodes in the cluster to access Google Cloud Platform APIs, such as the Cloud Datastore API. - -Alternatively, you can use make: - - make create-cluster - -## Create a Cloud Storage bucket - -The bookshelf application uses [Google Cloud Storage](https://cloud.google.com/storage) to store image files. Create a bucket for your project: - - gsutil mb gs:// - gsutil defacl set public-read gs:// - -Alternatively, you can use make: - - make create-bucket - -## Update config.py - -Modify config.py and enter your Cloud Project ID into the `PROJECT_ID` and `CLOUD_STORAGE_BUCKET` field. The remaining configuration values are only needed if you wish to use a different database or if you wish to enable log-in via oauth2, which requires a domain name. - -## Build the bookshelf container - -Before the application can be deployed to Kubernetes Engine, you will need build and push the image to [Google Container Registry](https://cloud.google.com/container-registry/). - - docker build -t gcr.io//bookshelf . - gcloud docker push gcr.io//bookshelf - -Alternatively, you can use make: - - make push - -## Deploy the bookshelf frontend - -The bookshelf app has two distinct "tiers". The frontend serves a web interface to create and manage books, while the worker handles fetching book information from the Google Books API. - -Update `bookshelf-frontend.yaml` with your Project ID or use `make template`. This file contains the Kubernetes resource definitions to deploy the frontend. You can use `kubectl` to create these resources on the cluster: - - kubectl create -f bookshelf-frontend.yaml - -Alternatively, you can use make: - - make deploy-frontend - -Once the resources are created, there should be 3 `bookshelf-frontend` pods on the cluster. To see the pods and ensure that they are running: - - kubectl get pods - -If the pods are not ready or if you see restarts, you can get the logs for a particular pod to figure out the issue: - - kubectl logs pod-id - -Once the pods are ready, you can get the public IP address of the load balancer: - - kubectl get services bookshelf-frontend - -You can then browse to the public IP address in your browser to see the bookshelf application. - -## Deploy worker - -Update `bookshelf-worker.yaml` with your Project ID or use `make template`. This file contains the Kubernetes resource definitions to deploy the worker. The worker doesn't need to serve web traffic or expose any ports, so it has significantly less configuration than the frontend. You can use `kubectl` to create these resources on the cluster: - - kubectl create -f bookshelf-worker.yaml - -Alternatively, you can use make: - - make deploy-worker - -Once again, use `kubectl get pods` to check the status of the worker pods. Once the worker pods are up and running, you should be able to create books on the frontend and the workers will handle updating book information in the background. diff --git a/optional-kubernetes-engine/bookshelf-frontend.yaml b/optional-kubernetes-engine/bookshelf-frontend.yaml deleted file mode 100644 index 265c8e35..00000000 --- a/optional-kubernetes-engine/bookshelf-frontend.yaml +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License - -# This file configures the bookshelf application frontend. The frontend serves -# public web traffic. - -apiVersion: extensions/v1beta1 -kind: Deployment -metadata: - name: bookshelf-frontend - labels: - app: bookshelf -# The bookshelf frontend replica set ensures that at least 3 -# instances of the bookshelf app are running on the cluster. -# For more info about Pods see: -# https://cloud.google.com/kubernetes-engine/docs/pods/ -spec: - replicas: 3 - template: - metadata: - labels: - app: bookshelf - tier: frontend - spec: - containers: - - name: bookshelf-app - # Replace [GCLOUD_PROJECT] with your project ID or use `make template`. - image: gcr.io/[GCLOUD_PROJECT]/bookshelf - # This setting makes nodes pull the docker image every time before - # starting the pod. This is useful when debugging, but should be turned - # off in production. - imagePullPolicy: Always - # The PROCESSES environment variable is used by Honcho in the - # Dockerfile's CMD to control which processes are started. In this - # case, only the bookshelf process is needed. - env: - - name: PROCESSES - value: bookshelf - # The bookshelf process listens on port 8080 for web traffic by default. - ports: - - name: http-server - containerPort: 8080 diff --git a/optional-kubernetes-engine/bookshelf-service.yaml b/optional-kubernetes-engine/bookshelf-service.yaml deleted file mode 100644 index db8f1fe7..00000000 --- a/optional-kubernetes-engine/bookshelf-service.yaml +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright 2016 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License - -# The bookshelf service provides a load-balancing proxy over the bookshelf -# frontend pods. By specifying the type as a 'LoadBalancer', Kubernetes Engine -# will create an external HTTP load balancer. -# For more information about Services see: -# https://cloud.google.com/kubernetes-engine/docs/services/ -# For more information about external HTTP load balancing see: -# https://cloud.google.com/kubernetes-engine/docs/load-balancer -apiVersion: v1 -kind: Service -metadata: - name: bookshelf-frontend - labels: - app: bookshelf - tier: frontend -spec: - type: LoadBalancer - ports: - - port: 80 - targetPort: http-server - selector: - app: bookshelf - tier: frontend diff --git a/optional-kubernetes-engine/bookshelf-worker.yaml b/optional-kubernetes-engine/bookshelf-worker.yaml deleted file mode 100644 index c30df89b..00000000 --- a/optional-kubernetes-engine/bookshelf-worker.yaml +++ /dev/null @@ -1,49 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License - -# This file configures the bookshelf task worker. The worker is responsible -# for processing book requests and updating book information. - -apiVersion: extensions/v1beta1 -kind: Deployment -metadata: - name: bookshelf-worker - labels: - app: bookshelf -# The bookshelf worker replica set ensures that at least 2 instances of the -# bookshelf worker pod are running on the cluster. -# For more info about Pods see: -# https://cloud.google.com/kubernetes-engine/docs/pods/ -spec: - replicas: 2 - template: - metadata: - labels: - app: bookshelf - tier: worker - spec: - containers: - - name: bookshelf-app - # Replace [GCLOUD_PROJECT] with your project ID or use `make template`. - image: gcr.io/[GCLOUD_PROJECT]/bookshelf - # This setting makes nodes pull the docker image every time before - # starting the pod. This is useful when debugging, but should be turned - # off in production. - imagePullPolicy: Always - # The PROCESSES environment variable is used by Honcho in the - # Dockerfile's CMD to control which processes are started. In this - # case, only the worker process is needed. - env: - - name: PROCESSES - value: worker diff --git a/optional-kubernetes-engine/bookshelf/__init__.py b/optional-kubernetes-engine/bookshelf/__init__.py deleted file mode 100644 index 5fd93b9b..00000000 --- a/optional-kubernetes-engine/bookshelf/__init__.py +++ /dev/null @@ -1,125 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import json -import logging - -from flask import current_app, Flask, redirect, request, session, url_for -import httplib2 -from oauth2client.contrib.flask_util import UserOAuth2 - - -oauth2 = UserOAuth2() - - -def create_app(config, debug=False, testing=False, config_overrides=None): - app = Flask(__name__) - app.config.from_object(config) - - app.debug = debug - app.testing = testing - - if config_overrides: - app.config.update(config_overrides) - - # Configure logging - if not app.testing: - logging.basicConfig(level=logging.INFO) - - # Setup the data model. - with app.app_context(): - model = get_model() - model.init_app(app) - - # Create a health check handler. Health checks are used when running on - # Google Compute Engine by the load balancer to determine which instances - # can serve traffic. Google App Engine also uses health checking, but - # accepts any non-500 response as healthy. - @app.route('/_ah/health') - def health_check(): - return 'ok', 200 - - # Initalize the OAuth2 helper. - oauth2.init_app( - app, - scopes=['email', 'profile'], - authorize_callback=_request_user_info) - - # Add a logout handler. - @app.route('/logout') - def logout(): - # Delete the user's profile and the credentials stored by oauth2. - del session['profile'] - session.modified = True - oauth2.storage.delete() - return redirect(request.referrer or '/') - - # Register the Bookshelf CRUD blueprint. - from .crud import crud - app.register_blueprint(crud, url_prefix='/books') - - # Add a default root route. - @app.route("/") - def index(): - return redirect(url_for('crud.list')) - - # Add an error handler. This is useful for debugging the live application, - # however, you should disable the output of the exception for production - # applications. - @app.errorhandler(500) - def server_error(e): - return """ - An internal error occurred:
{}
- See logs for full stacktrace. - """.format(e), 500 - - return app - - -def get_model(): - model_backend = current_app.config['DATA_BACKEND'] - if model_backend == 'cloudsql': - from . import model_cloudsql - model = model_cloudsql - elif model_backend == 'datastore': - from . import model_datastore - model = model_datastore - elif model_backend == 'mongodb': - from . import model_mongodb - model = model_mongodb - else: - raise ValueError( - "No appropriate databackend configured. " - "Please specify datastore, cloudsql, or mongodb") - - return model - - -def _request_user_info(credentials): - """ - Makes an HTTP request to the Google OAuth2 API to retrieve the user's basic - profile information, including full name and photo, and stores it in the - Flask session. - """ - http = httplib2.Http() - credentials.authorize(http) - resp, content = http.request( - 'https://www.googleapis.com/oauth2/v3/userinfo') - - if resp.status != 200: - current_app.logger.error( - "Error while obtaining user profile: \n%s: %s", resp, content) - return None - - session['profile'] = json.loads(content.decode('utf-8')) diff --git a/optional-kubernetes-engine/bookshelf/crud.py b/optional-kubernetes-engine/bookshelf/crud.py deleted file mode 100644 index e07b4053..00000000 --- a/optional-kubernetes-engine/bookshelf/crud.py +++ /dev/null @@ -1,131 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from bookshelf import get_model, oauth2, storage, tasks -from flask import Blueprint, current_app, redirect, render_template, request, \ - session, url_for - - -crud = Blueprint('crud', __name__) - - -def upload_image_file(file): - """ - Upload the user-uploaded file to Google Cloud Storage and retrieve its - publicly-accessible URL. - """ - if not file: - return None - - public_url = storage.upload_file( - file.read(), - file.filename, - file.content_type - ) - - current_app.logger.info( - "Uploaded file %s as %s.", file.filename, public_url) - - return public_url - - -@crud.route("/") -def list(): - token = request.args.get('page_token', None) - if token: - token = token.encode('utf-8') - - books, next_page_token = get_model().list(cursor=token) - - return render_template( - "list.html", - books=books, - next_page_token=next_page_token) - - -@crud.route("/mine") -@oauth2.required -def list_mine(): - token = request.args.get('page_token', None) - if token: - token = token.encode('utf-8') - - books, next_page_token = get_model().list_by_user( - user_id=session['profile']['id'], - cursor=token) - - return render_template( - "list.html", - books=books, - next_page_token=next_page_token) - - -@crud.route('/') -def view(id): - book = get_model().read(id) - return render_template("view.html", book=book) - - -@crud.route('/add', methods=['GET', 'POST']) -def add(): - if request.method == 'POST': - data = request.form.to_dict(flat=True) - - # If an image was uploaded, update the data to point to the new image. - image_url = upload_image_file(request.files.get('image')) - - if image_url: - data['imageUrl'] = image_url - - # If the user is logged in, associate their profile with the new book. - if 'profile' in session: - data['createdBy'] = session['profile']['name'] - data['createdById'] = session['profile']['email'] - - book = get_model().create(data) - - q = tasks.get_books_queue() - q.enqueue(tasks.process_book, book['id']) - - return redirect(url_for('.view', id=book['id'])) - - return render_template("form.html", action="Add", book={}) - - -@crud.route('//edit', methods=['GET', 'POST']) -def edit(id): - book = get_model().read(id) - - if request.method == 'POST': - data = request.form.to_dict(flat=True) - - image_url = upload_image_file(request.files.get('image')) - - if image_url: - data['imageUrl'] = image_url - - book = get_model().update(data, id) - - q = tasks.get_books_queue() - q.enqueue(tasks.process_book, book['id']) - - return redirect(url_for('.view', id=book['id'])) - - return render_template("form.html", action="Edit", book=book) - - -@crud.route('//delete') -def delete(id): - get_model().delete(id) - return redirect(url_for('.list')) diff --git a/optional-kubernetes-engine/bookshelf/model_cloudsql.py b/optional-kubernetes-engine/bookshelf/model_cloudsql.py deleted file mode 100644 index d84207d2..00000000 --- a/optional-kubernetes-engine/bookshelf/model_cloudsql.py +++ /dev/null @@ -1,119 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from flask import Flask -from flask_sqlalchemy import SQLAlchemy - - -builtin_list = list - - -db = SQLAlchemy() - - -def init_app(app): - # Disable track modifications, as it unnecessarily uses memory. - app.config.setdefault('SQLALCHEMY_TRACK_MODIFICATIONS', False) - db.init_app(app) - - -def from_sql(row): - """Translates a SQLAlchemy model instance into a dictionary""" - data = row.__dict__.copy() - data['id'] = row.id - data.pop('_sa_instance_state') - return data - - -class Book(db.Model): - __tablename__ = 'books' - - id = db.Column(db.Integer, primary_key=True) - title = db.Column(db.String(255)) - author = db.Column(db.String(255)) - publishedDate = db.Column(db.String(255)) - imageUrl = db.Column(db.String(255)) - description = db.Column(db.String(4096)) - createdBy = db.Column(db.String(255)) - createdById = db.Column(db.String(255)) - - def __repr__(self): - return " - - - Bookshelf - Python on Google Cloud Platform - - - - - - -
- {% block content %}{% endblock %} -
- {{user}} - - diff --git a/optional-kubernetes-engine/bookshelf/templates/form.html b/optional-kubernetes-engine/bookshelf/templates/form.html deleted file mode 100644 index 2de32a29..00000000 --- a/optional-kubernetes-engine/bookshelf/templates/form.html +++ /dev/null @@ -1,67 +0,0 @@ -{# -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -#} - -{% extends "base.html" %} - -{% block content %} -

{{action}} book

- -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- - - - - - - - -
- -{% endblock %} diff --git a/optional-kubernetes-engine/bookshelf/templates/list.html b/optional-kubernetes-engine/bookshelf/templates/list.html deleted file mode 100644 index 3362f0e2..00000000 --- a/optional-kubernetes-engine/bookshelf/templates/list.html +++ /dev/null @@ -1,55 +0,0 @@ -{# -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -#} - -{% extends "base.html" %} - -{% block content %} - -

Books

- - - Add book - - -{% for book in books %} - -{% else %} -

No books found

-{% endfor %} - -{% if next_page_token %} - -{% endif %} - -{% endblock %} diff --git a/optional-kubernetes-engine/bookshelf/templates/view.html b/optional-kubernetes-engine/bookshelf/templates/view.html deleted file mode 100644 index e654e8ab..00000000 --- a/optional-kubernetes-engine/bookshelf/templates/view.html +++ /dev/null @@ -1,53 +0,0 @@ -{# -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -#} - -{% extends "base.html" %} - -{% block content %} - -

Book

- - - -
-
- {% if book.imageUrl %} - - {% else %} - - {% endif %} -
-
-

- {{book.title}} - {{book.publishedDate}} -

-
By {{book.author|default('Unknown', True)}}
-

{{book.description}}

- Added by {{book.createdBy|default('Anonymous', True)}} -
-
- -{% endblock %} diff --git a/optional-kubernetes-engine/config.py b/optional-kubernetes-engine/config.py deleted file mode 100644 index 0fd601ed..00000000 --- a/optional-kubernetes-engine/config.py +++ /dev/null @@ -1,107 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -This file contains all of the configuration values for the application. -Update this file with the values for your specific Google Cloud project. -You can create and manage projects at https://console.developers.google.com -""" - -import os - -# The secret key is used by Flask to encrypt session cookies. -SECRET_KEY = 'secret' - -# There are three different ways to store the data in the application. -# You can choose 'datastore', 'cloudsql', or 'mongodb'. Be sure to -# configure the respective settings for the one you choose below. -# You do not have to configure the other data backends. If unsure, choose -# 'datastore' as it does not require any additional configuration. -DATA_BACKEND = 'datastore' - -# Google Cloud Project ID. This can be found on the 'Overview' page at -# https://console.developers.google.com -PROJECT_ID = 'your-project-id' - -# CloudSQL & SQLAlchemy configuration -# Replace the following values the respective values of your Cloud SQL -# instance. -CLOUDSQL_USER = 'root' -CLOUDSQL_PASSWORD = 'your-cloudsql-password' -CLOUDSQL_DATABASE = 'bookshelf' -# Set this value to the Cloud SQL connection name, e.g. -# "project:region:cloudsql-instance". -# You must also update the value in app.yaml. -CLOUDSQL_CONNECTION_NAME = 'your-cloudsql-connection-name' - -# The CloudSQL proxy is used locally to connect to the cloudsql instance. -# To start the proxy, use: -# -# $ cloud_sql_proxy -instances=your-connection-name=tcp:3306 -# -# Alternatively, you could use a local MySQL instance for testing. -LOCAL_SQLALCHEMY_DATABASE_URI = ( - 'mysql+pymysql://{user}:{password}@localhost/{database}').format( - user=CLOUDSQL_USER, password=CLOUDSQL_PASSWORD, - database=CLOUDSQL_DATABASE) - -# When running on App Engine a unix socket is used to connect to the cloudsql -# instance. -LIVE_SQLALCHEMY_DATABASE_URI = ( - 'mysql+pymysql://{user}:{password}@localhost/{database}' - '?unix_socket=/cloudsql/{connection_name}').format( - user=CLOUDSQL_USER, password=CLOUDSQL_PASSWORD, - database=CLOUDSQL_DATABASE, connection_name=CLOUDSQL_CONNECTION_NAME) - -if os.environ.get('GAE_INSTANCE'): - SQLALCHEMY_DATABASE_URI = LIVE_SQLALCHEMY_DATABASE_URI -else: - SQLALCHEMY_DATABASE_URI = LOCAL_SQLALCHEMY_DATABASE_URI - -# Mongo configuration -# If using mongolab, the connection URI is available from the mongolab control -# panel. If self-hosting on compute engine, replace the values below. -MONGO_URI = 'mongodb://user:password@host:27017/database' - -# Google Cloud Storage and upload settings. -# Typically, you'll name your bucket the same as your project. To create a -# bucket: -# -# $ gsutil mb gs:// -# -# You also need to make sure that the default ACL is set to public-read, -# otherwise users will not be able to see their upload images: -# -# $ gsutil defacl set public-read gs:// -# -# You can adjust the max content length and allow extensions settings to allow -# larger or more varied file types if desired. -CLOUD_STORAGE_BUCKET = 'your-bucket-name' -MAX_CONTENT_LENGTH = 8 * 1024 * 1024 -ALLOWED_EXTENSIONS = set(['png', 'jpg', 'jpeg', 'gif']) - -# OAuth2 configuration. -# This can be generated from the Google Developers Console at -# https://console.developers.google.com/project/_/apiui/credential. -# Note that you will need to add all URLs that your application uses as -# authorized redirect URIs. For example, typically you would add the following: -# -# * http://localhost:8080/oauth2callback -# * https://.appspot.com/oauth2callback. -# -# If you receive a invalid redirect URI error review you settings to ensure -# that the current URI is allowed. -GOOGLE_OAUTH2_CLIENT_ID = \ - 'your-client-id' -GOOGLE_OAUTH2_CLIENT_SECRET = 'your-client-secret' diff --git a/optional-kubernetes-engine/main.py b/optional-kubernetes-engine/main.py deleted file mode 100644 index 3fc2078e..00000000 --- a/optional-kubernetes-engine/main.py +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import bookshelf -import config - - -# Note: debug=True is enabled here to help with troubleshooting. You should -# remove this in production. -app = bookshelf.create_app(config, debug=True) - - -# Make the queue available at the top-level, this allows you to run -# `psqworker main.books_queue`. We have to use the app's context because -# it contains all the configuration for plugins. -# If you were using another task queue, such as celery or rq, you can use this -# section to configure your queues to work with Flask. -with app.app_context(): - books_queue = bookshelf.tasks.get_books_queue() - - -# This is only used when running locally. When running live, gunicorn runs -# the application. -if __name__ == '__main__': - app.run(host='127.0.0.1', port=8080, debug=True) diff --git a/optional-kubernetes-engine/procfile b/optional-kubernetes-engine/procfile deleted file mode 100644 index 99ae5024..00000000 --- a/optional-kubernetes-engine/procfile +++ /dev/null @@ -1,2 +0,0 @@ -bookshelf: gunicorn -b 0.0.0.0:$PORT main:app -worker: psqworker main.books_queue diff --git a/optional-kubernetes-engine/requirements-dev.txt b/optional-kubernetes-engine/requirements-dev.txt deleted file mode 100644 index 3118b924..00000000 --- a/optional-kubernetes-engine/requirements-dev.txt +++ /dev/null @@ -1,8 +0,0 @@ -tox==3.5.3 -flake8==3.6.0 -flaky==3.4.0 -pytest==4.0.1 -pytest-cov==2.6.0 -BeautifulSoup4==4.6.3 -requests==2.20.1 -retrying==1.3.3 diff --git a/optional-kubernetes-engine/requirements.txt b/optional-kubernetes-engine/requirements.txt deleted file mode 100644 index 5d998499..00000000 --- a/optional-kubernetes-engine/requirements.txt +++ /dev/null @@ -1,16 +0,0 @@ -Flask>=1.0.0 -google-cloud-datastore==1.7.1 -google-cloud-storage==1.13.0 -google-cloud-logging==1.8.0 -google-cloud-error_reporting==0.30.0 -gunicorn==19.9.0 -oauth2client==4.1.3 -mock==2.0.0 -Flask-SQLAlchemy==2.3.2 -PyMySQL==0.9.2 -Flask-PyMongo>=2.0.0 -PyMongo==3.7.2 -six==1.11.0 -requests[security]==2.20.1 -honcho==1.0.1 -psq==0.7.0 diff --git a/optional-kubernetes-engine/tests/conftest.py b/optional-kubernetes-engine/tests/conftest.py deleted file mode 100644 index 62c2a480..00000000 --- a/optional-kubernetes-engine/tests/conftest.py +++ /dev/null @@ -1,91 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""conftest.py is used to define common test fixtures for pytest.""" - -import bookshelf -import config -from google.cloud.exceptions import ServiceUnavailable -from oauth2client.client import HttpAccessTokenRefreshError -import pytest -from retrying import retry - - -#@pytest.yield_fixture(params=['datastore', 'cloudsql', 'mongodb']) -@pytest.yield_fixture(params=['datastore', 'mongodb']) -def app(request): - """This fixtures provides a Flask app instance configured for testing. - - Because it's parametric, it will cause every test that uses this fixture - to run three times: one time for each backend (datastore, cloudsql, and - mongodb). - - It also ensures the tests run within a request context, allowing - any calls to flask.request, flask.current_app, etc. to work.""" - app = bookshelf.create_app( - config, - testing=True, - config_overrides={ - 'DATA_BACKEND': request.param - }) - - with app.test_request_context(): - yield app - - -@pytest.yield_fixture -def model(monkeypatch, app): - """This fixture provides a modified version of the app's model that tracks - all created items and deletes them at the end of the test. - - Any tests that directly or indirectly interact with the database should use - this to ensure that resources are properly cleaned up. - - Monkeypatch is provided by pytest and used to patch the model's create - method. - - The app fixture is needed to provide the configuration and context needed - to get the proper model object. - """ - model = bookshelf.get_model() - - # Ensure no books exist before running. This typically helps if tests - # somehow left the database in a bad state. - delete_all_books(model) - - yield model - - # Delete all books that we created during tests. - delete_all_books(model) - - -# The backend data stores can sometimes be flaky. It's useful to retry this -# a few times before giving up. -@retry( - stop_max_attempt_number=3, - wait_exponential_multiplier=100, - wait_exponential_max=2000) -def delete_all_books(model): - while True: - books, _ = model.list(limit=50) - if not books: - break - for book in books: - model.delete(book['id']) - - -def flaky_filter(info, *args): - """Used by flaky to determine when to re-run a test case.""" - _, e, _ = info - return isinstance(e, (ServiceUnavailable, HttpAccessTokenRefreshError)) diff --git a/optional-kubernetes-engine/tests/test_auth.py b/optional-kubernetes-engine/tests/test_auth.py deleted file mode 100644 index 491de9e0..00000000 --- a/optional-kubernetes-engine/tests/test_auth.py +++ /dev/null @@ -1,133 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import contextlib - -import bookshelf -from conftest import flaky_filter -from flaky import flaky -import mock -from oauth2client.client import OAuth2Credentials -import pytest - - -@pytest.fixture -def client_with_credentials(app): - """This fixture provides a Flask app test client that has a session - pre-configured with use credentials.""" - credentials = OAuth2Credentials( - 'access_token', - 'client_id', - 'client_secret', - 'refresh_token', - '3600', - None, - 'Test', - id_token={'sub': '123', 'email': 'user@example.com'}, - scopes=('email', 'profile')) - - @contextlib.contextmanager - def inner(): - with app.test_client() as client: - with client.session_transaction() as session: - session['profile'] = {'email': 'abc@example.com', 'name': 'Test User'} - session['google_oauth2_credentials'] = credentials.to_json() - yield client - - return inner - - -# Mark all test cases in this class as flaky, so that if errors occur they -# can be retried. This is useful when databases are temporarily unavailable. -@flaky(rerun_filter=flaky_filter) -# Tell pytest to use both the app and model fixtures for all test cases. -# This ensures that configuration is properly applied and that all database -# resources created during tests are cleaned up. These fixtures are defined -# in conftest.py -@pytest.mark.usefixtures('app', 'model') -class TestAuth(object): - def test_not_logged_in(self, app): - with app.test_client() as c: - rv = c.get('/books/') - - assert rv.status == '200 OK' - body = rv.data.decode('utf-8') - assert 'Login' in body - - def test_logged_in(self, client_with_credentials): - with client_with_credentials() as c: - rv = c.get('/books/') - - assert rv.status == '200 OK' - body = rv.data.decode('utf-8') - assert 'Test User' in body - - def test_add_anonymous(self, app): - data = { - 'title': 'Test Book', - } - - with app.test_client() as c: - rv = c.post('/books/add', data=data, follow_redirects=True) - - assert rv.status == '200 OK' - body = rv.data.decode('utf-8') - assert 'Test Book' in body - assert 'Added by Anonymous' in body - - def test_add_logged_in(self, client_with_credentials): - data = { - 'title': 'Test Book', - } - - with client_with_credentials() as c: - rv = c.post('/books/add', data=data, follow_redirects=True) - - assert rv.status == '200 OK' - body = rv.data.decode('utf-8') - assert 'Test Book' in body - assert 'Added by Test User' in body - - def test_mine(self, model, client_with_credentials): - # Create two books, one created by the logged in user and one - # created by another user. - model.create({ - 'title': 'Book 1', - 'createdById': 'abc@example.com' - }) - - model.create({ - 'title': 'Book 2', - 'createdById': 'def@example.com' - }) - - # Check the "My Books" page and make sure only one of the books - # appears. - with client_with_credentials() as c: - rv = c.get('/books/mine') - - assert rv.status == '200 OK' - body = rv.data.decode('utf-8') - assert 'Book 1' in body - assert 'Book 2' not in body - - @mock.patch("httplib2.Http") - def test_request_user_info(self, HttpMock): - httpObj = mock.MagicMock() - responseMock = mock.MagicMock(status=200) - httpObj.request = mock.MagicMock( - return_value=(responseMock, b'{"name": "bill"}')) - HttpMock.return_value = httpObj - credentials = mock.MagicMock() - bookshelf._request_user_info(credentials) diff --git a/optional-kubernetes-engine/tests/test_crud.py b/optional-kubernetes-engine/tests/test_crud.py deleted file mode 100644 index c0d2f40f..00000000 --- a/optional-kubernetes-engine/tests/test_crud.py +++ /dev/null @@ -1,88 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import re - -from conftest import flaky_filter -from flaky import flaky -import pytest - - -# Mark all test cases in this class as flaky, so that if errors occur they -# can be retried. This is useful when databases are temporarily unavailable. -@flaky(rerun_filter=flaky_filter) -# Tell pytest to use both the app and model fixtures for all test cases. -# This ensures that configuration is properly applied and that all database -# resources created during tests are cleaned up. These fixtures are defined -# in conftest.py -@pytest.mark.usefixtures('app', 'model') -class TestCrudActions(object): - - def test_list(self, app, model): - for i in range(1, 12): - model.create({'title': u'Book {0}'.format(i)}) - - with app.test_client() as c: - rv = c.get('/books/') - - assert rv.status == '200 OK' - - body = rv.data.decode('utf-8') - assert 'Book 1' in body, "Should show books" - assert len(re.findall('

Book', body)) == 10, ( - "Should not show more than 10 books") - assert 'More' in body, "Should have more than one page" - - def test_add(self, app): - data = { - 'title': 'Test Book', - 'author': 'Test Author', - 'publishedDate': 'Test Date Published', - 'description': 'Test Description' - } - - with app.test_client() as c: - rv = c.post('/books/add', data=data, follow_redirects=True) - - assert rv.status == '200 OK' - body = rv.data.decode('utf-8') - assert 'Test Book' in body - assert 'Test Author' in body - assert 'Test Date Published' in body - assert 'Test Description' in body - - def test_edit(self, app, model): - existing = model.create({'title': "Temp Title"}) - - with app.test_client() as c: - rv = c.post( - '/books/%s/edit' % existing['id'], - data={'title': 'Updated Title'}, - follow_redirects=True) - - assert rv.status == '200 OK' - body = rv.data.decode('utf-8') - assert 'Updated Title' in body - assert 'Temp Title' not in body - - def test_delete(self, app, model): - existing = model.create({'title': "Temp Title"}) - - with app.test_client() as c: - rv = c.get( - '/books/%s/delete' % existing['id'], - follow_redirects=True) - - assert rv.status == '200 OK' - assert not model.read(existing['id']) diff --git a/optional-kubernetes-engine/tests/test_end_to_end.py b/optional-kubernetes-engine/tests/test_end_to_end.py deleted file mode 100644 index df80e8a0..00000000 --- a/optional-kubernetes-engine/tests/test_end_to_end.py +++ /dev/null @@ -1,80 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -import re - -from bs4 import BeautifulSoup -import pytest -import requests -from retrying import retry - - -@pytest.mark.e2e -def test_end_to_end(): - """Tests designed to be run against live environments. - - Unlike the integration tests in the other packages, these tests are - designed to be run against fully-functional live environments. - - To run locally, start both main.py and psq_worker main.books_queue and - run this file. - - It can be run against a live environment by setting the E2E_URL - environment variables before running the tests: - - E2E_URL=http://your-app-id.appspot.com \ - nosetests tests/test_end_to_end.py - """ - - base_url = os.environ.get('E2E_URL', 'http://localhost:8080') - - book_data = { - 'title': 'a confederacy of dunces', - } - - response = requests.post(base_url + '/books/add', data=book_data) - - # There was a 302, so get the book's URL from the redirect. - book_url = response.request.url - book_id = book_url.rsplit('/', 1).pop() - - # Use retry because it will take some indeterminate time for the pub/sub - # message to be processed. - @retry(wait_exponential_multiplier=5000, stop_max_attempt_number=12) - def check_for_updated_data(): - # Check that the book's information was updated. - response = requests.get(book_url) - assert response.status_code == 200 - - soup = BeautifulSoup(response.text, 'html.parser') - - title = soup.find('h4', 'book-title').contents[0].strip() - assert re.search(r'A Confederacy of Dunces', title, re.I) - - author = soup.find('h5', 'book-author').string - assert re.search(r'John Kennedy Toole', author, re.I) - - description = soup.find('p', 'book-description').string - assert re.search(r'Ignatius', description, re.I) - - image_src = soup.find('img', 'book-image')['src'] - image = requests.get(image_src) - assert image.status_code == 200 - - try: - check_for_updated_data() - finally: - # Delete the book we created. - requests.get(base_url + '/books/{}/delete'.format(book_id)) diff --git a/optional-kubernetes-engine/tests/test_storage.py b/optional-kubernetes-engine/tests/test_storage.py deleted file mode 100644 index 531c8827..00000000 --- a/optional-kubernetes-engine/tests/test_storage.py +++ /dev/null @@ -1,70 +0,0 @@ -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import re - -from conftest import flaky_filter -from flaky import flaky -import httplib2 -import pytest -from six import BytesIO - - -# Mark all test cases in this class as flaky, so that if errors occur they -# can be retried. This is useful when databases are temporarily unavailable. -@flaky(rerun_filter=flaky_filter) -# Tell pytest to use both the app and model fixtures for all test cases. -# This ensures that configuration is properly applied and that all database -# resources created during tests are cleaned up. These fixtures are defined -# in conftest.py -@pytest.mark.usefixtures('app', 'model') -class TestStorage(object): - - def test_upload_image(self, app): - data = { - 'title': 'Test Book', - 'author': 'Test Author', - 'publishedDate': 'Test Date Published', - 'description': 'Test Description', - 'image': (BytesIO(b'hello world'), 'hello.jpg') - } - - with app.test_client() as c: - rv = c.post('/books/add', data=data, follow_redirects=True) - - assert rv.status == '200 OK' - body = rv.data.decode('utf-8') - - img_tag = re.search(''), - '1337h4x0r.php') - } - - with app.test_client() as c: - rv = c.post('/books/add', data=data, follow_redirects=True) - - # check we weren't pwned - assert rv.status == '400 BAD REQUEST' diff --git a/optional-kubernetes-engine/tox.ini b/optional-kubernetes-engine/tox.ini deleted file mode 100644 index 8a04f077..00000000 --- a/optional-kubernetes-engine/tox.ini +++ /dev/null @@ -1,30 +0,0 @@ -[tox] -skipsdist = True -envlist = lint,py27,py36 - -[testenv] -deps = - -rrequirements.txt - -rrequirements-dev.txt -commands = - py.test --cov=bookshelf --no-success-flaky-report -m "not e2e" {posargs: tests} -passenv = GOOGLE_APPLICATION_CREDENTIALS DATASTORE_HOST E2E_URL -setenv = PYTHONPATH={toxinidir} - - -[testenv:py27-e2e] -basepython = python2.7 -commands = - py.test --no-success-flaky-report -m "e2e" {posargs: tests} - -[testenv:py36-e2e] -basepython = python3.6 -commands = - py.test --no-success-flaky-report -m "e2e" {posargs: tests} - -[testenv:lint] -deps = - flake8 - flake8-import-order -commands = - flake8 --import-order-style=google bookshelf tests From 9485a86d3fff29d5178679fb3c103ca8167ede18 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Sat, 23 Nov 2019 15:52:03 -0800 Subject: [PATCH 095/395] fixes tests and linter, removes travis and adds Kokoro --- .gitignore | 1 + .kokoro/common.cfg | 8 +++++-- .kokoro/presubmit.cfg | 0 .kokoro/system_tests.cfg | 10 --------- .kokoro/system_tests.sh | 1 + .travis.yml | 10 --------- README.md | 2 -- authenticating-users/main.py | 2 +- background/app/main.py | 3 ++- background/app/main_test.py | 33 +++++++++++----------------- background/function/main.py | 4 ++-- background/function/main_test.py | 15 +------------ conftest.py | 36 ------------------------------- encrypt-secrets.sh | 4 +--- noxfile.py | 12 ++--------- secrets.tar.enc | Bin 20512 -> 5152 bytes sessions/main.py | 4 ++-- sessions/main_test.py | 12 +---------- 18 files changed, 33 insertions(+), 124 deletions(-) create mode 100644 .kokoro/presubmit.cfg delete mode 100644 .travis.yml delete mode 100644 conftest.py diff --git a/.gitignore b/.gitignore index 6f2c88b4..fe877062 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ htmlcov/ nosetests.xml coverage.xml *,cover +sponge_log.xml *.log diff --git a/.kokoro/common.cfg b/.kokoro/common.cfg index f7909506..c55b58f2 100644 --- a/.kokoro/common.cfg +++ b/.kokoro/common.cfg @@ -3,6 +3,9 @@ # Download trampoline resources. These will be in ${KOKORO_GFILE_DIR} gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" +# Download secrets from Cloud Storage. +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/getting-started-python" + # All builds use the trampoline script to run in docker. build_file: "getting-started-python/.kokoro/trampoline.sh" @@ -12,7 +15,8 @@ env_vars: { value: "gcr.io/cloud-devrel-kokoro-resources/python@sha256:4b6ba8c199e96248980db4538065cddeea594138b9b9fb2d0388603922087747" } +# Tell the trampoline which build file to use. env_vars: { - key: "FIRESTORE_PROJECT_ID" - value: "getting-started-python-tests" + key: "TRAMPOLINE_BUILD_FILE" + value: "github/getting-started-python/.kokoro/system_tests.sh" } diff --git a/.kokoro/presubmit.cfg b/.kokoro/presubmit.cfg new file mode 100644 index 00000000..e69de29b diff --git a/.kokoro/system_tests.cfg b/.kokoro/system_tests.cfg index a412a3c7..e69de29b 100644 --- a/.kokoro/system_tests.cfg +++ b/.kokoro/system_tests.cfg @@ -1,10 +0,0 @@ -# Format: //devtools/kokoro/config/proto/build.proto - -# Download secrets from Cloud Storage. -gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/getting-started-python" - -# Tell the trampoline which build file to use. -env_vars: { - key: "TRAMPOLINE_BUILD_FILE" - value: "github/getting-started-python/.kokoro/system_tests.sh" -} diff --git a/.kokoro/system_tests.sh b/.kokoro/system_tests.sh index 3ece8473..be9f8eab 100755 --- a/.kokoro/system_tests.sh +++ b/.kokoro/system_tests.sh @@ -28,4 +28,5 @@ SECRETS_PASSWORD=$(cat "${KOKORO_GFILE_DIR}/secrets-password.txt") export GOOGLE_APPLICATION_CREDENTIALS="$(pwd)/service-account.json" # Run tests +nox -s lint nox -s run_tests diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b3bf7316..00000000 --- a/.travis.yml +++ /dev/null @@ -1,10 +0,0 @@ -sudo: false -language: python -env: - global: - - GOOGLE_APPLICATION_CREDENTIALS=$TRAVIS_BUILD_DIR/service-account.json - - secure: S/A6XENZTy+py6kQxUX5EWgNJv3NGRaSTo4LdWVaZ7qKww1+akeMNHKvPXjp398UtsXkCWCpdy5HeLDYdmzX83uuFoPD1S/z7ilVpo/q4sRxDQmnw4diPHSpx32+XRyKpMpbjgJTMhocqZGcJFI9jxZ4QAHK3rul8ulvTojCyQf7OrePhgfBB72f/ZHho73VqmsngGyWNI6MtGGrBzMTtXlAOt8BgDhYXzPjhEWkE+OKAF9sMJqFmxUXyKkfTBfcGHDYcAOlNtvVdZGRC9NmzRvZxC5T7lQv/WdfGBAT3Zr4NSobKHK6DAv9Q7QvVgHIs7HRomgpHQ9IKnwyhR4RTYAnTtZyHe0/fUlPUXC8+Pkk634eRgYuVNMH8Cf7eRXcLRGF6wgBe9nHnx/o0w/kLbEyirQo3kVIM4Y6yfR5MhZ0Nb4oRq5tuXzqN1B5ZFyfmHhkwsJoeL5wgIGcwOxkoCTH8HqvPfW+SJuUpQC/sB89ixfTfUXYyMJxgGTOdg+1wD4IAfivGyWiUBcx06glfC3Sn+75oflgMz4M8y6zafXTzux755U5pEMa8Gw+5BqpDlkRliZwdAJ6UwHo/XHeJ2rgiTddEgSZYN0CR5XQFcEc69lauUEPkeE5fPubSgcH28xvbhWq7WJ1qdi3Wdw8RaCLOys/B6vwL8GhSJP+ROg= -before_install: -- ./decrypt-secrets.sh "$SECRETS_PASSWORD" -script: -- nox --session lint travis diff --git a/README.md b/README.md index 85d0c8f8..85082b86 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,5 @@ # Getting started with Python on Google Cloud Platform -[![Build Status](https://travis-ci.org/GoogleCloudPlatform/getting-started-python.svg)](https://travis-ci.org/GoogleCloudPlatform/getting-started-python) - This repository is the complete sample code for the [Python Getting Started on Google Cloud Platform](http://cloud.google.com/python) tutorials. Please refer to the tutorials for instructions on configuring, running, and deploying these samples. The code for the samples is contained in individual folders in this repository. diff --git a/authenticating-users/main.py b/authenticating-users/main.py index b1a37722..9f4b9504 100644 --- a/authenticating-users/main.py +++ b/authenticating-users/main.py @@ -53,7 +53,7 @@ def get_metadata(item_name): path += item_name response = requests.get( '{}{}'.format(endpoint, path), - headers = {'Metadata-Flavor': 'Google'} + headers={'Metadata-Flavor': 'Google'} ) metadata = response.text return metadata diff --git a/background/app/main.py b/background/app/main.py index efb432bd..487e10a4 100644 --- a/background/app/main.py +++ b/background/app/main.py @@ -20,10 +20,11 @@ import json import os +from flask import Flask, redirect, render_template, request from google.cloud import firestore from google.cloud import pubsub -from flask import Flask, render_template, redirect, request + app = Flask(__name__) # Get client objects to reuse over multiple invocations diff --git a/background/app/main_test.py b/background/app/main_test.py index cef9c154..0832fb3b 100644 --- a/background/app/main_test.py +++ b/background/app/main_test.py @@ -13,27 +13,23 @@ # limitations under the License. import os -import pytest import uuid +import google.auth from google.cloud import firestore from google.cloud import pubsub +import main +import pytest -os.environ['GOOGLE_CLOUD_PROJECT'] = os.environ['FIRESTORE_PROJECT_ID'] -os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = os.path.realpath( - os.path.join( - os.path.dirname(__file__), - '..', - '..', - 'firestore-service-account.json' - ) -) +credentials, project_id = google.auth.default() +os.environ['GOOGLE_CLOUD_PROJECT'] = project_id SUBSCRIPTION_NAME = 'projects/{}/subscriptions/{}'.format( - os.getenv('FIRESTORE_PROJECT_ID'), 'test-' + str(uuid.uuid4()) + project_id, 'test-' + str(uuid.uuid4()) +) +TOPIC_NAME = 'projects/{}/topics/{}'.format( + project_id, 'translate' ) - -import main @pytest.yield_fixture @@ -68,15 +64,12 @@ def publisher(): @pytest.yield_fixture def subscriber(): subscriber = pubsub.SubscriberClient() - topic_name = 'projects/{}/topics/{}'.format( - os.getenv('FIRESTORE_PROJECT_ID'), 'translate' - ) - subscription = subscriber.create_subscription( - SUBSCRIPTION_NAME, topic_name + subscriber.create_subscription( + SUBSCRIPTION_NAME, TOPIC_NAME ) yield subscriber subscriber.delete_subscription(SUBSCRIPTION_NAME) - + def test_index(db, publisher): main.app.testing = True @@ -104,7 +97,7 @@ def test_translate(db, publisher, subscriber): assert r.status_code < 400 - response = subscriber.pull(SUBSCRIPTION_NAME, 1, timeout=2.0) + response = subscriber.pull(SUBSCRIPTION_NAME, 1, timeout=10.0) assert len(response.received_messages) == 1 assert b'This is a test' in response.received_messages[0].message.data assert b'fr' in response.received_messages[0].message.data diff --git a/background/function/main.py b/background/function/main.py index 4a4292c5..e2660aae 100644 --- a/background/function/main.py +++ b/background/function/main.py @@ -26,7 +26,6 @@ import base64 import hashlib import json -import os from google.cloud import firestore from google.cloud import translate @@ -72,6 +71,7 @@ def document_name(message): # Note that document IDs should not contain the '/' character name = base64.b64encode(hashed, altchars=b'+-').decode('utf-8') + return name @firestore.transactional @@ -81,7 +81,7 @@ def update_database(transaction, message): try: doc_ref.get(transaction=transaction) - except NotFound: + except firestore.NotFound: return # Don't replace an existing translation transaction.set(doc_ref, message) diff --git a/background/function/main_test.py b/background/function/main_test.py index 823ddb12..aa6d3ddb 100644 --- a/background/function/main_test.py +++ b/background/function/main_test.py @@ -14,21 +14,8 @@ import base64 import json -import os -import pytest from google.cloud import firestore - -os.environ['GOOGLE_CLOUD_PROJECT'] = os.environ['FIRESTORE_PROJECT_ID'] -os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = os.path.realpath( - os.path.join( - os.path.dirname(__file__), - '..', - '..', - 'firestore-service-account.json' - ) -) - import main @@ -41,7 +28,7 @@ def clear_collection(collection): def test_invocations(): - db = firestore.Client(project=os.environ['FIRESTORE_PROJECT_ID']) + db = firestore.Client() main.db = db translations = db.collection('translations') diff --git a/conftest.py b/conftest.py deleted file mode 100644 index b6078c07..00000000 --- a/conftest.py +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright 2016 Google Inc. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os - -import mock -import pytest - - -@pytest.fixture -def api_client_inject_project_id(): - """Patches all googleapiclient requests to replace 'YOUR_PROJECT_ID' with - the project ID.""" - import googleapiclient.http - - old_execute = googleapiclient.http.HttpRequest.execute - - def new_execute(self, http=None, num_retries=0): - self.uri = self.uri.replace('YOUR_PROJECT_ID', PROJECT) - return old_execute(self, http=http, num_retries=num_retries) - - with mock.patch( - 'googleapiclient.http.HttpRequest.execute', - new=new_execute): - yield diff --git a/encrypt-secrets.sh b/encrypt-secrets.sh index 8da7a394..5faf4a41 100755 --- a/encrypt-secrets.sh +++ b/encrypt-secrets.sh @@ -17,8 +17,6 @@ read -s -p "Enter password for encryption: " password echo -tar cvf secrets.tar {service-account.json,firestore-service-account.json,config.py} +tar cvf secrets.tar service-account.json openssl aes-256-cbc -k "$password" -in secrets.tar -out secrets.tar.enc rm secrets.tar - -travis encrypt "secrets_password=$password" --add diff --git a/noxfile.py b/noxfile.py index 858c8c91..6b81a427 100644 --- a/noxfile.py +++ b/noxfile.py @@ -9,6 +9,7 @@ DIRS = [ # Hello world doesn't have system tests, just a lint test which will be # covered by the global lint here. + 'authenticating-users', 'background/app', 'background/function', 'sessions', @@ -49,7 +50,7 @@ def run_test(session, dir): 'pytest', *(PYTEST_COMMON_ARGS + session.posargs), # Pytest will return 5 when no tests are collected. This can happen - # on travis where slow and flaky tests are excluded. + # when slow and flaky tests are excluded. # See http://doc.pytest.org/en/latest/_modules/_pytest/main.html success_codes=[0, 5]) @@ -59,12 +60,3 @@ def run_test(session, dir): def run_tests(session, dir=None): """Run all tests for all directories (slow!)""" run_test(session, dir) - - -@nox.session -@nox.parametrize('dir', DIRS) -def travis(session, dir=None): - """On travis, only run the py3.4 and cloudsql tests.""" - run_tests( - session, - dir=dir) diff --git a/secrets.tar.enc b/secrets.tar.enc index 5b505a77aab371838d308c9d99f0a78279eed983..25ccbe611303b3035e5e4ca3878bb381b269edb7 100644 GIT binary patch literal 5152 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 OqrA8;>ND5Y`(Q)it`0Z= literal 20512 zcmV(fK>EK^VQh3|WM5w##IhIrDex9H9M5#?O<(UqV3+oSt37Jn0*CAD&J}ui8CqLe zC)0nt&D|->e4MKQ#pb)sN^oiXAhJ??LqX5VQu)9(WflQ|%2-u%*=y5fmch$KxC6Sr z6fjPiJ){S4XZwDMO$L0YP>M@j<@5)v&RdWQ8TWiO0_S#LIokLuW)+sdv+1L; z!CBEOtUchU<`o=}l$sw(H5>V98^|_16nCW96~9Eb+6m!=sYK+n%-E0tzR7NN(fp5o z+9^s8sgAna0M{2-2J6zUT&*HUIZ@1#180x)-T4s)dX`m#W87%ra(8|;d+m$p6I2M^)S3ypQ?hYXZLR!5@IflC*hyAPkX1Y zSZj13PMb}L*m!mj2aeOPWp?Xq4qt&^J;+r!6rh!amsl=PlDMy=?QVw>Syt!H@mr-{ zxvA37=&vdZ7+G+Hxgjjf&nn8u>`KqMEDrEVQxEeDmePw_n2AF-I6aH+%47TX4iy9E zfvoA$u2eCf1YSDH)38Yos92UY-eu^C1bnv>oK?odL7wTMokFdJM$1{lHDZ9gUw3vk zP1ab`nROA=ec(@vAwr5FGuv%l*g`PyESVHm_*o4N;UG;O>~2AJORkdl_Mg1Y&!uYf zI5<=%_t-6ulA4A+6{7Hn0UVTO&?bY+hL54 zZ%a|V3~fidiieYtb=G71@jbrb2n|;V*^)l0Lc#1RAXq&=r;R`=329`vGwSg_ zlmpAWqb7o{)9uvWcVbadzi(O3%Wu{34v)1=wMb+?T+HPuWriAM$+Ve(pxlu4eRb)s zN|x4W=~FCfTTdN9m+ZGsG4Vdm!A*zDrRE&zqf7PME5DcOEW7oBWd01<8GP9?vCPYU zHL3vgw9O}bCNs&n7n^(=+^_&|(X2^trkFI})OYbJ4rRL4lX|w;mA({%grlFY0!_Wh zO`QIXuSQC;Y*?Zw&U}KZO*dQvug7NrFK^pGg#b+%L8#2b`$t&9O=T)Sn8=hv4F^ot z4Gvg-;K(HOy2pSxwzzmY$<${}Gi5qeaWFG;14Q$I0H_yRYx^u@S!KwavZAT}Apu?z^G-C#8llmVM--+r>zQ|)Jo zI^gr1UcpW8Y9|ixR0x1Sqh~WlCOZy-Z2f!~1lg+LElH^+6`r(J4~%EeY+ub+3eXM>C8h1e$-hu15ns%OR6kXZ zrAUgWWfJ&6G$y_4py`ma`raL6^6+E=uFl|*&|2XB6~yrN?g>T+M%Py1pMIi?iCktIus zc0!{94+H&n(&6O2xD?Mjxopg_a#EVROpO`oI`0V%I{0`@MymcKGh6w}g-30)Kjgrt zI$8&}{bgmr#g9~~4o2;Em0Cs@YjDn(R)fVQuUD(%H;WjYg_ZL!$38GaDoogODPLVO zn2pp?GVpHXX79XgR-64!EOM39#qjuk=sQG+02pAs*O|YDh3IG}GZ71$<%hSEX z)(jyEA#I!;0+tD1=_u9Y@!I794-UO^L(t@7W8kur%0qN<)D{g~CNCBf&LFF@vIAeg z=Ym&6Hq>c8XMY_wsiR+MhuP*^&Dji}RZ-5s?7R2PTDWi*0@6klv_Qp++Et-S1Juwg zTP4jjI}<5Vn9g7aXM4zejoDdJy>J;vY7_0}9aBpKe=AgTh&iGhnYJmXY+*RrVH>}c zrtQv2bk~cofu5UuhcPl0uzGCmtkRB~l(sg@KhL8HVmCAnXAc7BBj1W1UutbSq+Zky zc=iN}MGd=hzA!7`~~gdPr99)cbF+Of;VvqE9CXC0-DbFDD3bW-9wKHS5L zC1BkJEyJZh;1l$nwHwtvqXY5busC0CUtf5FcW8~BzM&tTxn^&z=kRt&zr0+n@~KzI zmYTYS5wN)`nFpjHr2E=TRPuqY^Gy{mb;w|v;u%!gfvQYwedC~@sBjRCqew97cG^-n zAdodWiW;C8Y>e|YJOsLx^0eY`;M|q21bFuXTNOVYz|*C=YY*}j4x!MfhWR=6N^z8` zTd2s$5(<2Wq~qvBAnyLn7#{=fg_v1wCN$0q zZ671l(YnVnC0EZVZb~fPA({V8KWIy!rJ4;gr^yev0!)~5!$Vv>DvF&@Su+XYH=50C zo%4>0*!b1TSx~OiJhP!qFmmzPC_Ke{EP+P4U(#{HQh3!2rON6<$xIz7>@p^hi(_4~ zw#@9PZ&u+PKNjR?n!=Znb&;VF?|vHYiJvHqSivp7gBA?DdOz=klN1g3V=cSTLj)3- zHuYoMe!UcO@V|HQ2osq1cTmKU7v-{C+3lVkE0zsJk^Kd4dOF{(7%V@}D3u|+(W#@h zO5ddI`V~nxa?vSLyKm8U99yY!#u+-%e_nyMfHM$$EQH#HOR03q*{M5p)o&xF!o0C< z8#rZR;cg4YL1MxhvSqg)H{0z0nC^Bqv5Y=z%IqM1KU&@zm2Rms+IUe}S`6W0fU`zXOg#<*)iPJ0 ztPYz2v0^|BJgJ46cy(hBvQ5fMWhQZY7Nnn-IZsSPUF)+y>7#U$>_-*+N$kE)KIFv8 zX;D_EK0er6h`&EZ+57KCdw%AFq_U~&g_eGLQo4iWFh^*8W88ix?;Tl;Mzp!d;5}ro zct!*t2(cVk=!y>4^0CTB2B1MS%FKG-tmvy>vHYz0`=Br*?!u?yq)y*2!b8pBA2y-0 z_LdK8hYEUSeOGq~Qt4vC=6?eP{33aWXx{xD!P%xIUcCd%)7Qo5}-tJ?=WLPg;Lh$CGUH|OIoUqT44vbMqq)CAg&WjyG?3-ejx zDd0QJP02Qo|6;3WG*8}Q5cZFw-`29LyWkr65;7TnF*1D4fzNJqV z?t9)?`Q4IV({6A!&4}jSFsz=Pkk=nOvO4>) zc~GiE&jij_a-<=*mBnVvdKwinOcRb{K!?trE{fgx%<}{1lFob=q*L34-@`kCYy>IU zPe{qELlYtE)`y;PikGpUesY=0vd9nTdFP%oCai}9m<1UDZEo-)#|H%5dXimK49m-K z4JtYGCb&f;+@%wT!u+ZJ&_At%ep)mjmhZpEV3@><-9Hy+M^i!5@vD3ARrz*ND@=;2cfJmx zp73J`g4%JEZ<>~+Nr6x5!V4=)^^L#irbd@xHMTb@(5#mvA(O6sWSBi(Lbzl^1`)hX zs%2R>;O#{F8Cd8;MUJs?aWFOOzcZuz;ES1{Q50ov?QtuPtCRuqu zXGo@&9`(=levCqgm@s68MbcP*RkC;nA}@P_<9umDNxGjd^VUw1(uUNV7?j}+t!`wHX(dTYq+@{zZ`UShY4`v* ztV>5oA*Adm#KG&nY}rv$WJTk{OoSXEvvrg!QdI79`;zyCM)&z2j$+S3x&zGJ1Dfj& z4_e*CITtmMP_>B=gRfdjm@&dlrVjHbEx?&d0sh9t2kpih9*a2d@wMn#JFc5&>t74v zoAdaussOQP#|AU_{-feGF7~_FotXFVxK0a7=P`#zC5ca2JFYO?P2yPD5#K1mM0nmm z8xyPkk4K%ECB)HhQ#&^5nBsRTo5H^P+Xiqi+v-HdU81obX zcfV%~9!Q}U!qT`eE*g;7gfV(UN*3SGY>7+@H=ni{gtI$FqdnGv&du+H9HF)I>J~;T zks>OG#|}+!2u`+#ZdC-yGL=_&Rj~~OP_zx|YZNN^Ue_h_*LTE!sv2)9V1kZ1dSPjt z3W`ehIr{J#u$6Y?<>+{5QyeW!&Zp22Nb?YYS{iUau4gytHmFbkbv|B4^L;dO#!x=< z23|iq{U{RQu)N92^#8=^gJ0i?9vlh=T6I2PP{s4B=Z-GOJQL6KP*%3I30f@COXq>{a~AliG(EMrT<5o z-pf0QKP_zHXe))AFc|s36`xd61o-wxL&&WsA7OVsTa0hDoSLpHM@Ed~rfD}blvC|f zo$TUx$5m^pxUEzuS4LHQ;ysU3YdIYZmW|vdgf=XpKrAoO1+iY;lk*)sL|935WWlxP zB(Myfc27yg^39&~Lpe+@lN1COeN(6L$>pZR%9;lfskJkI=L#p9Os_FFN}mp9Zyw`? zxSbuME$dc>&E>*_RC}}+kK55WGZ!znrFki+Vj@?izrreR0khff>;5Xb@WX&5agl48 z2ZjCJ&j2*DotHU(f1hhFL)0lwT$))UC5SG^AtQJeR z0TCX!c0a$UQ5uai3(V{TIWS%(qgQQluN^I{RQ(^2iWudzBGIH*VD+-4r&w8+h;uD^ zHZCK%fr3WK^IF=ayA8mvv>+!Wc_UvH4dol2op(|R(1V2f4!WdD{Aq+U81v<)M#qO9 zI{k4?GP-?hN1GV9JAT@bcjYva>h9V{)Y;A{=N}_4X<~fl4p%%?ga>y6zN=a%Ddwla z)17$KP;e8~eE|l;QitB9#iEanJS$AGfru+7x+#$l9LNemnthhNUwWXgor=6$nt@-F zNoYe09W-~YHkRCsB{I%Pry}E`%Ho+n5D!W zA=zM*76#g29wTx-+!4+548aPeN)cqxn(uIT@g4FkZIY7iD~{&4)M33N0gae@u7QYz zs`s69N=w?S($RWXZdEO%{$h4HBSw|N26c$b(BZ!h zSQPsru+q1#W9#Sslt@BwiWY~wla`*55y>U0snK{UsPO9;*te)mS_Wi@uBc3D5%Bg7 z)=Dpg*k!U|MI9i->JLbW|EhrA3i1}l5A=Ks2!j6^pFV?0>|=VARz{-U_Z_DxStG~m zUBO$2u{bg8kK(KT%h&mNHvdnbCn(J@U*PuC!a_OQ`VMP}_@a}88yMiGOLf^P6@A$GvCpJUK zxYq~w(pdCIOvd-`T1XP*1!N&xdT~hrrrH1w@lqkKJbF~T%(OhX7*nd*pu+B}zF*oC zJMDaRe@P@)q{gf`v=9NRqORT7)#?y6vsELp%1%||%I+Wwfg|>#yv4)V9AUWtcVoxG z-GY9U#fDhE5mJYi0OA=rP{=n}4G4BOCxcs+saY;%fGrHdi@Mu)6P)?)F-I_AWLy}d zzHBfUFXT7x+qJT8iSGhCF^(fCu{2_KcN8Knl95nY{soN@w2ce_@wi|gU0D4J1M*tCJTEPT=^EpB{#V$Ajv^zU~A?wiB2uZ%)n^H|i^Y4NP=wvf( zdq-O=(!P5~DiEIwn%WIreV<15vy7)!`UV2xC{jg5X=r%QpwfLxz`@r1RC>@EGSL9w>u z1r>^0a0>A5^4Grkh~A^3AUe)^?RVKB6LOx zahR4}N|TTIYl$pXX3hp&->HlWM;lv{MYdUY|7z%NN! z-c0w&_s2aPP1W_r-Mb0Xlk++tj|=fVK62i6(LDEPF749h#j;>E^IL9-O{gM=jQM2Z zQs^$Dz@3DRKJ?It>yToK>ri1b%Zapws!sfr@Jl7tGBwfXGa5@k6Re}jTMW}=z@I|7 zDR?Py(mz?XO|$=WI$raFw;!&Q9$X9=JvQKA=uyN|BV*k<3+A2$XVKOe&|f!ZJ}bsF zRT6u~|2h3w@lC)lGGWc9T-R8k4ry3QpzEnFdcd0Ix7fI!%GHTi=i>THEg0^b!qeRmTsxtv3(X%dFQ(Zp^zCKmWZ!LE4%jUaY z`1jakBe>#*V+k%$h{i}91_Cw<80Kzzy=@`p75{UfV$iTtGzwRXyVec?eo9EDQBfKh zYHUDYANvXs-RB{`_8(wN2PibqbFGDpV;r|ZPB#}Tv*4{_b@LkvDw{*(UKS4*0eQcl zJB}JP;;~0@N~|`;I9($ldV!TW@TGFj#23pE`n5fmRpS-yXPGB&^T*TooBB&>_W@7e zxtF-U&Ek)4BY_$z&?F*5HPadIk}y;l8wTMHDXo*L>h4ts{12K?`5LDRg&^6+qD%SVful_=;&7R z{;)y*XU34@1kZC>H_S6K*VtG-l_Y3^yWqqH2(n?2K&W{%kO;2PoB?AA-M3h=IKp|3 z8u2P;PUvf4Ayo@bN8EnBlWR?PHUB#8TlH3fB~C`?N?g3op~G_sBq*ex!UI=*cvBA+!d_o%X_bnu3&62fw#n+Ln~ zEad8;opEryM3%XjSy2k_lJ(XddU5BUy%1nRbv#nLJw$5L^*^*oOwX=a(Q~q_Mh~1M zn!f(6Q3(fp6?2x)g2fsx*ZJiV@iOM`J%Lk)_`hP+Uq%&@B*%*@OPZ3WEN|i)zs9m! zrP|d{T)YX(=Vz;SUdZ07gvZV#ED#kW1)Qsz zyq8+ENCgZ0@~pIzQzYaEL`3p`oRd+qwnRuvdwX4pGtZd@k& z7D=Ak>Yu&8(t;DKA?F)S7UKN98|r5skaV!Q*By7XKf>d*Teo3TtiX@ZD#g&#aN9Wp zNZBqi3i$$hB1aU_;b<57XrBo!KeKdg}U-0jE^wX867>vhedHI zzP+PEhtoiI=jM*aG0>orYlP*t+6oQvN=fb%0FacS9PreHxW7d zRm>9$RokF3X{ek@e zeHZ`;{{ghJ(G(SkKGb1c>~3lw+? zcGK*r8b)@4S`Q~*Ug*wxmQ-OIGPtKz#Vcsx(@+4S1cv6>Yj!i3UW77eb&srj^?rGh3S_V~VqJQ}8bV?#Xew%EyTyQ(@@ zrxUy>%%+KVIt_g&m!wHUG^s#nyoizLLRQ#6awMVZb)?+YkOtMOJ0s<4`#?%C(1_F2 zh-$CkGq~p6nmARZpBGJOJWN`kWYB3aqNM~5Jj%#im?@Bnh-;E{oJ+dxLZ{5rNdqFB zykRRz+ISVVgLeh6Hfg*lAX{jcfcQcPIUnTk!u^k=4dOlzt`l;hw&ds_jH_vT)!T_i#YB|M(2(6?=H?+^_2hXk2#zE`a%E!*qXE2gay zT3_oZ%_E#&W!ZjL_$XL%hGUkyF^gD_i|OpejuLV`$cdBW=EM&MBrgdRfsPfzds59m z^aB!34fzP5;*fpR1;^m#pM$1mwN_mCZVU}5C87d#>baP3dyo~V|CyaX8Yg${p>SM< z`2$yquYwMEz}JVey&j-aD=?z+sj}qv9uL0q=0oztS@~3gi=jop9@+K7)Rl;zL{>)1 zff5NG_-wil{9bpG(<6BnY=)UG#6lR|aCCCC3~5nXU~wY)%{8SHF#9p2Yx_eL5A zDMbKLf_|dQYnfS%Z0eR&n+_hnAQk3kZ)xs&-uY3wU5j{N1w0w|T*!?4WeEuFC8#0!6F5n@%_3`YLnT;sKVg=A%uX z_coCA76Zo+KaMydzgxz5WTEmUY6-A^DWvP_*<)p z$MVt=Kjzn37>x-9&gT%6j9vPbZ++!5IJu-}%JX&mT_}5Hw~AE~$fo$*@GIOUae}7s zNxwd{c>o$>iy9am$per++sZroFAinYl6tTj4?>MCFHw$D9_A|h`<{iaL{AbgK;Gzd zOF3l&zuuJoR5=RAvHX3W_MZqsj0nRFfbSVXGOsd!8Y;%>M;qFz9sArbNt7luW*5Wi z%T>YoLorwuG5PPSLBVYk<)*&k!WrXO9OfxG_gPS4#45$WOqjXx{`hj^V7|q+5aQaf zMS%6>j6nE@F2&TNy@|TnRlIAOYBC-fl`ZZ+ObXCOl1-mQ;7fH~mYKAUh9!MH ztG%HWOjrSKw*-}dR;A|~fsN|#?P5x@rpLP)eYMUZ0rXy=N2Pl}e#tQ41d6x>WOwGL z<<%_935DQ*_MV)dh5Cv1>UmHI=>aFJqh zxc6G#|0%&yf_{s_|#w&9ZrVu0a2BCK8j~PFyfusx$smfUZe*RbZymWSmrK;5d zs4%BLKXxfJ%M|o-G0=ycvW9oSB1AN=Vv6J2d-Wo zl#cz6rAF#vU_yXoG-?wwRM)}MW~FC)YAF=+fM?KO$6qNa9-mJ6sDh}+|e zN{T$)4apKEY*b^BpTJuirHe!e2lUcaa#iI;Iv$F;%i8>^z{kXuL|htNZ%t~Je8#6M zc(>Lwv`F}r$8f7mvOr1^$(ar)^fu?vGxdf~ruv{l0BLPI%!SJeS-x3BrS2VN0RLX5 zZD8w8G;vYbVsRs*oH8?^v7}2*Kr>O1w24V_60jUiPDNR_Q=S@D&2SwfKwMj)2Qz3J z<5CEABT-pD090FCCA$bR<1)O^b6_Xk=(X;x%v?pQgw8&8_Zuk_2HMdgb$w@Xl<4gu z>Ms*DzOKB2w)3s;7uP{>w`IG)etW+(&k<&S>bYAXMizR=2JmA;|GR*c=@^Dwv4&4%M#3+~F_oc6XQFY4V)I*`uA9RduuE+lvSzv5wA2+U7d zQl%zg6YRq!DXjH)z!@vcx||S838I^J`?YF9oS>4x2FDf;rKvb%*Xuq}Th9DAe_H*J zxDC`9SnK9`hMFKc^P^Z1RMdtM|< z2x*{QHdLt*ThPAYv#DXi`UtF-t*j3l_PWwDkJM#DiyXQudKb~h)6&CVbYR&*5u6px z(HX>yQvf*03R&R4v(h_2E4UH)vtOiAd-< zXNTmN)}f7hu=#mtyTsdbKz_@pR4lL5B?93goGE8qnZ`Kukgr+01PxSla(LzLqsJ>XfRCJVuk$v^MY;$Ny<*4%5aCYIc z&SEDMC;TVxzaB<1u@q0FiKxcoV@wKEIOGtAupT#Cgu9gr8eq7LEv*~;*^_FUzU9#* zb4g=G0#Y=SggY7}D-nI59Pf3V4-e>6M43?3WPx}O>0GrS3C8_5s zmgK(nOG}5z`40af!Q#Dl6v}j59wOtIae{PRmB((6SQpC+N!~$$wRV=lxi(6Wymya}x9`QTYv(&=117y)C;{0>h0G_e zc!37m*OjW_MlO7V7B7$;V|~J0Ew6Ba-OrC6iRLB7+3`e23f`O-$Gp5aZ%1-ck}Xvhg2@7m?hK3kNDitoC{OhJ{@cSItEsd$Ty& ze295@D=BPza}62By{NX6+?$xqa+g2#Wr%rLBq944O*;F4*`u}U_W9M%>I*cJvsHO~ zL$#1xVW49d4BOJVxS<^1w2u(nRV$S}B=GzKU^!lc(`Bc)hvlADDTBQIXZ=ettZ=Hs zen)-d5QFF^78a2!Lb60}z4WloJ_)vU0h78byn3jYQv|Z6O-U^do-(U1(ozu_heJjI z0|VXWry}4xqLbg>5*#nEwBg1BT`8E;*)Pz>?M_SIs9yNkevQIAT=Ncjn!1Q4zjJQ} z9li-jvSuE&3Y*P1vWvS(lEviHk-qucc0SA)qz=nt(t@sE(3jNbE>mnxirWlfIqxic zJ&&m+rBS+;ElEg?e|C3Si$qV{bazw8zCy$>*zKj3Iq2d1V`L=#iJ@SO?P4)($1E1a*XZ)mcKbL4TK$Q?F_DJt&HaFT z;$65bK^RMqs3eOG+DORZ>!W4&##%l!wTLCb01ht<47F zA6Ogf;`9~#Ai`3Be(F`W_n}~xiQ<=dp`-X?X#p5RKmF~}WsBq!{@jxWc2lk^+sfix zy=^Zp|FqJE-qNs*@Q2p4)@W(2^iNk>YoRoJd&7Iij`9#4PCAs`I3*po{NO_Jk<^O^ zh!~h6LG{q_iXZfKwEimi7_u=VI5rkOOmR;gS11zgc1Vo66xA#fFdEAjmP~2OOD?*q zG2**b`#|VJmj?@7MO#~>;YRw9S-(IMhh}Z+OlPeCD(d7@h-P6?1hEXsxI(iD#z?5r)MiF2RvG8ny%u?_rr0D>!49B)6<6Km}W~f!2S$^ zf4^u$w>YaH*d6GxACdJSsJVR#>{Qt>+qg{uUmIHB{O>3{CBB0Y=42eH7=>RjGK3t4 zn>}9K9QO{x{#@a1WVnSzI;2*G~Ae0lH_Un$9pvMDd=Zmrl@qRr#g^w zubS7beEjq1sxvH+9j&HmR(a`ObJza9-R*q8M3Af@Sw)x{q&tTH_Um53m*m zvNhLA&MJkK#;m#Do6^Sb3b8jb&8A1q>>W@p)wR+SUkyaWWi;Ap`8?S|Oih&-!AyDx z8{0-=m2pxV&7~c=(iSBP^o@DwaKJlIL_PdCyHtPfrHRkzw903+RcI(rgIOf8#&Vek z)O{lBG})kqn*Y1ak6&1w;Hxg8&WWn1q%-`xsZlx(KO`W&H^H4by{d`5io0 z0dbm9QIl`ECa6^sN-CCBFeL^!fXRRsnsNLke39NyM_)cSqsIU?oAhpkU*fJ#I=PU! zo<2J-`JQzrbA-qG!>rBqf=VHPAkq>+>Pq?GIp;4)Xug#CV1fCYKCN=bjrF_}tG4i7CEUHTgvs@AH@4S?22X z-96&T4~k)iq`w^wU9q(|Vs5D5q3Ym8^^>9VYJbV<Xw0WM6PtQRDF%1g7Y z@_&vvlpBD15NG`utGjDM5@W_sapXVm9TPUNfPt*yz~yyMldvOknx*T^C`kc^4-caB zPqM8)TC2XU^xBSyTsTaJnn#F_7k@VCCFq7IBlZ%UBQA$mF^msywlDXDTKXP4QzAE4 z``esk;jIPexs&3Wz3TiEIU|nCZcmC)C-?Q35SyQ2BZ1XSIQ))Vd~_~VNk2?L)RE@E z-0*aPfX7ZPDkRdrZ>n2ZcBqH;9j+# zW14t^5*L;**FiWAm{nh6(T~aO0}<*z{EFQGmoq2&S0|X>SS6Z{q<#*_aA$o^$vZv% z3EdZuR%#(ud(QMp%qnMsTu~=M@(wS7c@+jcFQm#!w*jv=V2S?Ugq?3Yfp6+#HJCas zpSnocJv%TS_i13IsiAao_o8#qck0CWu>^ItjS|LMRdLtot&#!LLa5w<4@~c@8u_t4 z<;=C4$E7(SJBpa(FPvqw=x&F;01<#=Q4xB#lhuwWC?=CE4Vyu_j&^>O$vTI^=s17y zzc#X`ZaF;$ku%q_27r_dO*&3XZu}}3`XS!wqD4Zsq-H_dZ*Mp2{|;^Zxgue=7yJ7YH#^Dg77~cXxq>vz}CcIUQZGsLr|JrNHmBWR#8^5#S6zN?G#kK z;z`?vyRaa#*t-bnZO8UWbCVXv!bwZ|{dofTSBD2*@lE+oMCNO<#UlSTdb-gsiEZYD z@FzNcK35?(z*a?0jPbVO%ELcbq12|fW#yk}5%@h4!1#y6Wz&1TGL}C{F#X6^z6&AG zWk-Ng2wFO8P3qB`P4lxGlvWR$9gPD3>~N(o3FM-kQ5-i}&Jolgd-$0~gh%Od@N`cC=&ik{mrqenjeU3r7{ssHQKmbpYie9fWi zS`AGSg7Z*qFB=hFG83y%!$4UJf}H-i-{nE!Kt0TRT=3~`R@k@2TMY05M5~w`mMkMJ z^kX3NVa|aSn=s0STXh}i>2#l@X!u<_A-! zDNUPJ037k^R5m(flvX5fSy8+m3Uvzs?p}#mNgT$EZa(fuTT?$R0cp-^&N3Xg_Txk} zKSq$0p(9x4B{J%nH3Cslo1jM^p{&~7=vgwXC(Y5e#RaMIpawpkoM5Gr7B}cJvrml?g}F`nHu?+Os+E`S>d5aEhem;ZnAjVxHYO>YEplDlvas9A_%tG zDCExQ@-Qozs1Ib4Gm9Z@H?sQyK{p&(^rVpvXRdhHj+5axBgT86iK9R_xb~fO?d{J1 z>UK5NCy0SMcdU=ba7&#k`IkCP|9G}YD|w)(tT3$Rg3=6SX#B}`a>!Aiiq8*W^#WbM zh@05iQkg#S3u$r5(cA-oBY}Gq@}P=W9pKF#KqN|7a%H!EuGr|zLoy4n_Qri21#j)4 zZD~2Kle~1LOP0|`1$b|YbFpdLbMZgZpK;$Be> z*8n0xm=gz#-E4;O=N^D}|EF2%;gKhOo^t0&s80PNH_$y6+%B`zeCG}B#{~gFl(mX; z?m#I9NAy3imw~TOVcr~#b5ojO5lLWWBc>SFtCPQAjW$#1|HY1fxuJ6SbushBb^=7W z_H1triemYtq?`$r8dsF7`ShN?n9wxtAx4~DMA2kW8W;!E&*Pi07I*(E7Z6!G9l2hC z^hSNGW8l%H77OvDz$x?W`|yM8g)zUJ=e1mt+H3I4hNyo~Yx&Ayg71y`VGz@BZ!QHE zw0sD3xl@nRzKX7HiBzXks92q8QqU0@zU2p0ae&)hT}i;L29s(*__ORX4Uz7xaxG!@ z>@YkpxY+!Dg{MbI72HVnh%?-PTvB7|gM-gViOiE-u*Cc1vXVfD9vu;sW$B0+1BWL& z8A90^J8d~$8YoXZ$xg#d4b!D!eA z)$}HuIKz6#Hn|pmHD;y;%Z|ox1@WV%e>C{ch70yeOsY=9U$vJtJ=?VjKesbw8%>6_ zxVlf)6%PsJUwX5Dr$`Mlk8W1mO&)jj=7G>30VmZ53$%8dI1g?rxIW^zJSGjjpjH8P)s&p;^9z zl-P%!=n^4)s8oV@yEd0iAnztuznB)mBzvh)6rmd0yWzsD3d^;FvB6ls$dMRQ{=j+{ zkOd(nRzyoKMK;PofBX>LC#h^RP=26S^0ch%<69ofD9?Ws&dK#+4o7GRz;~X}1Z=cY z`W?EE2cvKO^dn=um$1TeMPVvMa$`Vn(2%W2=U#c$37b)S05X4ocE4IzN2$@-o-j8_ zr~mwN0?51LOF|-ggAQ$ZC{9A{4ey6q z(&IwWQpxAYPb2NeSp8f~b}|!PGN%Kdeg+sjbe&*iuPn9ZZ{w!bd>X?IK<8}s}wNx&kJgY60#8?kD!y62H z$m7GgA}76ZXkhIXYp#6FNc~JZdGG?Tq3TGG@xyl$xe;emDBcukOKX~(>*YWT%_RACz)hqKX4)S#eeZeg zepG#3V;YaR&R#POnwcFxtoX?qxq>FK+4F@kIEyvHabmsOSa+v+Tbz1gR1+eNf8ncw z+>&%rindYdi&^6eW;?u{`q*US(-Kp%`hmYuberX((m#Zg-2w2)`N6v$b@hvr4SHH4 zj5k#-sg887wi5FZk`d;L9eK-y6}Um#2Twe>@^2kXPGjW#s{jGUxHfRx@4SS#=Qnji z%b)CU)r!9W1&R+Q7t^8$GBD%x>cN`N4{gz;(No(Ua*DE^RQi@v6)ekks?JrL-8+V%U05^ zE*T81xsx#zZIB6TAL4FR>pdR7+dtyRPMHY0TV%H;Ex zpYfX}V{-|+UChy?3}dwRQ)uG~4RRyz^TzdVjzq%^FKIMilOy}@`hFQ}8y=+gM9WCI z|7&P2g)Js~*ibYgVY$5=F>jp-rtzO-2&%jr&d8p^=(1tT^1caR>JQ=^JhpZBvYmrX zujjdNVgUhytG*!jjqe^lPs_tRT0z&gI-(?FxY zG+aHm51J88p+woQpOrYFPu$4>I7XSWO`?ak!?WlE^zL}3z$;WwG zOd6u=E${f+z*U@a7Z3PB3V)t9y`*)KCob?Su}%AT#EM5l(JL3aCAO4ss9eKd>^2-& zto+oS?xQC9qG4r@4&{ma^PT~v9YkU;>Uc0vTPHuB^%(1{upT5BUzo#W4N_m>3zZli@GJU47YB(a?#bbdfel>V4rx?e^qt^F^H62m zj8#iU@_$o~N$QLCGgi}<6?Am|gUmvyvy5`XB#V=}ZyE^pNQikQNO#ssFbUR2>!&YK zp3;y5Sv&})s)R}CT*tV9YzfTq__KDk5}LPrZnA3sQ=f8Quo59jUGtRKksYjyPs7il zcO?>*_^4UWR99%$>8=L#+$L6zTA-k+w`dA%1PS$?Gqq%glC?Ue5RLbi?d*@9>+{=E zGD=vIugCW@(UGY1OhDW85TmdFXLb1X3a2n@f74URZ3f*+q2}-s%GXu z&sraIILLvd3X`}75xHBp{P0?59Vlx?GJk+hi4R$TUZ8>#usi)~81-x;f00vZ+QXbJ zdKqnHgDBsZmNie_01YO9JYK~KG^>czG~=a_Mq7Qp^8hbBcH*K<;x>=Qz{`1uxMDch zJN>73VBKwNy%Ez~3_Y6Y6yv}=cDxi2a_*hhyYaYNACE=Hx8EpXZib6gBY+B@z(X8D zDN!;IOV~p6V_}TD8mV>XyldtXVyxfsjj}Z)x8`txnKSsd)5Tpc<$*uG8J}Q17;qVS z!jvz_#9d&xXj%rD%f7dAqiE$K<@#JiXgqz2k#~D)q zoVj({R(=eNom|XNzq7E!G^FjHxSSp;bAq(&S3B3r}}=D!;FS=W2%XvWTGdk@e3!AfAhiuW3tjS zJK(#Pf=gUtWhm*L&vBD*S42lC?9_9qW3mHjUNRy@eW@~sdFow7o*|=4PO7{ ze+?#A(h&=>>?@mxP4gEpxg%UFJQI}O(c*E2&*=h--fbg{Fpl}lmvAHW=4Y#${G+!9 zU*a{~%F^i{{Yce5i6;~6 zSzlevpQRs_3DOS>qSB$#)AYr^UG4klH@^z&v8ZblRBl4WDMbRGxpN%X4iGSxE&2Dh zzoF4w217-(*=_?Q*I^e3Le4{S>jol6Rwk>O1G3XOyT3hbQSIeUnSIf>*l+TfAksx1 z{nutW3vc&#`V}rJqVjR0XKp*GPlc6AgncZLZjvo0SQT1-6Q%oth-T-o(%kkidRnr# zC<$q<4#vrp8QokF85OBCcZ69t9l2H{LC7LN$?ACPT17=Iogx@lbJvZ^phMnss+W!r zDfNe=^tOe#+*@z8(o7sE%WLgz16(g?x}}2lCM5YOO|bGYSEhQw8Oc1Rw??NxGqz#% z&GHj$i>5nf%^Gqf45nsw;aJJVkl?R?+Q-2PQIc0pteut}wJI-|^i-&gWraL(H&;os zT=)gcDWU74efvwDRU=Z<02**drFu@|01OCjqh2k%xPx^tbc1a>sIw%aQco zOY*_fSM|k#lY(UmBn3PTzY`b))y*_vjHxFnJ5hYtWRNyB0V7Z3s-hpUNBX(q?oP;J z+QN3yJ*>gQQ!D-!Z`+radia(EcK^GHo@$1f@28yEJM|V;xNNVVxamGA_ZJX&cg8FT zob{^vmUx6ROBnBoXzW8Xt7h{!s;FRQTLr+_Jbxch+q#TgP?bOPWTG1c=idMGG_EM! zwh<^$F)xK1-A_SC6N@!^6EgY>^oNg z@S8Fsk3GuR9zBq}yv$!YGq^s_e zTyS&^Yfe0PW+9&;AEz)t*-W}|J&LIXBnjzwTKhJIo5Aw}$lcG#LKFV{rOXFC)OUSd z@*^>O2A%?=Z+t^Z5Ng)I`9V^=Fq#TDdA}C6jvcx{%cV90Ei`5-tsr-CHt4BAuAwC_jSFoQeLJuAcaA37%c;$Ve;D^TWId0^oV+6)cXry@ zH-j2;I=~Wy034P|RG2pch-j)1H6^TrAcAg-P>lvJsR~yg` zKv}I{8rq^vHbwlgufo~IJ2A1`W1An@PcOIvYQU9vRDtXps^$T@RHPJ)_%dg8i>7^$Y;>cQ3n{F$jO+_Q$i{dy9?$hbggLs|TrJQ+@DM0}!ul zJ2Ge8z{%NExF9XzIj7X*Eu#()LETYnn_oNzO`~H{f;7~0YWB6olUzLJ%N=DBgI?Mf zOZ(j~cBU5su)0r?UvLfGd}j%!TX>QyY8o&Z-9aUu+HHqlyS48H;}8PwzB7{=xDJr4x}bAi z#^$s&ZHgwg28E2R`(H5v-s+9J+n>56iun8CQ7G=TKS6yX%pbm)PgElMFV>TPiL=(7 zPSuxCLJvNY-dB=v^YaRC82x~mKU=K$(TfGo(?O$8Ll6+*l8O?Gs_-R*YPo?x&xfsI z+(7@*09$B)_F4i8=1UP(iTuSB-~(x@(BuWP#DTa;ugulUtv`Of;I0555n;{H0UT=o zG{5svqk`i3NFT-`O2V?+R+W{^p2 z+#cfXY2#E1D6djb=KQy~hRexBzrdwKI&!+ino-Rabwtjayh8X$ftyo4N)w_wIcu|j_0N{=$vV+{| zvflHA&&fL!4vvA1%o--Zw;TinkhA_WG?;-@`xhWi;k`DW32SJP;iQ!4dXtV5v+r@H?VT*|4h~ZN z15J76JJN9H7?OQ--o(=V06ks5M|0!6GR&fe+77t*yryerBQ-;2C>zn%w-=Yzdh)XP z9JB5??$tZ1iaL~Fq(H+pwkyIh=6iv6GjqN&IEO-!*iJ@`m1Y_=64n8q=M`|u&PL9P zi44ZRFzo3Ic&%=EP#;VqQOsvv-J1(1sxXN)D4f*DR0wtd5;+{^7LJN=b;RY`aSQUT zWG7$tp_B9DYd7?+u$0{Va~Y~!U)d4p{DZ^iNH4vT!USl`!mdY3p$Ge^|J9T3c8m~M zz)G!awCNcMO<$|eBSm0qXmA5na+@piBk@Ixf(TaDF0BsN#}cthlGW75bzrtGG}ni| ze5%Au@h@j&<2?8soo8-{pK1t3!g=Tf8z0gJi-S#Nx<=;08ZVLwALNZRj0CxatbZdm z&cNHdj2`8yxMevVv20}4(XEB?0a&lxvI80@pdx-aT@&!!4wy|a-UHnf&A12Nx4p&- zh1)d~j#9f+fPe5oD_|dK(b!#^&qzB{|ASoAHdEff|33ukGMxd z_;q2$lb(&@@QR4Z!6nu$XV{i{wr_r}G`cIYs;4>MReyz}1(o2E_ZkZQEF3cDgpaAe zB}N3^wF(Zs^|k0Ge<|a!Ob)mobNRPZ$B9w zRx=>GQ|f4Z5I9k5fGi`Rc$&#WnbS%94#fj#A>au(xcwLQIl^f*SwQLut^4vt8~QH&(=4}3Fjn^uD4haoLps(kfIrzQd|qA$EFbZB7SBpH4%gZ-Tg2z+P?^3jJK zVohizwxnUpv1gAXjJ8;ttbtYL_KgPwK&@6A?{BR@xuf&eZLG)9gG%iiBgkwZ_P^j@?>AGx&=dXU(j&e5@W z1Q&`@6xj@>wEk<<`+u<}3wRHoT6M7}1e-T#X~u1Pz2*_W2kN|9L&`gVEUf_3@kUHq nh$J8LSf%|MhZrScx_hb{M?32|HkZ@@5*JhF5764`q#HhKN*uq7 diff --git a/sessions/main.py b/sessions/main.py index 04e916ca..6db064ae 100644 --- a/sessions/main.py +++ b/sessions/main.py @@ -15,8 +15,8 @@ import random from uuid import uuid4 -from google.cloud import firestore from flask import Flask, make_response, request +from google.cloud import firestore app = Flask(__name__) @@ -66,7 +66,7 @@ def home(): session = get_session_data(transaction, request.cookies.get('session_id')) resp = make_response(template.format( - session['views'], + session['views'], session['greeting'] ) ) diff --git a/sessions/main_test.py b/sessions/main_test.py index f4c99ffb..8fbf79c4 100644 --- a/sessions/main_test.py +++ b/sessions/main_test.py @@ -12,21 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os -import pytest import re import uuid -os.environ['GOOGLE_CLOUD_PROJECT'] = os.environ['FIRESTORE_PROJECT_ID'] -os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = os.path.realpath( - os.path.join( - os.path.dirname(__file__), - '..', - 'firestore-service-account.json' - ) -) - import main +import pytest @pytest.fixture From 77ad58fb31ec98cc971fc36a2dca1e554083a74b Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Mon, 2 Dec 2019 16:50:01 -0800 Subject: [PATCH 096/395] adds back optional-kubernetes-engine for the sake of a quicklab --- optional-kubernetes-engine/.dockerignore | 17 +++ optional-kubernetes-engine/Dockerfile | 27 ++++ optional-kubernetes-engine/Makefile | 50 +++++++ optional-kubernetes-engine/README.md | 97 +++++++++++++ .../bookshelf-frontend.yaml | 53 +++++++ .../bookshelf-service.yaml | 36 +++++ .../bookshelf-worker.yaml | 49 +++++++ .../bookshelf/__init__.py | 125 ++++++++++++++++ optional-kubernetes-engine/bookshelf/crud.py | 131 +++++++++++++++++ .../bookshelf/model_cloudsql.py | 119 +++++++++++++++ .../bookshelf/model_datastore.py | 113 +++++++++++++++ .../bookshelf/model_mongodb.py | 99 +++++++++++++ .../bookshelf/storage.py | 72 ++++++++++ optional-kubernetes-engine/bookshelf/tasks.py | 121 ++++++++++++++++ .../bookshelf/templates/base.html | 40 ++++++ .../bookshelf/templates/form.html | 67 +++++++++ .../bookshelf/templates/list.html | 55 +++++++ .../bookshelf/templates/view.html | 53 +++++++ optional-kubernetes-engine/config.py | 107 ++++++++++++++ optional-kubernetes-engine/main.py | 36 +++++ optional-kubernetes-engine/procfile | 2 + .../requirements-dev.txt | 8 ++ optional-kubernetes-engine/requirements.txt | 16 +++ optional-kubernetes-engine/tests/conftest.py | 90 ++++++++++++ optional-kubernetes-engine/tests/test_auth.py | 136 ++++++++++++++++++ optional-kubernetes-engine/tests/test_crud.py | 88 ++++++++++++ .../tests/test_end_to_end.py | 80 +++++++++++ .../tests/test_storage.py | 70 +++++++++ optional-kubernetes-engine/tox.ini | 30 ++++ 29 files changed, 1987 insertions(+) create mode 100644 optional-kubernetes-engine/.dockerignore create mode 100644 optional-kubernetes-engine/Dockerfile create mode 100644 optional-kubernetes-engine/Makefile create mode 100644 optional-kubernetes-engine/README.md create mode 100644 optional-kubernetes-engine/bookshelf-frontend.yaml create mode 100644 optional-kubernetes-engine/bookshelf-service.yaml create mode 100644 optional-kubernetes-engine/bookshelf-worker.yaml create mode 100644 optional-kubernetes-engine/bookshelf/__init__.py create mode 100644 optional-kubernetes-engine/bookshelf/crud.py create mode 100644 optional-kubernetes-engine/bookshelf/model_cloudsql.py create mode 100644 optional-kubernetes-engine/bookshelf/model_datastore.py create mode 100644 optional-kubernetes-engine/bookshelf/model_mongodb.py create mode 100644 optional-kubernetes-engine/bookshelf/storage.py create mode 100644 optional-kubernetes-engine/bookshelf/tasks.py create mode 100644 optional-kubernetes-engine/bookshelf/templates/base.html create mode 100644 optional-kubernetes-engine/bookshelf/templates/form.html create mode 100644 optional-kubernetes-engine/bookshelf/templates/list.html create mode 100644 optional-kubernetes-engine/bookshelf/templates/view.html create mode 100644 optional-kubernetes-engine/config.py create mode 100644 optional-kubernetes-engine/main.py create mode 100644 optional-kubernetes-engine/procfile create mode 100644 optional-kubernetes-engine/requirements-dev.txt create mode 100644 optional-kubernetes-engine/requirements.txt create mode 100644 optional-kubernetes-engine/tests/conftest.py create mode 100644 optional-kubernetes-engine/tests/test_auth.py create mode 100644 optional-kubernetes-engine/tests/test_crud.py create mode 100644 optional-kubernetes-engine/tests/test_end_to_end.py create mode 100644 optional-kubernetes-engine/tests/test_storage.py create mode 100644 optional-kubernetes-engine/tox.ini diff --git a/optional-kubernetes-engine/.dockerignore b/optional-kubernetes-engine/.dockerignore new file mode 100644 index 00000000..fdc976d4 --- /dev/null +++ b/optional-kubernetes-engine/.dockerignore @@ -0,0 +1,17 @@ +__pycache__ +*.pyc +*.pyo +*.pyd +.Python +env +pip-log.txt +pip-delete-this-directory.txt +.tox +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +*.log +.git diff --git a/optional-kubernetes-engine/Dockerfile b/optional-kubernetes-engine/Dockerfile new file mode 100644 index 00000000..69f9d260 --- /dev/null +++ b/optional-kubernetes-engine/Dockerfile @@ -0,0 +1,27 @@ +# The Google App Engine python runtime is Debian Jessie with Python installed +# and various os-level packages to allow installation of popular Python +# libraries. The source is on github at: +# https://github.com/GoogleCloudPlatform/python-docker +FROM gcr.io/google-appengine/python + +# Create a virtualenv for the application dependencies. +# If you want to use Python 2, add the -p python2.7 flag. +RUN virtualenv -p python3.4 /env + +# Set virtualenv environment variables. This is equivalent to running +# source /env/bin/activate. This ensures the application is executed within +# the context of the virtualenv and will have access to its dependencies. +ENV VIRTUAL_ENV /env +ENV PATH /env/bin:$PATH + +# Install dependencies. +ADD requirements.txt /app/requirements.txt +RUN pip install -r /app/requirements.txt + +# Add application code. +ADD . /app + +# Instead of using gunicorn directly, we'll use Honcho. Honcho is a python port +# of the Foreman process manager. $PROCESSES is set in the pod manifest +# to control which processes Honcho will start. +CMD honcho start -f /app/procfile $PROCESSES diff --git a/optional-kubernetes-engine/Makefile b/optional-kubernetes-engine/Makefile new file mode 100644 index 00000000..f275ddf4 --- /dev/null +++ b/optional-kubernetes-engine/Makefile @@ -0,0 +1,50 @@ +GCLOUD_PROJECT:=$(shell gcloud config list project --format="value(core.project)") + +.PHONY: all +all: deploy + +.PHONY: create-cluster +create-cluster: + gcloud container clusters create bookshelf \ + --scopes "cloud-platform" \ + --num-nodes 2 + gcloud container clusters get-credentials bookshelf + +.PHONY: create-bucket +create-bucket: + gsutil mb gs://$(GCLOUD_PROJECT) + gsutil defacl set public-read gs://$(GCLOUD_PROJECT) + +.PHONY: build +build: + docker build -t gcr.io/$(GCLOUD_PROJECT)/bookshelf . + +.PHONY: push +push: build + gcloud docker -- push gcr.io/$(GCLOUD_PROJECT)/bookshelf + +.PHONY: template +template: + sed -i ".tmpl" "s/\[GCLOUD_PROJECT\]/$(GCLOUD_PROJECT)/g" bookshelf-frontend.yaml + sed -i ".tmpl" "s/\[GCLOUD_PROJECT\]/$(GCLOUD_PROJECT)/g" bookshelf-worker.yaml + +.PHONY: create-service +create-service: + kubectl create -f bookshelf-service.yaml + +.PHONY: deploy-frontend +deploy-frontend: push template + kubectl create -f bookshelf-frontend.yaml + +.PHONY: deploy-worker +deploy-worker: push template + kubectl create -f bookshelf-worker.yaml + +.PHONY: deploy +deploy: deploy-frontend deploy-worker create-service + +.PHONY: delete +delete: + -kubectl delete -f bookshelf-service.yaml + -kubectl delete -f bookshelf-worker.yaml + -kubectl delete -f bookshelf-frontend.yaml diff --git a/optional-kubernetes-engine/README.md b/optional-kubernetes-engine/README.md new file mode 100644 index 00000000..e3c17236 --- /dev/null +++ b/optional-kubernetes-engine/README.md @@ -0,0 +1,97 @@ +# Deploy Bookshelf to Google Kubernetes Engine + +This optional tutorial will walk you through how to deploy the Bookshelf sample application to [Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/). This tutorial is also applicable to [Kubernetes](http://kubernetes.io/) outside of Google Kubernetes Engine, but may require additional steps for external load balancing. + +## Pre-requisites + +1. Create a project in the [Google Cloud Platform Console](https://console.cloud.google.com). + +2. [Enable billing](https://console.cloud.google.com/project/_/settings) for your project. + +3. [Enable APIs](https://console.cloud.google.com/flows/enableapi?apiid=datastore,pubsub,storage_api,logging,plus) for your project. The provided link will enable all necessary APIs, but if you wish to do so manually you will need Datastore, Pub/Sub, Storage, and Logging. + +4. Install the [Google Cloud SDK](https://cloud.google.com/sdk) + + $ curl https://sdk.cloud.google.com | bash + $ gcloud init + +5. Install [Docker](https://www.docker.com/). + +## Create a cluster + +Create a cluster for the bookshelf application: + + gcloud container clusters create bookshelf \ + --scopes "cloud-platform" \ + --num-nodes 2 + gcloud container clusters get-credentials bookshelf + +The scopes specified in the `--scopes` argument allows nodes in the cluster to access Google Cloud Platform APIs, such as the Cloud Datastore API. + +Alternatively, you can use make: + + make create-cluster + +## Create a Cloud Storage bucket + +The bookshelf application uses [Google Cloud Storage](https://cloud.google.com/storage) to store image files. Create a bucket for your project: + + gsutil mb gs:// + gsutil defacl set public-read gs:// + +Alternatively, you can use make: + + make create-bucket + +## Update config.py + +Modify config.py and enter your Cloud Project ID into the `PROJECT_ID` and `CLOUD_STORAGE_BUCKET` field. The remaining configuration values are only needed if you wish to use a different database or if you wish to enable log-in via oauth2, which requires a domain name. + +## Build the bookshelf container + +Before the application can be deployed to Kubernetes Engine, you will need build and push the image to [Google Container Registry](https://cloud.google.com/container-registry/). + + docker build -t gcr.io//bookshelf . + gcloud docker push gcr.io//bookshelf + +Alternatively, you can use make: + + make push + +## Deploy the bookshelf frontend + +The bookshelf app has two distinct "tiers". The frontend serves a web interface to create and manage books, while the worker handles fetching book information from the Google Books API. + +Update `bookshelf-frontend.yaml` with your Project ID or use `make template`. This file contains the Kubernetes resource definitions to deploy the frontend. You can use `kubectl` to create these resources on the cluster: + + kubectl create -f bookshelf-frontend.yaml + +Alternatively, you can use make: + + make deploy-frontend + +Once the resources are created, there should be 3 `bookshelf-frontend` pods on the cluster. To see the pods and ensure that they are running: + + kubectl get pods + +If the pods are not ready or if you see restarts, you can get the logs for a particular pod to figure out the issue: + + kubectl logs pod-id + +Once the pods are ready, you can get the public IP address of the load balancer: + + kubectl get services bookshelf-frontend + +You can then browse to the public IP address in your browser to see the bookshelf application. + +## Deploy worker + +Update `bookshelf-worker.yaml` with your Project ID or use `make template`. This file contains the Kubernetes resource definitions to deploy the worker. The worker doesn't need to serve web traffic or expose any ports, so it has significantly less configuration than the frontend. You can use `kubectl` to create these resources on the cluster: + + kubectl create -f bookshelf-worker.yaml + +Alternatively, you can use make: + + make deploy-worker + +Once again, use `kubectl get pods` to check the status of the worker pods. Once the worker pods are up and running, you should be able to create books on the frontend and the workers will handle updating book information in the background. diff --git a/optional-kubernetes-engine/bookshelf-frontend.yaml b/optional-kubernetes-engine/bookshelf-frontend.yaml new file mode 100644 index 00000000..265c8e35 --- /dev/null +++ b/optional-kubernetes-engine/bookshelf-frontend.yaml @@ -0,0 +1,53 @@ +# Copyright 2015 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License + +# This file configures the bookshelf application frontend. The frontend serves +# public web traffic. + +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: bookshelf-frontend + labels: + app: bookshelf +# The bookshelf frontend replica set ensures that at least 3 +# instances of the bookshelf app are running on the cluster. +# For more info about Pods see: +# https://cloud.google.com/kubernetes-engine/docs/pods/ +spec: + replicas: 3 + template: + metadata: + labels: + app: bookshelf + tier: frontend + spec: + containers: + - name: bookshelf-app + # Replace [GCLOUD_PROJECT] with your project ID or use `make template`. + image: gcr.io/[GCLOUD_PROJECT]/bookshelf + # This setting makes nodes pull the docker image every time before + # starting the pod. This is useful when debugging, but should be turned + # off in production. + imagePullPolicy: Always + # The PROCESSES environment variable is used by Honcho in the + # Dockerfile's CMD to control which processes are started. In this + # case, only the bookshelf process is needed. + env: + - name: PROCESSES + value: bookshelf + # The bookshelf process listens on port 8080 for web traffic by default. + ports: + - name: http-server + containerPort: 8080 diff --git a/optional-kubernetes-engine/bookshelf-service.yaml b/optional-kubernetes-engine/bookshelf-service.yaml new file mode 100644 index 00000000..db8f1fe7 --- /dev/null +++ b/optional-kubernetes-engine/bookshelf-service.yaml @@ -0,0 +1,36 @@ +# Copyright 2016 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License + +# The bookshelf service provides a load-balancing proxy over the bookshelf +# frontend pods. By specifying the type as a 'LoadBalancer', Kubernetes Engine +# will create an external HTTP load balancer. +# For more information about Services see: +# https://cloud.google.com/kubernetes-engine/docs/services/ +# For more information about external HTTP load balancing see: +# https://cloud.google.com/kubernetes-engine/docs/load-balancer +apiVersion: v1 +kind: Service +metadata: + name: bookshelf-frontend + labels: + app: bookshelf + tier: frontend +spec: + type: LoadBalancer + ports: + - port: 80 + targetPort: http-server + selector: + app: bookshelf + tier: frontend diff --git a/optional-kubernetes-engine/bookshelf-worker.yaml b/optional-kubernetes-engine/bookshelf-worker.yaml new file mode 100644 index 00000000..c30df89b --- /dev/null +++ b/optional-kubernetes-engine/bookshelf-worker.yaml @@ -0,0 +1,49 @@ +# Copyright 2015 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License + +# This file configures the bookshelf task worker. The worker is responsible +# for processing book requests and updating book information. + +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: bookshelf-worker + labels: + app: bookshelf +# The bookshelf worker replica set ensures that at least 2 instances of the +# bookshelf worker pod are running on the cluster. +# For more info about Pods see: +# https://cloud.google.com/kubernetes-engine/docs/pods/ +spec: + replicas: 2 + template: + metadata: + labels: + app: bookshelf + tier: worker + spec: + containers: + - name: bookshelf-app + # Replace [GCLOUD_PROJECT] with your project ID or use `make template`. + image: gcr.io/[GCLOUD_PROJECT]/bookshelf + # This setting makes nodes pull the docker image every time before + # starting the pod. This is useful when debugging, but should be turned + # off in production. + imagePullPolicy: Always + # The PROCESSES environment variable is used by Honcho in the + # Dockerfile's CMD to control which processes are started. In this + # case, only the worker process is needed. + env: + - name: PROCESSES + value: worker diff --git a/optional-kubernetes-engine/bookshelf/__init__.py b/optional-kubernetes-engine/bookshelf/__init__.py new file mode 100644 index 00000000..5fd93b9b --- /dev/null +++ b/optional-kubernetes-engine/bookshelf/__init__.py @@ -0,0 +1,125 @@ +# Copyright 2015 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import logging + +from flask import current_app, Flask, redirect, request, session, url_for +import httplib2 +from oauth2client.contrib.flask_util import UserOAuth2 + + +oauth2 = UserOAuth2() + + +def create_app(config, debug=False, testing=False, config_overrides=None): + app = Flask(__name__) + app.config.from_object(config) + + app.debug = debug + app.testing = testing + + if config_overrides: + app.config.update(config_overrides) + + # Configure logging + if not app.testing: + logging.basicConfig(level=logging.INFO) + + # Setup the data model. + with app.app_context(): + model = get_model() + model.init_app(app) + + # Create a health check handler. Health checks are used when running on + # Google Compute Engine by the load balancer to determine which instances + # can serve traffic. Google App Engine also uses health checking, but + # accepts any non-500 response as healthy. + @app.route('/_ah/health') + def health_check(): + return 'ok', 200 + + # Initalize the OAuth2 helper. + oauth2.init_app( + app, + scopes=['email', 'profile'], + authorize_callback=_request_user_info) + + # Add a logout handler. + @app.route('/logout') + def logout(): + # Delete the user's profile and the credentials stored by oauth2. + del session['profile'] + session.modified = True + oauth2.storage.delete() + return redirect(request.referrer or '/') + + # Register the Bookshelf CRUD blueprint. + from .crud import crud + app.register_blueprint(crud, url_prefix='/books') + + # Add a default root route. + @app.route("/") + def index(): + return redirect(url_for('crud.list')) + + # Add an error handler. This is useful for debugging the live application, + # however, you should disable the output of the exception for production + # applications. + @app.errorhandler(500) + def server_error(e): + return """ + An internal error occurred:
{}
+ See logs for full stacktrace. + """.format(e), 500 + + return app + + +def get_model(): + model_backend = current_app.config['DATA_BACKEND'] + if model_backend == 'cloudsql': + from . import model_cloudsql + model = model_cloudsql + elif model_backend == 'datastore': + from . import model_datastore + model = model_datastore + elif model_backend == 'mongodb': + from . import model_mongodb + model = model_mongodb + else: + raise ValueError( + "No appropriate databackend configured. " + "Please specify datastore, cloudsql, or mongodb") + + return model + + +def _request_user_info(credentials): + """ + Makes an HTTP request to the Google OAuth2 API to retrieve the user's basic + profile information, including full name and photo, and stores it in the + Flask session. + """ + http = httplib2.Http() + credentials.authorize(http) + resp, content = http.request( + 'https://www.googleapis.com/oauth2/v3/userinfo') + + if resp.status != 200: + current_app.logger.error( + "Error while obtaining user profile: \n%s: %s", resp, content) + return None + + session['profile'] = json.loads(content.decode('utf-8')) diff --git a/optional-kubernetes-engine/bookshelf/crud.py b/optional-kubernetes-engine/bookshelf/crud.py new file mode 100644 index 00000000..e07b4053 --- /dev/null +++ b/optional-kubernetes-engine/bookshelf/crud.py @@ -0,0 +1,131 @@ +# Copyright 2015 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from bookshelf import get_model, oauth2, storage, tasks +from flask import Blueprint, current_app, redirect, render_template, request, \ + session, url_for + + +crud = Blueprint('crud', __name__) + + +def upload_image_file(file): + """ + Upload the user-uploaded file to Google Cloud Storage and retrieve its + publicly-accessible URL. + """ + if not file: + return None + + public_url = storage.upload_file( + file.read(), + file.filename, + file.content_type + ) + + current_app.logger.info( + "Uploaded file %s as %s.", file.filename, public_url) + + return public_url + + +@crud.route("/") +def list(): + token = request.args.get('page_token', None) + if token: + token = token.encode('utf-8') + + books, next_page_token = get_model().list(cursor=token) + + return render_template( + "list.html", + books=books, + next_page_token=next_page_token) + + +@crud.route("/mine") +@oauth2.required +def list_mine(): + token = request.args.get('page_token', None) + if token: + token = token.encode('utf-8') + + books, next_page_token = get_model().list_by_user( + user_id=session['profile']['id'], + cursor=token) + + return render_template( + "list.html", + books=books, + next_page_token=next_page_token) + + +@crud.route('/') +def view(id): + book = get_model().read(id) + return render_template("view.html", book=book) + + +@crud.route('/add', methods=['GET', 'POST']) +def add(): + if request.method == 'POST': + data = request.form.to_dict(flat=True) + + # If an image was uploaded, update the data to point to the new image. + image_url = upload_image_file(request.files.get('image')) + + if image_url: + data['imageUrl'] = image_url + + # If the user is logged in, associate their profile with the new book. + if 'profile' in session: + data['createdBy'] = session['profile']['name'] + data['createdById'] = session['profile']['email'] + + book = get_model().create(data) + + q = tasks.get_books_queue() + q.enqueue(tasks.process_book, book['id']) + + return redirect(url_for('.view', id=book['id'])) + + return render_template("form.html", action="Add", book={}) + + +@crud.route('//edit', methods=['GET', 'POST']) +def edit(id): + book = get_model().read(id) + + if request.method == 'POST': + data = request.form.to_dict(flat=True) + + image_url = upload_image_file(request.files.get('image')) + + if image_url: + data['imageUrl'] = image_url + + book = get_model().update(data, id) + + q = tasks.get_books_queue() + q.enqueue(tasks.process_book, book['id']) + + return redirect(url_for('.view', id=book['id'])) + + return render_template("form.html", action="Edit", book=book) + + +@crud.route('//delete') +def delete(id): + get_model().delete(id) + return redirect(url_for('.list')) diff --git a/optional-kubernetes-engine/bookshelf/model_cloudsql.py b/optional-kubernetes-engine/bookshelf/model_cloudsql.py new file mode 100644 index 00000000..d84207d2 --- /dev/null +++ b/optional-kubernetes-engine/bookshelf/model_cloudsql.py @@ -0,0 +1,119 @@ +# Copyright 2015 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from flask import Flask +from flask_sqlalchemy import SQLAlchemy + + +builtin_list = list + + +db = SQLAlchemy() + + +def init_app(app): + # Disable track modifications, as it unnecessarily uses memory. + app.config.setdefault('SQLALCHEMY_TRACK_MODIFICATIONS', False) + db.init_app(app) + + +def from_sql(row): + """Translates a SQLAlchemy model instance into a dictionary""" + data = row.__dict__.copy() + data['id'] = row.id + data.pop('_sa_instance_state') + return data + + +class Book(db.Model): + __tablename__ = 'books' + + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(255)) + author = db.Column(db.String(255)) + publishedDate = db.Column(db.String(255)) + imageUrl = db.Column(db.String(255)) + description = db.Column(db.String(4096)) + createdBy = db.Column(db.String(255)) + createdById = db.Column(db.String(255)) + + def __repr__(self): + return " + + + Bookshelf - Python on Google Cloud Platform + + + + + + +
+ {% block content %}{% endblock %} +
+ {{user}} + + diff --git a/optional-kubernetes-engine/bookshelf/templates/form.html b/optional-kubernetes-engine/bookshelf/templates/form.html new file mode 100644 index 00000000..2de32a29 --- /dev/null +++ b/optional-kubernetes-engine/bookshelf/templates/form.html @@ -0,0 +1,67 @@ +{# +# Copyright 2015 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#} + +{% extends "base.html" %} + +{% block content %} +

{{action}} book

+ +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + + + + + + +
+ +{% endblock %} diff --git a/optional-kubernetes-engine/bookshelf/templates/list.html b/optional-kubernetes-engine/bookshelf/templates/list.html new file mode 100644 index 00000000..3362f0e2 --- /dev/null +++ b/optional-kubernetes-engine/bookshelf/templates/list.html @@ -0,0 +1,55 @@ +{# +# Copyright 2015 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#} + +{% extends "base.html" %} + +{% block content %} + +

Books

+ + + Add book + + +{% for book in books %} + +{% else %} +

No books found

+{% endfor %} + +{% if next_page_token %} + +{% endif %} + +{% endblock %} diff --git a/optional-kubernetes-engine/bookshelf/templates/view.html b/optional-kubernetes-engine/bookshelf/templates/view.html new file mode 100644 index 00000000..e654e8ab --- /dev/null +++ b/optional-kubernetes-engine/bookshelf/templates/view.html @@ -0,0 +1,53 @@ +{# +# Copyright 2015 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#} + +{% extends "base.html" %} + +{% block content %} + +

Book

+ + + +
+
+ {% if book.imageUrl %} + + {% else %} + + {% endif %} +
+
+

+ {{book.title}} + {{book.publishedDate}} +

+
By {{book.author|default('Unknown', True)}}
+

{{book.description}}

+ Added by {{book.createdBy|default('Anonymous', True)}} +
+
+ +{% endblock %} diff --git a/optional-kubernetes-engine/config.py b/optional-kubernetes-engine/config.py new file mode 100644 index 00000000..0fd601ed --- /dev/null +++ b/optional-kubernetes-engine/config.py @@ -0,0 +1,107 @@ +# Copyright 2015 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +This file contains all of the configuration values for the application. +Update this file with the values for your specific Google Cloud project. +You can create and manage projects at https://console.developers.google.com +""" + +import os + +# The secret key is used by Flask to encrypt session cookies. +SECRET_KEY = 'secret' + +# There are three different ways to store the data in the application. +# You can choose 'datastore', 'cloudsql', or 'mongodb'. Be sure to +# configure the respective settings for the one you choose below. +# You do not have to configure the other data backends. If unsure, choose +# 'datastore' as it does not require any additional configuration. +DATA_BACKEND = 'datastore' + +# Google Cloud Project ID. This can be found on the 'Overview' page at +# https://console.developers.google.com +PROJECT_ID = 'your-project-id' + +# CloudSQL & SQLAlchemy configuration +# Replace the following values the respective values of your Cloud SQL +# instance. +CLOUDSQL_USER = 'root' +CLOUDSQL_PASSWORD = 'your-cloudsql-password' +CLOUDSQL_DATABASE = 'bookshelf' +# Set this value to the Cloud SQL connection name, e.g. +# "project:region:cloudsql-instance". +# You must also update the value in app.yaml. +CLOUDSQL_CONNECTION_NAME = 'your-cloudsql-connection-name' + +# The CloudSQL proxy is used locally to connect to the cloudsql instance. +# To start the proxy, use: +# +# $ cloud_sql_proxy -instances=your-connection-name=tcp:3306 +# +# Alternatively, you could use a local MySQL instance for testing. +LOCAL_SQLALCHEMY_DATABASE_URI = ( + 'mysql+pymysql://{user}:{password}@localhost/{database}').format( + user=CLOUDSQL_USER, password=CLOUDSQL_PASSWORD, + database=CLOUDSQL_DATABASE) + +# When running on App Engine a unix socket is used to connect to the cloudsql +# instance. +LIVE_SQLALCHEMY_DATABASE_URI = ( + 'mysql+pymysql://{user}:{password}@localhost/{database}' + '?unix_socket=/cloudsql/{connection_name}').format( + user=CLOUDSQL_USER, password=CLOUDSQL_PASSWORD, + database=CLOUDSQL_DATABASE, connection_name=CLOUDSQL_CONNECTION_NAME) + +if os.environ.get('GAE_INSTANCE'): + SQLALCHEMY_DATABASE_URI = LIVE_SQLALCHEMY_DATABASE_URI +else: + SQLALCHEMY_DATABASE_URI = LOCAL_SQLALCHEMY_DATABASE_URI + +# Mongo configuration +# If using mongolab, the connection URI is available from the mongolab control +# panel. If self-hosting on compute engine, replace the values below. +MONGO_URI = 'mongodb://user:password@host:27017/database' + +# Google Cloud Storage and upload settings. +# Typically, you'll name your bucket the same as your project. To create a +# bucket: +# +# $ gsutil mb gs:// +# +# You also need to make sure that the default ACL is set to public-read, +# otherwise users will not be able to see their upload images: +# +# $ gsutil defacl set public-read gs:// +# +# You can adjust the max content length and allow extensions settings to allow +# larger or more varied file types if desired. +CLOUD_STORAGE_BUCKET = 'your-bucket-name' +MAX_CONTENT_LENGTH = 8 * 1024 * 1024 +ALLOWED_EXTENSIONS = set(['png', 'jpg', 'jpeg', 'gif']) + +# OAuth2 configuration. +# This can be generated from the Google Developers Console at +# https://console.developers.google.com/project/_/apiui/credential. +# Note that you will need to add all URLs that your application uses as +# authorized redirect URIs. For example, typically you would add the following: +# +# * http://localhost:8080/oauth2callback +# * https://.appspot.com/oauth2callback. +# +# If you receive a invalid redirect URI error review you settings to ensure +# that the current URI is allowed. +GOOGLE_OAUTH2_CLIENT_ID = \ + 'your-client-id' +GOOGLE_OAUTH2_CLIENT_SECRET = 'your-client-secret' diff --git a/optional-kubernetes-engine/main.py b/optional-kubernetes-engine/main.py new file mode 100644 index 00000000..3fc2078e --- /dev/null +++ b/optional-kubernetes-engine/main.py @@ -0,0 +1,36 @@ +# Copyright 2015 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import bookshelf +import config + + +# Note: debug=True is enabled here to help with troubleshooting. You should +# remove this in production. +app = bookshelf.create_app(config, debug=True) + + +# Make the queue available at the top-level, this allows you to run +# `psqworker main.books_queue`. We have to use the app's context because +# it contains all the configuration for plugins. +# If you were using another task queue, such as celery or rq, you can use this +# section to configure your queues to work with Flask. +with app.app_context(): + books_queue = bookshelf.tasks.get_books_queue() + + +# This is only used when running locally. When running live, gunicorn runs +# the application. +if __name__ == '__main__': + app.run(host='127.0.0.1', port=8080, debug=True) diff --git a/optional-kubernetes-engine/procfile b/optional-kubernetes-engine/procfile new file mode 100644 index 00000000..99ae5024 --- /dev/null +++ b/optional-kubernetes-engine/procfile @@ -0,0 +1,2 @@ +bookshelf: gunicorn -b 0.0.0.0:$PORT main:app +worker: psqworker main.books_queue diff --git a/optional-kubernetes-engine/requirements-dev.txt b/optional-kubernetes-engine/requirements-dev.txt new file mode 100644 index 00000000..3118b924 --- /dev/null +++ b/optional-kubernetes-engine/requirements-dev.txt @@ -0,0 +1,8 @@ +tox==3.5.3 +flake8==3.6.0 +flaky==3.4.0 +pytest==4.0.1 +pytest-cov==2.6.0 +BeautifulSoup4==4.6.3 +requests==2.20.1 +retrying==1.3.3 diff --git a/optional-kubernetes-engine/requirements.txt b/optional-kubernetes-engine/requirements.txt new file mode 100644 index 00000000..5d998499 --- /dev/null +++ b/optional-kubernetes-engine/requirements.txt @@ -0,0 +1,16 @@ +Flask>=1.0.0 +google-cloud-datastore==1.7.1 +google-cloud-storage==1.13.0 +google-cloud-logging==1.8.0 +google-cloud-error_reporting==0.30.0 +gunicorn==19.9.0 +oauth2client==4.1.3 +mock==2.0.0 +Flask-SQLAlchemy==2.3.2 +PyMySQL==0.9.2 +Flask-PyMongo>=2.0.0 +PyMongo==3.7.2 +six==1.11.0 +requests[security]==2.20.1 +honcho==1.0.1 +psq==0.7.0 diff --git a/optional-kubernetes-engine/tests/conftest.py b/optional-kubernetes-engine/tests/conftest.py new file mode 100644 index 00000000..d098db9d --- /dev/null +++ b/optional-kubernetes-engine/tests/conftest.py @@ -0,0 +1,90 @@ +# Copyright 2015 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""conftest.py is used to define common test fixtures for pytest.""" + +import bookshelf +import config +from google.cloud.exceptions import ServiceUnavailable +from oauth2client.client import HttpAccessTokenRefreshError +import pytest +from retrying import retry + + +@pytest.yield_fixture(params=['datastore', 'mongodb']) +def app(request): + """This fixtures provides a Flask app instance configured for testing. + + Because it's parametric, it will cause every test that uses this fixture + to run three times: one time for each backend (datastore, cloudsql, and + mongodb). + + It also ensures the tests run within a request context, allowing + any calls to flask.request, flask.current_app, etc. to work.""" + app = bookshelf.create_app( + config, + testing=True, + config_overrides={ + 'DATA_BACKEND': request.param + }) + + with app.test_request_context(): + yield app + + +@pytest.yield_fixture +def model(monkeypatch, app): + """This fixture provides a modified version of the app's model that tracks + all created items and deletes them at the end of the test. + + Any tests that directly or indirectly interact with the database should use + this to ensure that resources are properly cleaned up. + + Monkeypatch is provided by pytest and used to patch the model's create + method. + + The app fixture is needed to provide the configuration and context needed + to get the proper model object. + """ + model = bookshelf.get_model() + + # Ensure no books exist before running. This typically helps if tests + # somehow left the database in a bad state. + delete_all_books(model) + + yield model + + # Delete all books that we created during tests. + delete_all_books(model) + + +# The backend data stores can sometimes be flaky. It's useful to retry this +# a few times before giving up. +@retry( + stop_max_attempt_number=3, + wait_exponential_multiplier=100, + wait_exponential_max=2000) +def delete_all_books(model): + while True: + books, _ = model.list(limit=50) + if not books: + break + for book in books: + model.delete(book['id']) + + +def flaky_filter(info, *args): + """Used by flaky to determine when to re-run a test case.""" + _, e, _ = info + return isinstance(e, (ServiceUnavailable, HttpAccessTokenRefreshError)) diff --git a/optional-kubernetes-engine/tests/test_auth.py b/optional-kubernetes-engine/tests/test_auth.py new file mode 100644 index 00000000..6367608c --- /dev/null +++ b/optional-kubernetes-engine/tests/test_auth.py @@ -0,0 +1,136 @@ +# Copyright 2015 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import contextlib + +import bookshelf +from conftest import flaky_filter +from flaky import flaky +import mock +from oauth2client.client import OAuth2Credentials +import pytest + + +@pytest.fixture +def client_with_credentials(app): + """This fixture provides a Flask app test client that has a session + pre-configured with use credentials.""" + credentials = OAuth2Credentials( + 'access_token', + 'client_id', + 'client_secret', + 'refresh_token', + '3600', + None, + 'Test', + id_token={'sub': '123', 'email': 'user@example.com'}, + scopes=('email', 'profile')) + + @contextlib.contextmanager + def inner(): + with app.test_client() as client: + with client.session_transaction() as session: + session['profile'] = { + 'email': 'abc@example.com', + 'name': 'Test User' + } + session['google_oauth2_credentials'] = credentials.to_json() + yield client + + return inner + + +# Mark all test cases in this class as flaky, so that if errors occur they +# can be retried. This is useful when databases are temporarily unavailable. +@flaky(rerun_filter=flaky_filter) +# Tell pytest to use both the app and model fixtures for all test cases. +# This ensures that configuration is properly applied and that all database +# resources created during tests are cleaned up. These fixtures are defined +# in conftest.py +@pytest.mark.usefixtures('app', 'model') +class TestAuth(object): + def test_not_logged_in(self, app): + with app.test_client() as c: + rv = c.get('/books/') + + assert rv.status == '200 OK' + body = rv.data.decode('utf-8') + assert 'Login' in body + + def test_logged_in(self, client_with_credentials): + with client_with_credentials() as c: + rv = c.get('/books/') + + assert rv.status == '200 OK' + body = rv.data.decode('utf-8') + assert 'Test User' in body + + def test_add_anonymous(self, app): + data = { + 'title': 'Test Book', + } + + with app.test_client() as c: + rv = c.post('/books/add', data=data, follow_redirects=True) + + assert rv.status == '200 OK' + body = rv.data.decode('utf-8') + assert 'Test Book' in body + assert 'Added by Anonymous' in body + + def test_add_logged_in(self, client_with_credentials): + data = { + 'title': 'Test Book', + } + + with client_with_credentials() as c: + rv = c.post('/books/add', data=data, follow_redirects=True) + + assert rv.status == '200 OK' + body = rv.data.decode('utf-8') + assert 'Test Book' in body + assert 'Added by Test User' in body + + def test_mine(self, model, client_with_credentials): + # Create two books, one created by the logged in user and one + # created by another user. + model.create({ + 'title': 'Book 1', + 'createdById': 'abc@example.com' + }) + + model.create({ + 'title': 'Book 2', + 'createdById': 'def@example.com' + }) + + # Check the "My Books" page and make sure only one of the books + # appears. + with client_with_credentials() as c: + rv = c.get('/books/mine') + + assert rv.status == '200 OK' + body = rv.data.decode('utf-8') + assert 'Book 1' in body + assert 'Book 2' not in body + + @mock.patch("httplib2.Http") + def test_request_user_info(self, HttpMock): + httpObj = mock.MagicMock() + responseMock = mock.MagicMock(status=200) + httpObj.request = mock.MagicMock( + return_value=(responseMock, b'{"name": "bill"}')) + HttpMock.return_value = httpObj + credentials = mock.MagicMock() + bookshelf._request_user_info(credentials) diff --git a/optional-kubernetes-engine/tests/test_crud.py b/optional-kubernetes-engine/tests/test_crud.py new file mode 100644 index 00000000..c0d2f40f --- /dev/null +++ b/optional-kubernetes-engine/tests/test_crud.py @@ -0,0 +1,88 @@ +# Copyright 2015 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import re + +from conftest import flaky_filter +from flaky import flaky +import pytest + + +# Mark all test cases in this class as flaky, so that if errors occur they +# can be retried. This is useful when databases are temporarily unavailable. +@flaky(rerun_filter=flaky_filter) +# Tell pytest to use both the app and model fixtures for all test cases. +# This ensures that configuration is properly applied and that all database +# resources created during tests are cleaned up. These fixtures are defined +# in conftest.py +@pytest.mark.usefixtures('app', 'model') +class TestCrudActions(object): + + def test_list(self, app, model): + for i in range(1, 12): + model.create({'title': u'Book {0}'.format(i)}) + + with app.test_client() as c: + rv = c.get('/books/') + + assert rv.status == '200 OK' + + body = rv.data.decode('utf-8') + assert 'Book 1' in body, "Should show books" + assert len(re.findall('

Book', body)) == 10, ( + "Should not show more than 10 books") + assert 'More' in body, "Should have more than one page" + + def test_add(self, app): + data = { + 'title': 'Test Book', + 'author': 'Test Author', + 'publishedDate': 'Test Date Published', + 'description': 'Test Description' + } + + with app.test_client() as c: + rv = c.post('/books/add', data=data, follow_redirects=True) + + assert rv.status == '200 OK' + body = rv.data.decode('utf-8') + assert 'Test Book' in body + assert 'Test Author' in body + assert 'Test Date Published' in body + assert 'Test Description' in body + + def test_edit(self, app, model): + existing = model.create({'title': "Temp Title"}) + + with app.test_client() as c: + rv = c.post( + '/books/%s/edit' % existing['id'], + data={'title': 'Updated Title'}, + follow_redirects=True) + + assert rv.status == '200 OK' + body = rv.data.decode('utf-8') + assert 'Updated Title' in body + assert 'Temp Title' not in body + + def test_delete(self, app, model): + existing = model.create({'title': "Temp Title"}) + + with app.test_client() as c: + rv = c.get( + '/books/%s/delete' % existing['id'], + follow_redirects=True) + + assert rv.status == '200 OK' + assert not model.read(existing['id']) diff --git a/optional-kubernetes-engine/tests/test_end_to_end.py b/optional-kubernetes-engine/tests/test_end_to_end.py new file mode 100644 index 00000000..df80e8a0 --- /dev/null +++ b/optional-kubernetes-engine/tests/test_end_to_end.py @@ -0,0 +1,80 @@ +# Copyright 2015 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import re + +from bs4 import BeautifulSoup +import pytest +import requests +from retrying import retry + + +@pytest.mark.e2e +def test_end_to_end(): + """Tests designed to be run against live environments. + + Unlike the integration tests in the other packages, these tests are + designed to be run against fully-functional live environments. + + To run locally, start both main.py and psq_worker main.books_queue and + run this file. + + It can be run against a live environment by setting the E2E_URL + environment variables before running the tests: + + E2E_URL=http://your-app-id.appspot.com \ + nosetests tests/test_end_to_end.py + """ + + base_url = os.environ.get('E2E_URL', 'http://localhost:8080') + + book_data = { + 'title': 'a confederacy of dunces', + } + + response = requests.post(base_url + '/books/add', data=book_data) + + # There was a 302, so get the book's URL from the redirect. + book_url = response.request.url + book_id = book_url.rsplit('/', 1).pop() + + # Use retry because it will take some indeterminate time for the pub/sub + # message to be processed. + @retry(wait_exponential_multiplier=5000, stop_max_attempt_number=12) + def check_for_updated_data(): + # Check that the book's information was updated. + response = requests.get(book_url) + assert response.status_code == 200 + + soup = BeautifulSoup(response.text, 'html.parser') + + title = soup.find('h4', 'book-title').contents[0].strip() + assert re.search(r'A Confederacy of Dunces', title, re.I) + + author = soup.find('h5', 'book-author').string + assert re.search(r'John Kennedy Toole', author, re.I) + + description = soup.find('p', 'book-description').string + assert re.search(r'Ignatius', description, re.I) + + image_src = soup.find('img', 'book-image')['src'] + image = requests.get(image_src) + assert image.status_code == 200 + + try: + check_for_updated_data() + finally: + # Delete the book we created. + requests.get(base_url + '/books/{}/delete'.format(book_id)) diff --git a/optional-kubernetes-engine/tests/test_storage.py b/optional-kubernetes-engine/tests/test_storage.py new file mode 100644 index 00000000..531c8827 --- /dev/null +++ b/optional-kubernetes-engine/tests/test_storage.py @@ -0,0 +1,70 @@ +# Copyright 2015 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import re + +from conftest import flaky_filter +from flaky import flaky +import httplib2 +import pytest +from six import BytesIO + + +# Mark all test cases in this class as flaky, so that if errors occur they +# can be retried. This is useful when databases are temporarily unavailable. +@flaky(rerun_filter=flaky_filter) +# Tell pytest to use both the app and model fixtures for all test cases. +# This ensures that configuration is properly applied and that all database +# resources created during tests are cleaned up. These fixtures are defined +# in conftest.py +@pytest.mark.usefixtures('app', 'model') +class TestStorage(object): + + def test_upload_image(self, app): + data = { + 'title': 'Test Book', + 'author': 'Test Author', + 'publishedDate': 'Test Date Published', + 'description': 'Test Description', + 'image': (BytesIO(b'hello world'), 'hello.jpg') + } + + with app.test_client() as c: + rv = c.post('/books/add', data=data, follow_redirects=True) + + assert rv.status == '200 OK' + body = rv.data.decode('utf-8') + + img_tag = re.search(''), + '1337h4x0r.php') + } + + with app.test_client() as c: + rv = c.post('/books/add', data=data, follow_redirects=True) + + # check we weren't pwned + assert rv.status == '400 BAD REQUEST' diff --git a/optional-kubernetes-engine/tox.ini b/optional-kubernetes-engine/tox.ini new file mode 100644 index 00000000..8a04f077 --- /dev/null +++ b/optional-kubernetes-engine/tox.ini @@ -0,0 +1,30 @@ +[tox] +skipsdist = True +envlist = lint,py27,py36 + +[testenv] +deps = + -rrequirements.txt + -rrequirements-dev.txt +commands = + py.test --cov=bookshelf --no-success-flaky-report -m "not e2e" {posargs: tests} +passenv = GOOGLE_APPLICATION_CREDENTIALS DATASTORE_HOST E2E_URL +setenv = PYTHONPATH={toxinidir} + + +[testenv:py27-e2e] +basepython = python2.7 +commands = + py.test --no-success-flaky-report -m "e2e" {posargs: tests} + +[testenv:py36-e2e] +basepython = python3.6 +commands = + py.test --no-success-flaky-report -m "e2e" {posargs: tests} + +[testenv:lint] +deps = + flake8 + flake8-import-order +commands = + flake8 --import-order-style=google bookshelf tests From 81615bffb607d518834379465a256d10c5f772b4 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Tue, 3 Dec 2019 19:19:43 +0100 Subject: [PATCH 097/395] Add renovate.json (#209) --- renovate.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 renovate.json diff --git a/renovate.json b/renovate.json new file mode 100644 index 00000000..f45d8f11 --- /dev/null +++ b/renovate.json @@ -0,0 +1,5 @@ +{ + "extends": [ + "config:base" + ] +} From 7d21da46d3770a1618ff71c2b3d0b0f07bb677e8 Mon Sep 17 00:00:00 2001 From: DPEBot Date: Tue, 3 Dec 2019 10:20:13 -0800 Subject: [PATCH 098/395] Auto-update dependencies. (#186) --- authenticating-users/requirements.txt | 6 +++--- sessions/requirements.txt | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index 464a3752..17d4e850 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -1,6 +1,6 @@ # [START getting_started_requirements] Flask>=1.0.0 -cryptography -python-jose[cryptography] -requests +cryptography==2.8 +python-jose[cryptography]==3.0.1 +requests==2.22.0 # [END getting_started_requirements] diff --git a/sessions/requirements.txt b/sessions/requirements.txt index 6bbf2aa4..1ce2ff95 100644 --- a/sessions/requirements.txt +++ b/sessions/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-firestore==1.4.0 +google-cloud-firestore==1.6.0 flask>=1.0.0 From d1a30bb28a18a10a2a8c9d4b6166fe690f33431e Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Tue, 3 Dec 2019 22:19:37 +0100 Subject: [PATCH 099/395] Pin dependencies (#212) --- authenticating-users/requirements.txt | 2 +- background/app/requirements.txt | 2 +- optional-kubernetes-engine/requirements.txt | 4 ++-- requirements.txt | 4 ++-- sessions/requirements.txt | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index 17d4e850..b754d36e 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -1,5 +1,5 @@ # [START getting_started_requirements] -Flask>=1.0.0 +Flask==1.1.1 cryptography==2.8 python-jose[cryptography]==3.0.1 requests==2.22.0 diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 78b2e87d..30b96493 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ google-cloud-firestore==1.4.0 google-cloud-pubsub==0.45.0 -flask>=1.0.0 +flask==1.1.1 diff --git a/optional-kubernetes-engine/requirements.txt b/optional-kubernetes-engine/requirements.txt index 5d998499..39f98ac9 100644 --- a/optional-kubernetes-engine/requirements.txt +++ b/optional-kubernetes-engine/requirements.txt @@ -1,4 +1,4 @@ -Flask>=1.0.0 +Flask==1.1.1 google-cloud-datastore==1.7.1 google-cloud-storage==1.13.0 google-cloud-logging==1.8.0 @@ -8,7 +8,7 @@ oauth2client==4.1.3 mock==2.0.0 Flask-SQLAlchemy==2.3.2 PyMySQL==0.9.2 -Flask-PyMongo>=2.0.0 +Flask-PyMongo==2.3.0 PyMongo==3.7.2 six==1.11.0 requests[security]==2.20.1 diff --git a/requirements.txt b/requirements.txt index 4b0a8521..98ea66dd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,8 +2,8 @@ BeautifulSoup4==4.6.0 flake8==3.5.0 flaky==3.4.0 mock==2.0.0 -nox>=2018.10.9 +nox==2019.11.9 pytest==3.8.2 pytest-cov==2.5.1 retrying==1.3.3 -requests>=2.20.0 +requests==2.22.0 diff --git a/sessions/requirements.txt b/sessions/requirements.txt index 1ce2ff95..88580978 100644 --- a/sessions/requirements.txt +++ b/sessions/requirements.txt @@ -1,2 +1,2 @@ google-cloud-firestore==1.6.0 -flask>=1.0.0 +flask==1.1.1 From a0b538079678b32ae0bbd6398f8c008af775342f Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Tue, 3 Dec 2019 22:21:07 +0100 Subject: [PATCH 100/395] Update dependency flake8 to v3.7.9 (#218) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 98ea66dd..3ce34a76 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ BeautifulSoup4==4.6.0 -flake8==3.5.0 +flake8==3.7.9 flaky==3.4.0 mock==2.0.0 nox==2019.11.9 From 114e7b7101d52336f7e1353fd43197086c39552c Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Tue, 3 Dec 2019 13:29:16 -0800 Subject: [PATCH 101/395] clean up root requirements.txt (#214) --- pytest.ini | 1 - requirements.txt | 5 ----- 2 files changed, 6 deletions(-) diff --git a/pytest.ini b/pytest.ini index 2008cdfa..eb509ac0 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,6 +1,5 @@ [pytest] addopts = -v - --no-success-flaky-report --tb=native norecursedirs = .git env lib .tox .nox diff --git a/requirements.txt b/requirements.txt index 3ce34a76..27ed8d8a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,4 @@ -BeautifulSoup4==4.6.0 flake8==3.7.9 -flaky==3.4.0 -mock==2.0.0 nox==2019.11.9 pytest==3.8.2 -pytest-cov==2.5.1 -retrying==1.3.3 requests==2.22.0 From 0c4201c3b73f35edfe271612168cb3ba97083016 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Wed, 4 Dec 2019 22:54:42 +0100 Subject: [PATCH 102/395] Update dependency google-cloud-firestore to v1.6.0 (#222) --- background/app/requirements.txt | 2 +- background/function/requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 30b96493..96de7b4a 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ -google-cloud-firestore==1.4.0 +google-cloud-firestore==1.6.0 google-cloud-pubsub==0.45.0 flask==1.1.1 diff --git a/background/function/requirements.txt b/background/function/requirements.txt index fef973c8..91e5b093 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ google-cloud-translate==1.6.0 -google-cloud-firestore==1.4.0 +google-cloud-firestore==1.6.0 From b436a88a916ebbd619b67909a2a7625c56e464fb Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Wed, 4 Dec 2019 22:55:06 +0100 Subject: [PATCH 103/395] Update dependency google-cloud-storage to v1.23.0 (#224) --- optional-kubernetes-engine/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/optional-kubernetes-engine/requirements.txt b/optional-kubernetes-engine/requirements.txt index 39f98ac9..ada0ba0d 100644 --- a/optional-kubernetes-engine/requirements.txt +++ b/optional-kubernetes-engine/requirements.txt @@ -1,6 +1,6 @@ Flask==1.1.1 google-cloud-datastore==1.7.1 -google-cloud-storage==1.13.0 +google-cloud-storage==1.23.0 google-cloud-logging==1.8.0 google-cloud-error_reporting==0.30.0 gunicorn==19.9.0 From 09cacba033254f4417e4f97f3d9b072fa21e43d7 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Wed, 4 Dec 2019 22:55:48 +0100 Subject: [PATCH 104/395] Update dependency google-cloud-translate to v1.7.0 (#225) --- background/function/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/function/requirements.txt b/background/function/requirements.txt index 91e5b093..6dce9f78 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-translate==1.6.0 +google-cloud-translate==1.7.0 google-cloud-firestore==1.6.0 From f89dcd5c8d5b93c5760a86c49e0840fe3e681434 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Wed, 4 Dec 2019 22:56:44 +0100 Subject: [PATCH 105/395] Update dependency pytest to v3.10.1 (#228) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 27ed8d8a..e8216097 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ flake8==3.7.9 nox==2019.11.9 -pytest==3.8.2 +pytest==3.10.1 requests==2.22.0 From 3bf1182acf2dd16834ee9f89d1a4e6b47a1ec4cf Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Wed, 4 Dec 2019 22:57:31 +0100 Subject: [PATCH 106/395] Update dependency google-cloud-pubsub to v1 (#232) --- background/app/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 96de7b4a..a7cd28d6 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ google-cloud-firestore==1.6.0 -google-cloud-pubsub==0.45.0 +google-cloud-pubsub==1.0.2 flask==1.1.1 From af7d6a542978a1508ee079b15a09d96f0ae78810 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Wed, 4 Dec 2019 22:58:07 +0100 Subject: [PATCH 107/395] Update dependency requests to v2.22.0 (#229) --- optional-kubernetes-engine/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/optional-kubernetes-engine/requirements.txt b/optional-kubernetes-engine/requirements.txt index ada0ba0d..ed6dd546 100644 --- a/optional-kubernetes-engine/requirements.txt +++ b/optional-kubernetes-engine/requirements.txt @@ -11,6 +11,6 @@ PyMySQL==0.9.2 Flask-PyMongo==2.3.0 PyMongo==3.7.2 six==1.11.0 -requests[security]==2.20.1 +requests[security]==2.22.0 honcho==1.0.1 psq==0.7.0 From c626a2ccb96ad10387c80d5f52886d632c94c385 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Wed, 4 Dec 2019 22:58:21 +0100 Subject: [PATCH 108/395] Update dependency google-cloud-translate to v2 (#233) --- background/function/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/function/requirements.txt b/background/function/requirements.txt index 6dce9f78..2aab9b14 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-translate==1.7.0 +google-cloud-translate==2.0.0 google-cloud-firestore==1.6.0 From a0a3b992ca83e66c3247a90f1d1c3d3f03921151 Mon Sep 17 00:00:00 2001 From: rohitshah-tudip <31612944+rohitshah-tudip@users.noreply.github.com> Date: Thu, 5 Dec 2019 03:30:16 +0530 Subject: [PATCH 109/395] Rollback flask-version-update (#237) --- optional-kubernetes-engine/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/optional-kubernetes-engine/requirements.txt b/optional-kubernetes-engine/requirements.txt index ed6dd546..1e998c54 100644 --- a/optional-kubernetes-engine/requirements.txt +++ b/optional-kubernetes-engine/requirements.txt @@ -1,4 +1,4 @@ -Flask==1.1.1 +Flask==1.0.4 google-cloud-datastore==1.7.1 google-cloud-storage==1.23.0 google-cloud-logging==1.8.0 From 5a053a027c896e3655c1a65454eae27b3b44b403 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 4 Dec 2019 14:04:51 -0800 Subject: [PATCH 110/395] Ignore dependencies in `optional-kubernetes-engine` --- renovate.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/renovate.json b/renovate.json index f45d8f11..07729f34 100644 --- a/renovate.json +++ b/renovate.json @@ -1,5 +1,8 @@ { "extends": [ "config:base" + ], + "ignorePaths": [ + "optional-kubernetes-engine" ] } From 1c9dfd122335ca1596ae5e4e5774a80de302da90 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Fri, 6 Dec 2019 15:53:59 -0800 Subject: [PATCH 111/395] Python Getting Started "Bookshelf" app (#210) --- .gitignore | 1 + bookshelf/app.yaml | 2 + bookshelf/firestore.py | 69 ++++++++++++++ bookshelf/images/moby-dick.png | Bin 0 -> 336311 bytes bookshelf/main.py | 153 +++++++++++++++++++++++++++++ bookshelf/main_test.py | 169 +++++++++++++++++++++++++++++++++ bookshelf/requirements.txt | 7 ++ bookshelf/storage.py | 73 ++++++++++++++ bookshelf/templates/base.html | 40 ++++++++ bookshelf/templates/form.html | 59 ++++++++++++ bookshelf/templates/list.html | 61 ++++++++++++ bookshelf/templates/view.html | 54 +++++++++++ noxfile.py | 3 +- 13 files changed, 689 insertions(+), 2 deletions(-) create mode 100644 bookshelf/app.yaml create mode 100644 bookshelf/firestore.py create mode 100755 bookshelf/images/moby-dick.png create mode 100644 bookshelf/main.py create mode 100644 bookshelf/main_test.py create mode 100644 bookshelf/requirements.txt create mode 100644 bookshelf/storage.py create mode 100644 bookshelf/templates/base.html create mode 100644 bookshelf/templates/form.html create mode 100644 bookshelf/templates/list.html create mode 100644 bookshelf/templates/view.html diff --git a/.gitignore b/.gitignore index fe877062..1e32d951 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ htmlcov/ .cache nosetests.xml coverage.xml +*_log.xml *,cover sponge_log.xml diff --git a/bookshelf/app.yaml b/bookshelf/app.yaml new file mode 100644 index 00000000..ac4c378f --- /dev/null +++ b/bookshelf/app.yaml @@ -0,0 +1,2 @@ +runtime: python37 + diff --git a/bookshelf/firestore.py b/bookshelf/firestore.py new file mode 100644 index 00000000..43f18b25 --- /dev/null +++ b/bookshelf/firestore.py @@ -0,0 +1,69 @@ +# Copyright 2019 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START bookshelf_firestore_client_import] +from google.cloud import firestore +# [END bookshelf_firestore_client_import] + + +def document_to_dict(doc): + if not doc.exists: + return None + doc_dict = doc.to_dict() + doc_dict['id'] = doc.id + return doc_dict + + +def next_page(limit=10, start_after=None): + db = firestore.Client() + + query = db.collection(u'Book').limit(limit).order_by(u'title') + + if start_after: + # Construct a new query starting at this document. + query = query.start_after({u'title': start_after}) + + docs = query.stream() + docs = list(map(document_to_dict, docs)) + + last_title = None + if limit == len(docs): + # Get the last document from the results and set as the last title. + last_title = docs[-1][u'title'] + return docs, last_title + + +def read(book_id): + # [START bookshelf_firestore_client] + db = firestore.Client() + book_ref = db.collection(u'Book').document(book_id) + snapshot = book_ref.get() + # [END bookshelf_firestore_client] + return document_to_dict(snapshot) + + +def update(data, book_id=None): + db = firestore.Client() + book_ref = db.collection(u'Book').document(book_id) + book_ref.set(data) + return document_to_dict(book_ref.get()) + + +create = update + + +def delete(id): + db = firestore.Client() + book_ref = db.collection(u'Book').document(id) + book_ref.delete() diff --git a/bookshelf/images/moby-dick.png b/bookshelf/images/moby-dick.png new file mode 100755 index 0000000000000000000000000000000000000000..ef789cdb995897176df06bd9881b1afdc0972149 GIT binary patch literal 336311 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�$G}kKauvPB)NjTI?zkrEM^96d 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Ξp3NtH|t8{GM8<9m!W>_rIAzGGr@Q${Ck?X=q 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 literal 0 HcmV?d00001 diff --git a/bookshelf/main.py b/bookshelf/main.py new file mode 100644 index 00000000..1962c485 --- /dev/null +++ b/bookshelf/main.py @@ -0,0 +1,153 @@ +# Copyright 2019 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +import firestore +from flask import current_app, flash, Flask, Markup, redirect, render_template +from flask import request, url_for +from google.cloud import error_reporting +import google.cloud.logging +import storage + + +# [START upload_image_file] +def upload_image_file(img): + """ + Upload the user-uploaded file to Google Cloud Storage and retrieve its + publicly-accessible URL. + """ + if not img: + return None + + public_url = storage.upload_file( + img.read(), + img.filename, + img.content_type + ) + + current_app.logger.info( + 'Uploaded file %s as %s.', img.filename, public_url) + + return public_url +# [END upload_image_file] + + +app = Flask(__name__) +app.config.update( + SECRET_KEY='secret', + MAX_CONTENT_LENGTH=8 * 1024 * 1024, + ALLOWED_EXTENSIONS=set(['png', 'jpg', 'jpeg', 'gif']) +) + +app.debug = False +app.testing = False + +# Configure logging +if not app.testing: + logging.basicConfig(level=logging.INFO) + client = google.cloud.logging.Client() + # Attaches a Google Stackdriver logging handler to the root logger + client.setup_logging(logging.INFO) + + +@app.route('/') +def list(): + start_after = request.args.get('start_after', None) + books, last_title = firestore.next_page(start_after=start_after) + + return render_template('list.html', books=books, last_title=last_title) + + +@app.route('/books/') +def view(book_id): + book = firestore.read(book_id) + return render_template('view.html', book=book) + + +@app.route('/books/add', methods=['GET', 'POST']) +def add(): + if request.method == 'POST': + data = request.form.to_dict(flat=True) + + # If an image was uploaded, update the data to point to the new image. + image_url = upload_image_file(request.files.get('image')) + + if image_url: + data['imageUrl'] = image_url + + book = firestore.create(data) + + return redirect(url_for('.view', book_id=book['id'])) + + return render_template('form.html', action='Add', book={}) + + +@app.route('/books//edit', methods=['GET', 'POST']) +def edit(book_id): + book = firestore.read(book_id) + + if request.method == 'POST': + data = request.form.to_dict(flat=True) + + # If an image was uploaded, update the data to point to the new image. + image_url = upload_image_file(request.files.get('image')) + + if image_url: + data['imageUrl'] = image_url + + book = firestore.update(data, book_id) + + return redirect(url_for('.view', book_id=book['id'])) + + return render_template('form.html', action='Edit', book=book) + + +@app.route('/books//delete') +def delete(book_id): + firestore.delete(book_id) + return redirect(url_for('.list')) + + +@app.route('/logs') +def logs(): + logging.info('Hey, you triggered a custom log entry. Good job!') + flash(Markup('''You triggered a custom log entry. You can view it in the + Cloud Console''')) + return redirect(url_for('.list')) + + +@app.route('/errors') +def errors(): + raise Exception('This is an intentional exception.') + + +# Add an error handler that reports exceptions to Stackdriver Error +# Reporting. Note that this error handler is only used when debug +# is False +@app.errorhandler(500) +def server_error(e): + client = error_reporting.Client() + client.report_exception( + http_context=error_reporting.build_flask_context(request)) + return """ + An internal error occurred:

{}
+ See logs for full stacktrace. + """.format(e), 500 + + +# This is only used when running locally. When running live, gunicorn runs +# the application. +if __name__ == '__main__': + app.run(host='127.0.0.1', port=8080, debug=True) diff --git a/bookshelf/main_test.py b/bookshelf/main_test.py new file mode 100644 index 00000000..404de778 --- /dev/null +++ b/bookshelf/main_test.py @@ -0,0 +1,169 @@ +# Copyright 2019 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import re + +import google.auth +import main +import pytest +import requests +from six import BytesIO + + +credentials, project_id = google.auth.default() +os.environ['GOOGLE_CLOUD_PROJECT'] = project_id + + +@pytest.yield_fixture +def app(request): + """This fixture provides a Flask app instance configured for testing. + + It also ensures the tests run within a request context, allowing + any calls to flask.request, flask.current_app, etc. to work.""" + app = main.app + + with app.test_request_context(): + yield app + + +@pytest.yield_fixture +def firestore(): + + import firestore + """This fixture provides a modified version of the app's Firebase model that + tracks all created items and deletes them at the end of the test. + + Any tests that directly or indirectly interact with the database should use + this to ensure that resources are properly cleaned up. + """ + + # Ensure no books exist before running the tests. This typically helps if + # tests somehow left the database in a bad state. + delete_all_books(firestore) + + yield firestore + + # Delete all books that we created during tests. + delete_all_books(firestore) + + +def delete_all_books(firestore): + while True: + books, _ = firestore.next_page(limit=50) + if not books: + break + for book in books: + firestore.delete(book['id']) + + +def test_list(app, firestore): + for i in range(1, 12): + firestore.create({'title': u'Book {0}'.format(i)}) + + with app.test_client() as c: + rv = c.get('/') + + assert rv.status == '200 OK' + + body = rv.data.decode('utf-8') + assert 'Book 1' in body, "Should show books" + assert len(re.findall('

Book', body)) <= 10, ( + "Should not show more than 10 books") + assert 'More' in body, "Should have more than one page" + + +def test_add(app): + data = { + 'title': 'Test Book', + 'author': 'Test Author', + 'publishedDate': 'Test Date Published', + 'description': 'Test Description' + } + + with app.test_client() as c: + rv = c.post('books/add', data=data, follow_redirects=True) + + assert rv.status == '200 OK' + body = rv.data.decode('utf-8') + assert 'Test Book' in body + assert 'Test Author' in body + assert 'Test Date Published' in body + assert 'Test Description' in body + + +def test_edit(app, firestore): + existing = firestore.create({'title': "Temp Title"}) + + with app.test_client() as c: + rv = c.post( + 'books/%s/edit' % existing['id'], + data={'title': 'Updated Title'}, + follow_redirects=True) + + assert rv.status == '200 OK' + body = rv.data.decode('utf-8') + assert 'Updated Title' in body + assert 'Temp Title' not in body + + +def test_delete(app, firestore): + existing = firestore.create({'title': "Temp Title"}) + + with app.test_client() as c: + rv = c.get( + 'books/%s/delete' % existing['id'], + follow_redirects=True) + + assert rv.status == '200 OK' + assert not firestore.read(existing['id']) + + +def test_upload_image(app): + data = { + 'title': 'Test Book', + 'author': 'Test Author', + 'publishedDate': 'Test Date Published', + 'description': 'Test Description', + 'image': (BytesIO(b'hello world'), 'hello.jpg') + } + + with app.test_client() as c: + rv = c.post('books/add', data=data, follow_redirects=True) + + assert rv.status == '200 OK' + body = rv.data.decode('utf-8') + + img_tag = re.search(''), + '1337h4x0r.php') + } + + with app.test_client() as c: + rv = c.post('/books/add', data=data, follow_redirects=True) + + # check we weren't pwned + assert rv.status == '400 BAD REQUEST' diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt new file mode 100644 index 00000000..73b26e14 --- /dev/null +++ b/bookshelf/requirements.txt @@ -0,0 +1,7 @@ +Flask>=1.0.0 +google-cloud-firestore==1.6.0 +google-cloud-storage==1.23.0 +google-cloud-logging==1.14.0 +google-cloud-error-reporting==0.33.0 +gunicorn==19.9.0 +six==1.11.0 diff --git a/bookshelf/storage.py b/bookshelf/storage.py new file mode 100644 index 00000000..53a1c567 --- /dev/null +++ b/bookshelf/storage.py @@ -0,0 +1,73 @@ +# Copyright 2019 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import + +import datetime +import os + +from flask import current_app +from google.cloud import storage +import six +from werkzeug import secure_filename +from werkzeug.exceptions import BadRequest + + +def _check_extension(filename, allowed_extensions): + file, ext = os.path.splitext(filename) + if (ext.replace('.', '') not in allowed_extensions): + raise BadRequest( + '{0} has an invalid name or extension'.format(filename)) + + +def _safe_filename(filename): + """ + Generates a safe filename that is unlikely to collide with existing objects + in Google Cloud Storage. + + ``filename.ext`` is transformed into ``filename-YYYY-MM-DD-HHMMSS.ext`` + """ + filename = secure_filename(filename) + date = datetime.datetime.utcnow().strftime("%Y-%m-%d-%H%M%S") + basename, extension = filename.rsplit('.', 1) + return "{0}-{1}.{2}".format(basename, date, extension) + + +def upload_file(file_stream, filename, content_type): + """ + Uploads a file to a given Cloud Storage bucket and returns the public url + to the new object. + """ + _check_extension(filename, current_app.config['ALLOWED_EXTENSIONS']) + filename = _safe_filename(filename) + + bucketname = os.getenv('GOOGLE_STORAGE_BUCKET') or os.getenv( + 'GOOGLE_CLOUD_PROJECT') + '_bucket' + + # [START bookshelf_cloud_storage_client] + client = storage.Client() + bucket = client.bucket(bucketname) + blob = bucket.blob(filename) + + blob.upload_from_string( + file_stream, + content_type=content_type) + + url = blob.public_url + # [END bookshelf_cloud_storage_client] + + if isinstance(url, six.binary_type): + url = url.decode('utf-8') + + return url diff --git a/bookshelf/templates/base.html b/bookshelf/templates/base.html new file mode 100644 index 00000000..0c273faf --- /dev/null +++ b/bookshelf/templates/base.html @@ -0,0 +1,40 @@ +{# +# Copyright 2019 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#} + + + + Bookshelf - Python on Google Cloud Platform + + + + + + +
+ {% block content %}{% endblock %} +
+ {{user}} + + diff --git a/bookshelf/templates/form.html b/bookshelf/templates/form.html new file mode 100644 index 00000000..64950d2e --- /dev/null +++ b/bookshelf/templates/form.html @@ -0,0 +1,59 @@ +{# +# Copyright 2019 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#} + +{# [START form] #} +{% extends "base.html" %} + +{% block content %} +

{{action}} book

+ +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + + +
+ +{% endblock %} +{# [END form] #} diff --git a/bookshelf/templates/list.html b/bookshelf/templates/list.html new file mode 100644 index 00000000..ffbad499 --- /dev/null +++ b/bookshelf/templates/list.html @@ -0,0 +1,61 @@ +{# +# Copyright 2019 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#} + +{% extends "base.html" %} + +{% block content %} + +{% with messages = get_flashed_messages() %} + {% for message in messages %} +

{{ message }}

+ {% endfor %} +{% endwith %} + +

Books

+ + + Add book + + +{% for book in books %} + +{% else %} +

No books found

+{% endfor %} + +{% if last_title %} + +{% endif %} + +{% endblock %} diff --git a/bookshelf/templates/view.html b/bookshelf/templates/view.html new file mode 100644 index 00000000..70ac5a49 --- /dev/null +++ b/bookshelf/templates/view.html @@ -0,0 +1,54 @@ +{# +# Copyright 2019 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#} + +{% extends "base.html" %} + +{% block content %} + +

Book

+ + + +
+ {# [START book_image] #} +
+ {% if book.imageUrl %} + + {% else %} + + {% endif %} +
+ {# [END book_image] #} +
+

+ {{book.title}} + {{book.publishedDate}} +

+
By {{book.author|default('Unknown', True)}}
+

{{book.description}}

+
+
+ +{% endblock %} diff --git a/noxfile.py b/noxfile.py index 6b81a427..5aea04b5 100644 --- a/noxfile.py +++ b/noxfile.py @@ -7,12 +7,11 @@ 'git+https://github.com/GoogleCloudPlatform/python-repo-tools.git' DIRS = [ - # Hello world doesn't have system tests, just a lint test which will be - # covered by the global lint here. 'authenticating-users', 'background/app', 'background/function', 'sessions', + 'bookshelf', ] PYTEST_COMMON_ARGS = ['--junitxml=sponge_log.xml', '-m', 'not e2e'] From 97d7ddcf5ef10e6026086f1a30850ba204a5de41 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 11 Dec 2019 10:48:55 -0800 Subject: [PATCH 112/395] Fixes translate bug from upgrading to v2 (#244) --- background/function/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/function/main.py b/background/function/main.py index e2660aae..ccf94244 100644 --- a/background/function/main.py +++ b/background/function/main.py @@ -28,7 +28,7 @@ import json from google.cloud import firestore -from google.cloud import translate +from google.cloud import translate_v2 as translate # [END getting_started_background_translate_setup] # [START getting_started_background_translate_init] From 377e2c64a63b0c2f3a269a0572b0aebf576b6a75 Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Wed, 11 Dec 2019 12:36:05 -0800 Subject: [PATCH 113/395] On-ramp GCE sample (#205) --- gce/README.md | 7 +++++++ gce/deploy.sh | 35 +++++++++++++++++++++++++++++++ gce/main.py | 25 ++++++++++++++++++++++ gce/procfile | 1 + gce/python-app.conf | 11 ++++++++++ gce/requirements.txt | 3 +++ gce/startup-script.sh | 49 +++++++++++++++++++++++++++++++++++++++++++ gce/teardown.sh | 25 ++++++++++++++++++++++ noxfile.py | 1 + 9 files changed, 157 insertions(+) create mode 100644 gce/README.md create mode 100644 gce/deploy.sh create mode 100644 gce/main.py create mode 100644 gce/procfile create mode 100644 gce/python-app.conf create mode 100644 gce/requirements.txt create mode 100644 gce/startup-script.sh create mode 100644 gce/teardown.sh diff --git a/gce/README.md b/gce/README.md new file mode 100644 index 00000000..d7c9f1ff --- /dev/null +++ b/gce/README.md @@ -0,0 +1,7 @@ +# Hello World for Python on Google Compute Engine + + This folder contains the sample code for the [Deploying to Google Compute Engine][tutorial-gce] +tutorial. Please refer to the tutorial for instructions on configuring, running, +and deploying this sample. + + [tutorial-gce]: https://cloud.google.com/python/tutorials/getting-started-on-compute-engine diff --git a/gce/deploy.sh b/gce/deploy.sh new file mode 100644 index 00000000..b3449837 --- /dev/null +++ b/gce/deploy.sh @@ -0,0 +1,35 @@ +# Copyright 2019 Google LLC All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -ex + +# [START getting_started_gce_create_instance] +MY_INSTANCE_NAME="my-app-instance" +ZONE=us-central1-a + +gcloud compute instances create $MY_INSTANCE_NAME \ + --image-family=debian-9 \ + --image-project=debian-cloud \ + --machine-type=g1-small \ + --scopes userinfo-email,cloud-platform \ + --metadata-from-file startup-script=startup-script.sh \ + --zone $ZONE \ + --tags http-server +# [END getting_started_gce_create_instance] + +gcloud compute firewall-rules create default-allow-http-8080 \ + --allow tcp:8080 \ + --source-ranges 0.0.0.0/0 \ + --target-tags http-server \ + --description "Allow port 8080 access to http-server" diff --git a/gce/main.py b/gce/main.py new file mode 100644 index 00000000..f4435efc --- /dev/null +++ b/gce/main.py @@ -0,0 +1,25 @@ +# Copyright 2019 Google LLC All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from flask import Flask +app = Flask(__name__) + + +@app.route('/', methods=['GET']) +def say_hello(): + return "Hello, world!" + + +if __name__ == '__main__': + app.run(host='127.0.0.1', port=8080, debug=True) diff --git a/gce/procfile b/gce/procfile new file mode 100644 index 00000000..9d9842b4 --- /dev/null +++ b/gce/procfile @@ -0,0 +1 @@ +hello: /opt/app/gce/env/bin/gunicorn -b 0.0.0.0:8080 main:app diff --git a/gce/python-app.conf b/gce/python-app.conf new file mode 100644 index 00000000..d3a9b17b --- /dev/null +++ b/gce/python-app.conf @@ -0,0 +1,11 @@ +[program:pythonapp] +directory=/opt/app/gce +command=/opt/app/gce/env/bin/honcho start -f ./procfile hello +autostart=true +autorestart=true +user=pythonapp +# Environment variables ensure that the application runs inside of the +# configured virtualenv. +environment=VIRTUAL_ENV="/opt/app/gce/env",PATH="/opt/app/gce/env/bin",HOME="/home/pythonapp",USER="pythonapp" +stdout_logfile=syslog +stderr_logfile=syslog diff --git a/gce/requirements.txt b/gce/requirements.txt new file mode 100644 index 00000000..f561b3c0 --- /dev/null +++ b/gce/requirements.txt @@ -0,0 +1,3 @@ +flask==1.1.1 +honcho==1.0.1 +gunicorn==19.9.0 diff --git a/gce/startup-script.sh b/gce/startup-script.sh new file mode 100644 index 00000000..2068606e --- /dev/null +++ b/gce/startup-script.sh @@ -0,0 +1,49 @@ +# Copyright 2019 Google LLC All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Echo commands +set -v + +# [START getting_started_gce_startup_script] +# Install Stackdriver logging agent +curl -sSO https://dl.google.com/cloudagents/install-logging-agent.sh +sudo bash install-logging-agent.sh + +# Install or update needed software +apt-get update +apt-get install -yq git supervisor python python-pip +pip install --upgrade pip virtualenv + +# Account to own server process +useradd -m -d /home/pythonapp pythonapp + +# Fetch source code +export HOME=/root +git clone https://github.com/GoogleCloudPlatform/getting-started-python.git /opt/app + +# Python environment setup +virtualenv -p python3 /opt/app/gce/env +source /opt/app/gce/env/bin/activate +/opt/app/gce/env/bin/pip install -r /opt/app/gce/requirements.txt + +# Set ownership to newly created account +chown -R pythonapp:pythonapp /opt/app + +# Put supervisor configuration in proper place +cp /opt/app/gce/python-app.conf /etc/supervisor/conf.d/python-app.conf + +# Start service via supervisorctl +supervisorctl reread +supervisorctl update +# [END getting_started_gce_startup_script] diff --git a/gce/teardown.sh b/gce/teardown.sh new file mode 100644 index 00000000..b31d1139 --- /dev/null +++ b/gce/teardown.sh @@ -0,0 +1,25 @@ + #! /bin/bash + +# Copyright 2019 Google LLC All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -x + +MY_INSTANCE_NAME="my-instance-name" +ZONE=us-central1-f + +gcloud compute instances delete $MY_INSTANCE_NAME \ + --zone=$ZONE --delete-disks=all + +gcloud compute firewall-rules delete default-allow-http-8080 diff --git a/noxfile.py b/noxfile.py index 5aea04b5..8d7c7d5e 100644 --- a/noxfile.py +++ b/noxfile.py @@ -10,6 +10,7 @@ 'authenticating-users', 'background/app', 'background/function', + 'gce', 'sessions', 'bookshelf', ] From 735a3f7ef32b23e76f2ddb7572d30772d3829494 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Fri, 13 Dec 2019 11:42:02 -0800 Subject: [PATCH 114/395] Misc session tutorial app fixes (#245) --- sessions/main.py | 4 +++- sessions/main_test.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/sessions/main.py b/sessions/main.py index 6db064ae..62871ba1 100644 --- a/sessions/main.py +++ b/sessions/main.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +# [START getting_started_sessions_all] import random from uuid import uuid4 @@ -60,7 +61,7 @@ def get_session_data(transaction, session_id): @app.route('/', methods=['GET']) def home(): - template = '{} views for {}' + template = '{} views for "{}"' transaction = db.transaction() session = get_session_data(transaction, request.cookies.get('session_id')) @@ -76,3 +77,4 @@ def home(): if __name__ == '__main__': app.run(host='127.0.0.1', port=8080) +# [END getting_started_sessions_all] diff --git a/sessions/main_test.py b/sessions/main_test.py index 8fbf79c4..06965c08 100644 --- a/sessions/main_test.py +++ b/sessions/main_test.py @@ -43,7 +43,7 @@ def test_session(client): data = r.data.decode('utf-8') assert '1 views' in data - match = re.search('views for ([A-Za-z ]+)', data) + match = re.search('views for "([A-Za-z ]+)"', data) assert match is not None greeting = match.group(1) From 495dbbac65740e3961dc28c6a001bef86d8ca9a0 Mon Sep 17 00:00:00 2001 From: Risto Mononen Date: Wed, 25 Dec 2019 12:31:03 +0200 Subject: [PATCH 115/395] Use https for the kitten images --- bookshelf/templates/list.html | 2 +- bookshelf/templates/view.html | 2 +- optional-kubernetes-engine/bookshelf/templates/list.html | 2 +- optional-kubernetes-engine/bookshelf/templates/view.html | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bookshelf/templates/list.html b/bookshelf/templates/list.html index ffbad499..b255d209 100644 --- a/bookshelf/templates/list.html +++ b/bookshelf/templates/list.html @@ -37,7 +37,7 @@

Books

{% if book.imageUrl %} {% else %} - + {% endif %}

diff --git a/bookshelf/templates/view.html b/bookshelf/templates/view.html index 70ac5a49..476db111 100644 --- a/bookshelf/templates/view.html +++ b/bookshelf/templates/view.html @@ -37,7 +37,7 @@

Book

{% if book.imageUrl %} {% else %} - + {% endif %}
{# [END book_image] #} diff --git a/optional-kubernetes-engine/bookshelf/templates/list.html b/optional-kubernetes-engine/bookshelf/templates/list.html index 3362f0e2..a80d5b56 100644 --- a/optional-kubernetes-engine/bookshelf/templates/list.html +++ b/optional-kubernetes-engine/bookshelf/templates/list.html @@ -31,7 +31,7 @@

Books

{% if book.imageUrl %} {% else %} - + {% endif %}
diff --git a/optional-kubernetes-engine/bookshelf/templates/view.html b/optional-kubernetes-engine/bookshelf/templates/view.html index e654e8ab..2f337229 100644 --- a/optional-kubernetes-engine/bookshelf/templates/view.html +++ b/optional-kubernetes-engine/bookshelf/templates/view.html @@ -36,7 +36,7 @@

Book

{% if book.imageUrl %} {% else %} - + {% endif %}
From 93fffe9e6528f6cd5d8ec170e4e5e2e3b2775903 Mon Sep 17 00:00:00 2001 From: Swati Murarka Date: Sat, 28 Dec 2019 03:37:16 +0530 Subject: [PATCH 116/395] Downgrade the requests[security] version from 2.22.0 to 2.21.0. (#247) --- optional-kubernetes-engine/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/optional-kubernetes-engine/requirements.txt b/optional-kubernetes-engine/requirements.txt index 1e998c54..33ba8279 100644 --- a/optional-kubernetes-engine/requirements.txt +++ b/optional-kubernetes-engine/requirements.txt @@ -11,6 +11,6 @@ PyMySQL==0.9.2 Flask-PyMongo==2.3.0 PyMongo==3.7.2 six==1.11.0 -requests[security]==2.22.0 +requests[security]==2.21.0 honcho==1.0.1 psq==0.7.0 From 6dddbe0a857b1d9eca45228a5fbbf2232e97e412 Mon Sep 17 00:00:00 2001 From: Graham Paye Date: Mon, 3 Feb 2020 14:03:27 -0800 Subject: [PATCH 117/395] update readme to reflect bookshelf rewrite (#252) --- README.md | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 85082b86..d86cd082 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,22 @@ # Getting started with Python on Google Cloud Platform -This repository is the complete sample code for the [Python Getting Started on Google Cloud Platform](http://cloud.google.com/python) tutorials. Please refer to the tutorials for instructions on configuring, running, and deploying these samples. +This repository is the complete sample code for the [Python Getting Started on Google Cloud Platform](https://cloud.google.com/python/docs/) tutorials. Please refer to the tutorials for instructions on configuring, running, and deploying these samples. The code for the samples is contained in individual folders in this repository. -Note that 7-gce is the final version of the project, including the complete Bookshelf app on Managed VMs in addition to scripts to alternatively deploy on GCE. - - Tutorial | Folder ---------|------- -[Hello world](https://cloud.google.com/python/getting-started/hello-world) | [1-hello-world](https://github.com/GoogleCloudPlatform/getting-started-python/tree/master/1-hello-world) -[Structured data](https://cloud.google.com/python/getting-started/using-structured-data) | [2-structured-data](https://github.com/GoogleCloudPlatform/getting-started-python/tree/master/2-structured-data) -[Cloud Storage](https://cloud.google.com/python/getting-started/using-cloud-storage) | [3-binary-data](https://github.com/GoogleCloudPlatform/getting-started-python/tree/master/3-binary-data) -[Authenticating users](https://cloud.google.com/python/getting-started/authenticate-users) | [4-auth](https://github.com/GoogleCloudPlatform/getting-started-python/tree/master/4-auth) -[Logging app events](https://cloud.google.com/python/monitor-and-debug/logging-application-events) | [5-logging](https://github.com/GoogleCloudPlatform/getting-started-python/tree/master/5-logging) -[Using Cloud Pub/Sub](https://cloud.google.com/python/getting-started/using-pub-sub) | [6-pubsub](https://github.com/GoogleCloudPlatform/getting-started-python/tree/master/6-pubsub) -[Deploying to Google Compute Engine](https://cloud.google.com/python/getting-started/run-on-compute-engine) | [7-gce](https://github.com/GoogleCloudPlatform/getting-started-python/tree/master/7-gce) +[Getting Started](https://cloud.google.com/python/getting-started/) | [bookshelf](https://github.com/GoogleCloudPlatform/getting-started-python/tree/master/bookshelf) +[Background Processing](https://cloud.google.com/python/getting-started/background-processing) | [background](https://github.com/GoogleCloudPlatform/getting-started-python/tree/master/background) +[Deploying to Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/docs/quickstarts/deploying-a-language-specific-app) | [bookshelf](https://github.com/GoogleCloudPlatform/getting-started-python/tree/master/bookshelf) +[Deploying to Google Compute Engine](https://cloud.google.com/python/tutorials/getting-started-on-compute-engine) | [gce](https://github.com/GoogleCloudPlatform/getting-started-python/tree/master/gce) +[Handling Sessions with Firestore](https://cloud.google.com/python/getting-started/session-handling-with-firestore) | [sessions](https://github.com/GoogleCloudPlatform/getting-started-python/tree/master/sessions) +[Authenticating Users with IAP](https://cloud.google.com/python/getting-started/authenticate-users) | [authenticating-users](https://github.com/GoogleCloudPlatform/getting-started-python/tree/master/authenticating-users) ## Contributing changes * See [CONTRIBUTING.md](CONTRIBUTING.md) - ## Licensing * See [LICENSE](LICENSE) From 0da31ab75575b987d5b8fd411247ff86cbdccf46 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Mon, 3 Feb 2020 14:05:59 -0800 Subject: [PATCH 118/395] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d86cd082..9c081cea 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Tutorial | Folder ---------|------- [Getting Started](https://cloud.google.com/python/getting-started/) | [bookshelf](https://github.com/GoogleCloudPlatform/getting-started-python/tree/master/bookshelf) [Background Processing](https://cloud.google.com/python/getting-started/background-processing) | [background](https://github.com/GoogleCloudPlatform/getting-started-python/tree/master/background) -[Deploying to Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/docs/quickstarts/deploying-a-language-specific-app) | [bookshelf](https://github.com/GoogleCloudPlatform/getting-started-python/tree/master/bookshelf) +[Deploying to Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/docs/quickstarts/deploying-a-language-specific-app) | [in "kubernetes-engine-samples" repo](https://github.com/GoogleCloudPlatform/kubernetes-engine-samples/tree/master/quickstart/ruby) [Deploying to Google Compute Engine](https://cloud.google.com/python/tutorials/getting-started-on-compute-engine) | [gce](https://github.com/GoogleCloudPlatform/getting-started-python/tree/master/gce) [Handling Sessions with Firestore](https://cloud.google.com/python/getting-started/session-handling-with-firestore) | [sessions](https://github.com/GoogleCloudPlatform/getting-started-python/tree/master/sessions) [Authenticating Users with IAP](https://cloud.google.com/python/getting-started/authenticate-users) | [authenticating-users](https://github.com/GoogleCloudPlatform/getting-started-python/tree/master/authenticating-users) From 3ee0d7aec317ce82057852397f2ecc592489b765 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Mon, 3 Feb 2020 14:07:20 -0800 Subject: [PATCH 119/395] Changes link from Ruby to Python --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9c081cea..0e5b3292 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Tutorial | Folder ---------|------- [Getting Started](https://cloud.google.com/python/getting-started/) | [bookshelf](https://github.com/GoogleCloudPlatform/getting-started-python/tree/master/bookshelf) [Background Processing](https://cloud.google.com/python/getting-started/background-processing) | [background](https://github.com/GoogleCloudPlatform/getting-started-python/tree/master/background) -[Deploying to Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/docs/quickstarts/deploying-a-language-specific-app) | [in "kubernetes-engine-samples" repo](https://github.com/GoogleCloudPlatform/kubernetes-engine-samples/tree/master/quickstart/ruby) +[Deploying to Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/docs/quickstarts/deploying-a-language-specific-app) | [in "kubernetes-engine-samples" repo](https://github.com/GoogleCloudPlatform/kubernetes-engine-samples/tree/master/quickstart/python) [Deploying to Google Compute Engine](https://cloud.google.com/python/tutorials/getting-started-on-compute-engine) | [gce](https://github.com/GoogleCloudPlatform/getting-started-python/tree/master/gce) [Handling Sessions with Firestore](https://cloud.google.com/python/getting-started/session-handling-with-firestore) | [sessions](https://github.com/GoogleCloudPlatform/getting-started-python/tree/master/sessions) [Authenticating Users with IAP](https://cloud.google.com/python/getting-started/authenticate-users) | [authenticating-users](https://github.com/GoogleCloudPlatform/getting-started-python/tree/master/authenticating-users) From 5ec84bbe7a437925bc7bb093104a892cf5bf8171 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Tue, 4 Feb 2020 23:38:54 +0000 Subject: [PATCH 120/395] Update dependency google-cloud-translate to v2.0.1 --- background/function/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/function/requirements.txt b/background/function/requirements.txt index 2aab9b14..5fd22937 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-translate==2.0.0 +google-cloud-translate==2.0.1 google-cloud-firestore==1.6.0 From 32115f6fec4940936c830d695a21206c422158d9 Mon Sep 17 00:00:00 2001 From: Mike Truty Date: Tue, 11 Feb 2020 11:02:08 -0600 Subject: [PATCH 121/395] Fix error from werkzeug import. (#257) --- bookshelf/storage.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bookshelf/storage.py b/bookshelf/storage.py index 53a1c567..f6f9b1d3 100644 --- a/bookshelf/storage.py +++ b/bookshelf/storage.py @@ -20,8 +20,8 @@ from flask import current_app from google.cloud import storage import six -from werkzeug import secure_filename from werkzeug.exceptions import BadRequest +from werkzeug.utils import secure_filename def _check_extension(filename, allowed_extensions): @@ -33,8 +33,8 @@ def _check_extension(filename, allowed_extensions): def _safe_filename(filename): """ - Generates a safe filename that is unlikely to collide with existing objects - in Google Cloud Storage. + Generates a safe filename that is unlikely to collide with existing + objects in Google Cloud Storage. ``filename.ext`` is transformed into ``filename-YYYY-MM-DD-HHMMSS.ext`` """ From 3a3df4066354d4fb837e2c0e1707cb18a3b3ca08 Mon Sep 17 00:00:00 2001 From: Tyler Bui-Palsulich Date: Thu, 27 Feb 2020 14:31:05 -0500 Subject: [PATCH 122/395] .kokoro: send logs to the Build Cop Bot --- .kokoro/system_tests.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.kokoro/system_tests.sh b/.kokoro/system_tests.sh index be9f8eab..7e65184d 100755 --- a/.kokoro/system_tests.sh +++ b/.kokoro/system_tests.sh @@ -30,3 +30,8 @@ export GOOGLE_APPLICATION_CREDENTIALS="$(pwd)/service-account.json" # Run tests nox -s lint nox -s run_tests + +# Send the test log to the Build Cop Bot. +# See https://github.com/googleapis/repo-automation-bots/tree/master/packages/buildcop. +chmod +x $KOKORO_GFILE_DIR/linux_amd64/buildcop +$KOKORO_GFILE_DIR/linux_amd64/buildcop From 2cd84489bd220f99cf02b28269b96b5901a1ce03 Mon Sep 17 00:00:00 2001 From: Tyler Bui-Palsulich Date: Thu, 27 Feb 2020 15:28:29 -0500 Subject: [PATCH 123/395] .kokoro: only publish nightly logs --- .kokoro/system_tests.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.kokoro/system_tests.sh b/.kokoro/system_tests.sh index 7e65184d..f60e50fe 100755 --- a/.kokoro/system_tests.sh +++ b/.kokoro/system_tests.sh @@ -31,7 +31,9 @@ export GOOGLE_APPLICATION_CREDENTIALS="$(pwd)/service-account.json" nox -s lint nox -s run_tests -# Send the test log to the Build Cop Bot. +# If this is a nightly build, send the test log to the Build Cop Bot. # See https://github.com/googleapis/repo-automation-bots/tree/master/packages/buildcop. -chmod +x $KOKORO_GFILE_DIR/linux_amd64/buildcop -$KOKORO_GFILE_DIR/linux_amd64/buildcop +if [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"system_tests"* ]]; then + chmod +x $KOKORO_GFILE_DIR/linux_amd64/buildcop + $KOKORO_GFILE_DIR/linux_amd64/buildcop +fi \ No newline at end of file From ce7f6a24ffca129e6898bc8828b99b53e0b72b06 Mon Sep 17 00:00:00 2001 From: Christopher Wilcox Date: Wed, 25 Mar 2020 13:04:11 -0700 Subject: [PATCH 124/395] Update requirements to allow newer six versions. --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 73b26e14..b3e68adc 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -4,4 +4,4 @@ google-cloud-storage==1.23.0 google-cloud-logging==1.14.0 google-cloud-error-reporting==0.33.0 gunicorn==19.9.0 -six==1.11.0 +six>=1.11.0 From 22b7e850da354e74fe443bbe7d432bbb122a8ea6 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Fri, 3 Apr 2020 19:31:06 +0000 Subject: [PATCH 125/395] Update dependency Flask to v1.1.2 --- authenticating-users/requirements.txt | 2 +- background/app/requirements.txt | 2 +- gce/requirements.txt | 2 +- sessions/requirements.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index b754d36e..7c32cf69 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -1,5 +1,5 @@ # [START getting_started_requirements] -Flask==1.1.1 +Flask==1.1.2 cryptography==2.8 python-jose[cryptography]==3.0.1 requests==2.22.0 diff --git a/background/app/requirements.txt b/background/app/requirements.txt index a7cd28d6..117c0d77 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ google-cloud-firestore==1.6.0 google-cloud-pubsub==1.0.2 -flask==1.1.1 +flask==1.1.2 diff --git a/gce/requirements.txt b/gce/requirements.txt index f561b3c0..dc4e4742 100644 --- a/gce/requirements.txt +++ b/gce/requirements.txt @@ -1,3 +1,3 @@ -flask==1.1.1 +flask==1.1.2 honcho==1.0.1 gunicorn==19.9.0 diff --git a/sessions/requirements.txt b/sessions/requirements.txt index 88580978..d7455046 100644 --- a/sessions/requirements.txt +++ b/sessions/requirements.txt @@ -1,2 +1,2 @@ google-cloud-firestore==1.6.0 -flask==1.1.1 +flask==1.1.2 From 6aae0debdc912c02f15e51b3ba18d6150013b907 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 11 Jun 2020 01:54:28 +0000 Subject: [PATCH 126/395] Update dependency cryptography to v2.9.2 --- authenticating-users/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index 7c32cf69..6f3adef4 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -1,6 +1,6 @@ # [START getting_started_requirements] Flask==1.1.2 -cryptography==2.8 +cryptography==2.9.2 python-jose[cryptography]==3.0.1 requests==2.22.0 # [END getting_started_requirements] From 4a48bd37aa3d0aaa74285a9d1a965a277cce3196 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 11 Jun 2020 01:54:34 +0000 Subject: [PATCH 127/395] Update dependency flake8 to v3.8.3 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e8216097..e523f9cd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -flake8==3.7.9 +flake8==3.8.3 nox==2019.11.9 pytest==3.10.1 requests==2.22.0 From c86296d0756b49a8a8f50bfcbfee129461ee1b3b Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 11 Jun 2020 01:54:40 +0000 Subject: [PATCH 128/395] Update dependency google-cloud-error-reporting to v0.34.0 --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index b3e68adc..6b828a71 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -2,6 +2,6 @@ Flask>=1.0.0 google-cloud-firestore==1.6.0 google-cloud-storage==1.23.0 google-cloud-logging==1.14.0 -google-cloud-error-reporting==0.33.0 +google-cloud-error-reporting==0.34.0 gunicorn==19.9.0 six>=1.11.0 From a646f44bc28b0b4faf724e2c8059c30c47fc795a Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 11 Jun 2020 01:54:52 +0000 Subject: [PATCH 129/395] Update dependency google-cloud-pubsub to v1.6.0 --- background/app/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 117c0d77..855a2103 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ google-cloud-firestore==1.6.0 -google-cloud-pubsub==1.0.2 +google-cloud-pubsub==1.6.0 flask==1.1.2 From c1114bd889b1539c9edca67e4599add5e7a77321 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 11 Jun 2020 01:55:07 +0000 Subject: [PATCH 130/395] Update dependency pytest to v5 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e8216097..8ccf58c0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ flake8==3.7.9 nox==2019.11.9 -pytest==3.10.1 +pytest==5.4.3 requests==2.22.0 From d78b594d4631052ea94976035cfe88e0ed0a03c8 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 11 Jun 2020 01:57:17 +0000 Subject: [PATCH 131/395] Update dependency python-jose to v3.1.0 --- authenticating-users/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index 6f3adef4..1acb241f 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -1,6 +1,6 @@ # [START getting_started_requirements] Flask==1.1.2 cryptography==2.9.2 -python-jose[cryptography]==3.0.1 +python-jose[cryptography]==3.1.0 requests==2.22.0 # [END getting_started_requirements] From ce3da08b134cf62741a3b9386a76a24a972b1263 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 11 Jun 2020 17:32:28 +0000 Subject: [PATCH 132/395] Update dependency google-cloud-storage to v1.29.0 --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 6b828a71..bb90c450 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,6 +1,6 @@ Flask>=1.0.0 google-cloud-firestore==1.6.0 -google-cloud-storage==1.23.0 +google-cloud-storage==1.29.0 google-cloud-logging==1.14.0 google-cloud-error-reporting==0.34.0 gunicorn==19.9.0 From 6725c25371c97b2c7f60e8cceb835f88e64d05f5 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 11 Jun 2020 18:01:44 +0000 Subject: [PATCH 133/395] Pin dependencies --- bookshelf/requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index bb90c450..90adc005 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,7 +1,7 @@ -Flask>=1.0.0 +Flask==1.1.2 google-cloud-firestore==1.6.0 google-cloud-storage==1.29.0 google-cloud-logging==1.14.0 google-cloud-error-reporting==0.34.0 gunicorn==19.9.0 -six>=1.11.0 +six==1.15.0 From 28b26d83118212d174f2da5075d86080920d80fe Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 11 Jun 2020 18:03:57 +0000 Subject: [PATCH 134/395] Update dependency google-cloud-logging to v1.15.0 --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 90adc005..a0abd32a 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,7 +1,7 @@ Flask==1.1.2 google-cloud-firestore==1.6.0 google-cloud-storage==1.29.0 -google-cloud-logging==1.14.0 +google-cloud-logging==1.15.0 google-cloud-error-reporting==0.34.0 gunicorn==19.9.0 six==1.15.0 From 88389f6b9a08b89f0493d09122a69b38fa4507c9 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 11 Jun 2020 18:04:08 +0000 Subject: [PATCH 135/395] Update dependency nox to v2020 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a11d09dc..c5b8e7ac 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ flake8==3.8.3 -nox==2019.11.9 +nox==2020.5.24 pytest==5.4.3 requests==2.22.0 From b50bd55c332970d6da50f64abda2b7c0acb5664d Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Wed, 17 Jun 2020 19:24:22 +0000 Subject: [PATCH 136/395] Update dependency requests to v2.24.0 --- authenticating-users/requirements.txt | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index 1acb241f..93299dc1 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -2,5 +2,5 @@ Flask==1.1.2 cryptography==2.9.2 python-jose[cryptography]==3.1.0 -requests==2.22.0 +requests==2.24.0 # [END getting_started_requirements] diff --git a/requirements.txt b/requirements.txt index a11d09dc..dacd2bb6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ flake8==3.8.3 nox==2019.11.9 pytest==5.4.3 -requests==2.22.0 +requests==2.24.0 From f1170dc637c0f70e3f41055061d88c6b169e05ca Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 6 Jul 2020 16:36:41 +0000 Subject: [PATCH 137/395] Update dependency google-cloud-pubsub to v1.6.1 --- background/app/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 855a2103..912a60f8 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ google-cloud-firestore==1.6.0 -google-cloud-pubsub==1.6.0 +google-cloud-pubsub==1.6.1 flask==1.1.2 From c32a20678a3f92a0914e408b08120db52cdb596a Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Tue, 7 Jul 2020 01:37:01 +0000 Subject: [PATCH 138/395] Update dependency google-cloud-firestore to v1.8.1 --- background/app/requirements.txt | 2 +- background/function/requirements.txt | 2 +- bookshelf/requirements.txt | 2 +- sessions/requirements.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 855a2103..c0197323 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ -google-cloud-firestore==1.6.0 +google-cloud-firestore==1.8.1 google-cloud-pubsub==1.6.0 flask==1.1.2 diff --git a/background/function/requirements.txt b/background/function/requirements.txt index 2aab9b14..6a480dde 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ google-cloud-translate==2.0.0 -google-cloud-firestore==1.6.0 +google-cloud-firestore==1.8.1 diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 90adc005..93a8ef00 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,5 +1,5 @@ Flask==1.1.2 -google-cloud-firestore==1.6.0 +google-cloud-firestore==1.8.1 google-cloud-storage==1.29.0 google-cloud-logging==1.14.0 google-cloud-error-reporting==0.34.0 diff --git a/sessions/requirements.txt b/sessions/requirements.txt index d7455046..985af74e 100644 --- a/sessions/requirements.txt +++ b/sessions/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-firestore==1.6.0 +google-cloud-firestore==1.8.1 flask==1.1.2 From fe0fe6a76a5301efbbffb93d9f4482351adb3140 Mon Sep 17 00:00:00 2001 From: Takashi Matsuo Date: Thu, 9 Jul 2020 22:53:49 +0000 Subject: [PATCH 139/395] testing: migrate to Trampoline V2 --- .kokoro/common.cfg | 8 +- .kokoro/system_tests.sh | 12 +- .kokoro/trampoline_v2.sh | 476 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 493 insertions(+), 3 deletions(-) create mode 100755 .kokoro/trampoline_v2.sh diff --git a/.kokoro/common.cfg b/.kokoro/common.cfg index c55b58f2..116e7f72 100644 --- a/.kokoro/common.cfg +++ b/.kokoro/common.cfg @@ -18,5 +18,11 @@ env_vars: { # Tell the trampoline which build file to use. env_vars: { key: "TRAMPOLINE_BUILD_FILE" - value: "github/getting-started-python/.kokoro/system_tests.sh" + value: ".kokoro/system_tests.sh" +} + +# We still rely on the legacy service account. +env_vars: { + key: "TRAMPOLINE_USE_LEGACY_SERVICE_ACCOUNT" + value: "true" } diff --git a/.kokoro/system_tests.sh b/.kokoro/system_tests.sh index f60e50fe..fae2d232 100755 --- a/.kokoro/system_tests.sh +++ b/.kokoro/system_tests.sh @@ -18,7 +18,8 @@ set -eo pipefail export PATH=${PATH}:${HOME}/gcloud/google-cloud-sdk/bin -cd github/getting-started-python +cd "${PROJECT_ROOT:-github/getting-started-python}" + # Unencrypt and extract secrets SECRETS_PASSWORD=$(cat "${KOKORO_GFILE_DIR}/secrets-password.txt") @@ -27,6 +28,13 @@ SECRETS_PASSWORD=$(cat "${KOKORO_GFILE_DIR}/secrets-password.txt") # Setup environment variables export GOOGLE_APPLICATION_CREDENTIALS="$(pwd)/service-account.json" +# This block is executed only with Trampoline V2. +if [[ -n "${TRAMPOLINE_VERSION:-}" ]]; then + # Install nox as a user and add it to the PATH. + python3 -m pip install --user nox + export PATH="${PATH}:${HOME}/.local/bin" +fi + # Run tests nox -s lint nox -s run_tests @@ -36,4 +44,4 @@ nox -s run_tests if [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"system_tests"* ]]; then chmod +x $KOKORO_GFILE_DIR/linux_amd64/buildcop $KOKORO_GFILE_DIR/linux_amd64/buildcop -fi \ No newline at end of file +fi diff --git a/.kokoro/trampoline_v2.sh b/.kokoro/trampoline_v2.sh new file mode 100755 index 00000000..8a97c4cd --- /dev/null +++ b/.kokoro/trampoline_v2.sh @@ -0,0 +1,476 @@ +#!/usr/bin/env bash +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# trampoline_v2.sh +# +# This script does 3 things. +# +# 1. Prepare the Docker image for the test +# 2. Run the Docker with appropriate flags to run the test +# 3. Upload the newly built Docker image +# +# in a way that is somewhat compatible with trampoline_v1. +# +# To run this script, first download few files from gcs to /dev/shm. +# (/dev/shm is passed into the container as KOKORO_GFILE_DIR). +# +# gsutil cp gs://cloud-devrel-kokoro-resources/python-docs-samples/secrets_viewer_service_account.json /dev/shm +# gsutil cp gs://cloud-devrel-kokoro-resources/python-docs-samples/automl_secrets.txt /dev/shm +# +# Then run the script. +# .kokoro/trampoline_v2.sh +# +# These environment variables are required: +# TRAMPOLINE_IMAGE: The docker image to use. +# TRAMPOLINE_DOCKERFILE: The location of the Dockerfile. +# +# You can optionally change these environment variables: +# TRAMPOLINE_IMAGE_UPLOAD: +# (true|false): Whether to upload the Docker image after the +# successful builds. +# TRAMPOLINE_BUILD_FILE: The script to run in the docker container. +# TRAMPOLINE_WORKSPACE: The workspace path in the docker container. +# Defaults to /workspace. +# TRAMPOLINE_SKIP_DOWNLOAD_IMAGE: Skip downloading the image when you +# know you have the image locally. +# +# Potentially there are some repo specific envvars in .trampolinerc in +# the project root. + + +set -euo pipefail + +TRAMPOLINE_VERSION="2.0.2" + +if command -v tput >/dev/null && [[ -n "${TERM:-}" ]]; then + readonly IO_COLOR_RED="$(tput setaf 1)" + readonly IO_COLOR_GREEN="$(tput setaf 2)" + readonly IO_COLOR_YELLOW="$(tput setaf 3)" + readonly IO_COLOR_RESET="$(tput sgr0)" +else + readonly IO_COLOR_RED="" + readonly IO_COLOR_GREEN="" + readonly IO_COLOR_YELLOW="" + readonly IO_COLOR_RESET="" +fi + +function function_exists { + [ $(LC_ALL=C type -t $1)"" == "function" ] +} + +# Logs a message using the given color. The first argument must be one +# of the IO_COLOR_* variables defined above, such as +# "${IO_COLOR_YELLOW}". The remaining arguments will be logged in the +# given color. The log message will also have an RFC-3339 timestamp +# prepended (in UTC). You can disable the color output by setting +# TERM=vt100. +function log_impl() { + local color="$1" + shift + local timestamp="$(date -u "+%Y-%m-%dT%H:%M:%SZ")" + echo "================================================================" + echo "${color}${timestamp}:" "$@" "${IO_COLOR_RESET}" + echo "================================================================" +} + +# Logs the given message with normal coloring and a timestamp. +function log() { + log_impl "${IO_COLOR_RESET}" "$@" +} + +# Logs the given message in green with a timestamp. +function log_green() { + log_impl "${IO_COLOR_GREEN}" "$@" +} + +# Logs the given message in yellow with a timestamp. +function log_yellow() { + log_impl "${IO_COLOR_YELLOW}" "$@" +} + +# Logs the given message in red with a timestamp. +function log_red() { + log_impl "${IO_COLOR_RED}" "$@" +} + +readonly tmpdir=$(mktemp -d -t ci-XXXXXXXX) +readonly tmphome="${tmpdir}/h" +mkdir -p "${tmphome}" + +function cleanup() { + rm -rf "${tmpdir}" +} +trap cleanup EXIT + +RUNNING_IN_CI="${RUNNING_IN_CI:-false}" + +# The workspace in the container, defaults to /workspace. +TRAMPOLINE_WORKSPACE="${TRAMPOLINE_WORKSPACE:-/workspace}" + +pass_down_envvars=( + # TRAMPOLINE_V2 variables. + # Tells scripts whether they are running as part of CI or not. + "RUNNING_IN_CI" + # Indicates which CI system we're in. + "TRAMPOLINE_CI" + # Indicates the version of the script. + "TRAMPOLINE_VERSION" +) + +log_yellow "Building with Trampoline ${TRAMPOLINE_VERSION}" + +# Detect which CI systems we're in. If we're in any of the CI systems +# we support, `RUNNING_IN_CI` will be true and `TRAMPOLINE_CI` will be +# the name of the CI system. Both envvars will be passing down to the +# container for telling which CI system we're in. +if [[ -n "${KOKORO_BUILD_ID:-}" ]]; then + # descriptive env var for indicating it's on CI. + RUNNING_IN_CI="true" + TRAMPOLINE_CI="kokoro" + if [[ "${TRAMPOLINE_USE_LEGACY_SERVICE_ACCOUNT:-}" == "true" ]]; then + if [[ ! -f "${KOKORO_GFILE_DIR}/kokoro-trampoline.service-account.json" ]]; then + log_red "${KOKORO_GFILE_DIR}/kokoro-trampoline.service-account.json does not exist. Did you forget to mount cloud-devrel-kokoro-resources/trampoline? Aborting." + exit 1 + fi + # This service account will be activated later. + TRAMPOLINE_SERVICE_ACCOUNT="${KOKORO_GFILE_DIR}/kokoro-trampoline.service-account.json" + else + if [[ "${TRAMPOLINE_VERBOSE:-}" == "true" ]]; then + gcloud auth list + fi + log_yellow "Configuring Container Registry access" + gcloud auth configure-docker --quiet + fi + pass_down_envvars+=( + # KOKORO dynamic variables. + "KOKORO_BUILD_NUMBER" + "KOKORO_BUILD_ID" + "KOKORO_JOB_NAME" + "KOKORO_GIT_COMMIT" + "KOKORO_GITHUB_COMMIT" + "KOKORO_GITHUB_PULL_REQUEST_NUMBER" + "KOKORO_GITHUB_PULL_REQUEST_COMMIT" + # For Build Cop Bot + "KOKORO_GITHUB_COMMIT_URL" + "KOKORO_GITHUB_PULL_REQUEST_URL" + ) +elif [[ "${TRAVIS:-}" == "true" ]]; then + RUNNING_IN_CI="true" + TRAMPOLINE_CI="travis" + pass_down_envvars+=( + "TRAVIS_BRANCH" + "TRAVIS_BUILD_ID" + "TRAVIS_BUILD_NUMBER" + "TRAVIS_BUILD_WEB_URL" + "TRAVIS_COMMIT" + "TRAVIS_COMMIT_MESSAGE" + "TRAVIS_COMMIT_RANGE" + "TRAVIS_JOB_NAME" + "TRAVIS_JOB_NUMBER" + "TRAVIS_JOB_WEB_URL" + "TRAVIS_PULL_REQUEST" + "TRAVIS_PULL_REQUEST_BRANCH" + "TRAVIS_PULL_REQUEST_SHA" + "TRAVIS_PULL_REQUEST_SLUG" + "TRAVIS_REPO_SLUG" + "TRAVIS_SECURE_ENV_VARS" + "TRAVIS_TAG" + ) +elif [[ -n "${GITHUB_RUN_ID:-}" ]]; then + RUNNING_IN_CI="true" + TRAMPOLINE_CI="github-workflow" + pass_down_envvars+=( + "GITHUB_WORKFLOW" + "GITHUB_RUN_ID" + "GITHUB_RUN_NUMBER" + "GITHUB_ACTION" + "GITHUB_ACTIONS" + "GITHUB_ACTOR" + "GITHUB_REPOSITORY" + "GITHUB_EVENT_NAME" + "GITHUB_EVENT_PATH" + "GITHUB_SHA" + "GITHUB_REF" + "GITHUB_HEAD_REF" + "GITHUB_BASE_REF" + ) +elif [[ "${CIRCLECI:-}" == "true" ]]; then + RUNNING_IN_CI="true" + TRAMPOLINE_CI="circleci" + pass_down_envvars+=( + "CIRCLE_BRANCH" + "CIRCLE_BUILD_NUM" + "CIRCLE_BUILD_URL" + "CIRCLE_COMPARE_URL" + "CIRCLE_JOB" + "CIRCLE_NODE_INDEX" + "CIRCLE_NODE_TOTAL" + "CIRCLE_PREVIOUS_BUILD_NUM" + "CIRCLE_PROJECT_REPONAME" + "CIRCLE_PROJECT_USERNAME" + "CIRCLE_REPOSITORY_URL" + "CIRCLE_SHA1" + "CIRCLE_STAGE" + "CIRCLE_USERNAME" + "CIRCLE_WORKFLOW_ID" + "CIRCLE_WORKFLOW_JOB_ID" + "CIRCLE_WORKFLOW_UPSTREAM_JOB_IDS" + "CIRCLE_WORKFLOW_WORKSPACE_ID" + ) +fi + +# Configure the service account for pulling the docker image. +if [[ -n "${TRAMPOLINE_SERVICE_ACCOUNT:-}" ]]; then + + mkdir -p "${tmpdir}/gcloud" + gcloud_config_dir="${tmpdir}/gcloud" + + log_yellow "Using isolated gcloud config: ${gcloud_config_dir}." + export CLOUDSDK_CONFIG="${gcloud_config_dir}" + + log_yellow "Using ${TRAMPOLINE_SERVICE_ACCOUNT} for authentication." + gcloud auth activate-service-account \ + --key-file "${TRAMPOLINE_SERVICE_ACCOUNT}" + log_yellow "Configuring Container Registry access" + gcloud auth configure-docker --quiet +fi + +function repo_root() { + local dir="$1" + while [[ ! -d "${dir}/.git" ]]; do + dir="$(dirname "$dir")" + done + echo "${dir}" +} + +# Detect the project root. In CI builds, we assume the script is in +# the git tree and traverse from there, otherwise, traverse from `pwd` +# to find `.git` directory. +if [[ "${RUNNING_IN_CI:-}" == "true" ]]; then + PROGRAM_PATH="$(realpath "$0")" + PROGRAM_DIR="$(dirname "${PROGRAM_PATH}")" + PROJECT_ROOT="$(repo_root "${PROGRAM_DIR}")" +else + PROJECT_ROOT="$(repo_root $(pwd))" +fi + +log_yellow "Changing to the project root: ${PROJECT_ROOT}." +cd "${PROJECT_ROOT}" + +required_envvars=( + # The basic trampoline configurations. + "TRAMPOLINE_IMAGE" + "TRAMPOLINE_BUILD_FILE" +) + +if [[ -f "${PROJECT_ROOT}/.trampolinerc" ]]; then + source "${PROJECT_ROOT}/.trampolinerc" +fi + +log_yellow "Checking environment variables." +for e in "${required_envvars[@]}" +do + if [[ -z "${!e:-}" ]]; then + log "Missing ${e} env var. Aborting." + exit 1 + fi +done + +if [[ "${TRAMPOLINE_SKIP_DOWNLOAD_IMAGE:-false}" == "true" ]]; then + log_yellow "Re-using the local Docker image." + has_cache="true" +else + log_yellow "Preparing Docker image." + # Download the docker image specified by `TRAMPOLINE_IMAGE` + + set +e # ignore error on docker operations + # We may want to add --max-concurrent-downloads flag. + + log_yellow "Start pulling the Docker image: ${TRAMPOLINE_IMAGE}." + if docker pull "${TRAMPOLINE_IMAGE}"; then + log_green "Finished pulling the Docker image: ${TRAMPOLINE_IMAGE}." + has_cache="true" + else + log_red "Failed pulling the Docker image: ${TRAMPOLINE_IMAGE}." + has_cache="false" + fi +fi + + +# The default user for a Docker container has uid 0 (root). To avoid +# creating root-owned files in the build directory we tell docker to +# use the current user ID. +user_uid="$(id -u)" +user_gid="$(id -g)" +user_name="$(id -un)" + +# To allow docker in docker, we add the user to the docker group in +# the host os. +docker_gid=$(cut -d: -f3 < <(getent group docker)) + +update_cache="false" +if [[ "${TRAMPOLINE_DOCKERFILE:-none}" != "none" ]]; then + # Build the Docker image from the source. + context_dir=$(dirname "${TRAMPOLINE_DOCKERFILE}") + docker_build_flags=( + "-f" "${TRAMPOLINE_DOCKERFILE}" + "-t" "${TRAMPOLINE_IMAGE}" + "--build-arg" "UID=${user_uid}" + "--build-arg" "USERNAME=${user_name}" + ) + if [[ "${has_cache}" == "true" ]]; then + docker_build_flags+=("--cache-from" "${TRAMPOLINE_IMAGE}") + fi + + log_yellow "Start building the docker image." + if [[ "${TRAMPOLINE_SHOW_COMMAND:-false}" == "true" ]]; then + echo "docker build" "${docker_build_flags[@]}" "${context_dir}" + fi + + # ON CI systems, we want to suppress docker build logs, only + # output the logs when it fails. + if [[ "${RUNNING_IN_CI:-}" == "true" ]]; then + if docker build "${docker_build_flags[@]}" "${context_dir}" \ + > "${tmpdir}/docker_build.log" 2>&1; then + if [[ "${TRAMPOLINE_VERBOSE:-}" == "true" ]]; then + cat "${tmpdir}/docker_build.log" + fi + + log_green "Finished building the docker image." + update_cache="true" + else + log_red "Failed to build the Docker image, aborting." + log_yellow "Dumping the build logs:" + cat "${tmpdir}/docker_build.log" + exit 1 + fi + else + if docker build "${docker_build_flags[@]}" "${context_dir}"; then + log_green "Finished building the docker image." + update_cache="true" + else + log_red "Failed to build the Docker image, aborting." + exit 1 + fi + fi +else + if [[ "${has_cache}" != "true" ]]; then + log_red "failed to download the image ${TRAMPOLINE_IMAGE}, aborting." + exit 1 + fi +fi + +# We use an array for the flags so they are easier to document. +docker_flags=( + # Remove the container after it exists. + "--rm" + + # Use the host network. + "--network=host" + + # Run in priviledged mode. We are not using docker for sandboxing or + # isolation, just for packaging our dev tools. + "--privileged" + + # Run the docker script with the user id. Because the docker image gets to + # write in ${PWD} you typically want this to be your user id. + # To allow docker in docker, we need to use docker gid on the host. + "--user" "${user_uid}:${docker_gid}" + + # Pass down the USER. + "--env" "USER=${user_name}" + + # Mount the project directory inside the Docker container. + "--volume" "${PROJECT_ROOT}:${TRAMPOLINE_WORKSPACE}" + "--workdir" "${TRAMPOLINE_WORKSPACE}" + "--env" "PROJECT_ROOT=${TRAMPOLINE_WORKSPACE}" + + # Mount the temporary home directory. + "--volume" "${tmphome}:/h" + "--env" "HOME=/h" + + # Allow docker in docker. + "--volume" "/var/run/docker.sock:/var/run/docker.sock" + + # Mount the /tmp so that docker in docker can mount the files + # there correctly. + "--volume" "/tmp:/tmp" + # Pass down the KOKORO_GFILE_DIR and KOKORO_KEYSTORE_DIR + # TODO(tmatsuo): This part is not portable. + "--env" "TRAMPOLINE_SECRET_DIR=/secrets" + "--volume" "${KOKORO_GFILE_DIR:-/dev/shm}:/secrets/gfile" + "--env" "KOKORO_GFILE_DIR=/secrets/gfile" + "--volume" "${KOKORO_KEYSTORE_DIR:-/dev/shm}:/secrets/keystore" + "--env" "KOKORO_KEYSTORE_DIR=/secrets/keystore" +) + +# Add an option for nicer output if the build gets a tty. +if [[ -t 0 ]]; then + docker_flags+=("-it") +fi + +# Passing down env vars +for e in "${pass_down_envvars[@]}" +do + if [[ -n "${!e:-}" ]]; then + docker_flags+=("--env" "${e}=${!e}") + fi +done + +# If arguments are given, all arguments will become the commands run +# in the container, otherwise run TRAMPOLINE_BUILD_FILE. +if [[ $# -ge 1 ]]; then + log_yellow "Running the given commands '" "${@:1}" "' in the container." + readonly commands=("${@:1}") + if [[ "${TRAMPOLINE_VERBOSE:-}" == "true" ]]; then + echo docker run "${docker_flags[@]}" "${TRAMPOLINE_IMAGE}" "${commands[@]}" + fi + docker run "${docker_flags[@]}" "${TRAMPOLINE_IMAGE}" "${commands[@]}" +else + log_yellow "Running the tests in a Docker container." + docker_flags+=("--entrypoint=${TRAMPOLINE_BUILD_FILE}") + if [[ "${TRAMPOLINE_VERBOSE:-}" == "true" ]]; then + echo docker run "${docker_flags[@]}" "${TRAMPOLINE_IMAGE}" + fi + docker run "${docker_flags[@]}" "${TRAMPOLINE_IMAGE}" +fi + + +test_retval=$? + +if [[ ${test_retval} -eq 0 ]]; then + log_green "Build finished with ${test_retval}" +else + log_red "Build finished with ${test_retval}" +fi + +# Only upload it when the test passes. +if [[ "${update_cache}" == "true" ]] && \ + [[ $test_retval == 0 ]] && \ + [[ "${TRAMPOLINE_IMAGE_UPLOAD:-false}" == "true" ]]; then + log_yellow "Uploading the Docker image." + if docker push "${TRAMPOLINE_IMAGE}"; then + log_green "Finished uploading the Docker image." + else + log_red "Failed uploading the Docker image." + fi + # Call trampoline_after_upload_hook if it's defined. + if function_exists trampoline_after_upload_hook; then + trampoline_after_upload_hook + fi + +fi + +exit "${test_retval}" From d69be159b21d610e68c939c3dad549091d799fc3 Mon Sep 17 00:00:00 2001 From: Takashi Matsuo Date: Thu, 9 Jul 2020 23:05:26 +0000 Subject: [PATCH 140/395] update the build_file --- .kokoro/common.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.kokoro/common.cfg b/.kokoro/common.cfg index 116e7f72..3957c279 100644 --- a/.kokoro/common.cfg +++ b/.kokoro/common.cfg @@ -7,7 +7,7 @@ gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/getting-started-python" # All builds use the trampoline script to run in docker. -build_file: "getting-started-python/.kokoro/trampoline.sh" +build_file: "getting-started-python/.kokoro/trampoline_v2.sh" # Use the Python worker docker iamge. env_vars: { From b31cccbfe77bc9f6f36a2a15a2ca5327fa564a77 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 13 Jul 2020 17:57:27 +0000 Subject: [PATCH 141/395] Update dependency google-cloud-pubsub to v1.7.0 --- background/app/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 60badd6c..fdd0ecda 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ google-cloud-firestore==1.8.1 -google-cloud-pubsub==1.6.1 +google-cloud-pubsub==1.7.0 flask==1.1.2 From 5292a2d799fa806c8ab26c73175414017baf40d6 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 30 Jul 2020 12:58:16 +0000 Subject: [PATCH 142/395] Update dependency pytest to v6 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4a058639..a7575b1b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ flake8==3.8.3 nox==2020.5.24 -pytest==5.4.3 +pytest==6.0.1 requests==2.24.0 From 07d5fb6b82c5d8ac38c16e38374aacd9edd11b02 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 3 Aug 2020 20:15:23 +0000 Subject: [PATCH 143/395] Update dependency google-cloud-translate to v2.0.2 --- background/function/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/function/requirements.txt b/background/function/requirements.txt index 598eb0f0..6c7d8a5b 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-translate==2.0.1 +google-cloud-translate==2.0.2 google-cloud-firestore==1.8.1 From 2478a47731a502f25fe9052d2018a6ddbadb8863 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Wed, 26 Aug 2020 21:41:37 +0000 Subject: [PATCH 144/395] Update dependency google-cloud-storage to v1.31.0 --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index b500f5b6..5e91fb3a 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,6 +1,6 @@ Flask==1.1.2 google-cloud-firestore==1.8.1 -google-cloud-storage==1.29.0 +google-cloud-storage==1.31.0 google-cloud-logging==1.15.0 google-cloud-error-reporting==0.34.0 gunicorn==19.9.0 From f6b5e300b4914c87273bad8d0775300e499d745f Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 27 Aug 2020 05:50:14 +0000 Subject: [PATCH 145/395] Update dependency cryptography to v3 --- authenticating-users/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index 93299dc1..25b3a9a0 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -1,6 +1,6 @@ # [START getting_started_requirements] Flask==1.1.2 -cryptography==2.9.2 +cryptography==3.1 python-jose[cryptography]==3.1.0 requests==2.24.0 # [END getting_started_requirements] From db6436f90fb41db7f7d38776eb09e6aecdf745b2 Mon Sep 17 00:00:00 2001 From: Takashi Matsuo Date: Thu, 27 Aug 2020 11:22:00 -0700 Subject: [PATCH 146/395] chore: enable snippet-bot --- .github/snippet-bot.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/snippet-bot.yml diff --git a/.github/snippet-bot.yml b/.github/snippet-bot.yml new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/.github/snippet-bot.yml @@ -0,0 +1 @@ + From 4f5fc88b8ad1ab6cfcf1bab2258dd4383ce85dcb Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 27 Aug 2020 19:16:15 +0000 Subject: [PATCH 147/395] chore(deps): update dependency gunicorn to v19.10.0 --- bookshelf/requirements.txt | 2 +- gce/requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index b500f5b6..ca1b45fc 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -3,5 +3,5 @@ google-cloud-firestore==1.8.1 google-cloud-storage==1.29.0 google-cloud-logging==1.15.0 google-cloud-error-reporting==0.34.0 -gunicorn==19.9.0 +gunicorn==19.10.0 six==1.15.0 diff --git a/gce/requirements.txt b/gce/requirements.txt index dc4e4742..c6015489 100644 --- a/gce/requirements.txt +++ b/gce/requirements.txt @@ -1,3 +1,3 @@ flask==1.1.2 honcho==1.0.1 -gunicorn==19.9.0 +gunicorn==19.10.0 From 3a8c92ccc91c48672b05f684641c450b6204ee16 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 3 Sep 2020 17:55:20 +0000 Subject: [PATCH 148/395] Update dependency python-jose to v3.2.0 --- authenticating-users/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index 25b3a9a0..a18ebe86 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -1,6 +1,6 @@ # [START getting_started_requirements] Flask==1.1.2 cryptography==3.1 -python-jose[cryptography]==3.1.0 +python-jose[cryptography]==3.2.0 requests==2.24.0 # [END getting_started_requirements] From e0b520e03fbd43f3966ff20630128a345e7637e7 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 3 Sep 2020 18:01:10 +0000 Subject: [PATCH 149/395] Update dependency google-cloud-logging to v1.15.1 --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 5e91fb3a..0ba8f558 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,7 +1,7 @@ Flask==1.1.2 google-cloud-firestore==1.8.1 google-cloud-storage==1.31.0 -google-cloud-logging==1.15.0 +google-cloud-logging==1.15.1 google-cloud-error-reporting==0.34.0 gunicorn==19.9.0 six==1.15.0 From b198284668e03ebf891046d77e404e2def5eb0ed Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 3 Sep 2020 18:01:20 +0000 Subject: [PATCH 150/395] Update dependency nox to v2020.8.22 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a7575b1b..4bc8d779 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ flake8==3.8.3 -nox==2020.5.24 +nox==2020.8.22 pytest==6.0.1 requests==2.24.0 From 3b20dc044dd7b98760ee6a3765b5b6d309427abd Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 3 Sep 2020 18:02:57 +0000 Subject: [PATCH 151/395] chore(deps): update dependency gunicorn to v20 --- bookshelf/requirements.txt | 2 +- gce/requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 51e7c310..1f7bf03e 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -3,5 +3,5 @@ google-cloud-firestore==1.8.1 google-cloud-storage==1.31.0 google-cloud-logging==1.15.0 google-cloud-error-reporting==0.34.0 -gunicorn==19.10.0 +gunicorn==20.0.4 six==1.15.0 diff --git a/gce/requirements.txt b/gce/requirements.txt index c6015489..1eb18140 100644 --- a/gce/requirements.txt +++ b/gce/requirements.txt @@ -1,3 +1,3 @@ flask==1.1.2 honcho==1.0.1 -gunicorn==19.10.0 +gunicorn==20.0.4 From f0d48563997804e724084e4c1a502e3e3dd3c2dc Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 3 Sep 2020 18:06:16 +0000 Subject: [PATCH 152/395] chore(deps): update dependency google-cloud-translate to v3 --- background/function/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/function/requirements.txt b/background/function/requirements.txt index 6c7d8a5b..5735e2cf 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-translate==2.0.2 +google-cloud-translate==3.0.1 google-cloud-firestore==1.8.1 From 49acb86098a4d104c62d2509a8f39bf5682dd6ef Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 3 Sep 2020 18:11:56 +0000 Subject: [PATCH 153/395] Update dependency google-cloud-firestore to v1.9.0 --- background/app/requirements.txt | 2 +- background/function/requirements.txt | 2 +- bookshelf/requirements.txt | 2 +- sessions/requirements.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index fdd0ecda..d05bdb07 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ -google-cloud-firestore==1.8.1 +google-cloud-firestore==1.9.0 google-cloud-pubsub==1.7.0 flask==1.1.2 diff --git a/background/function/requirements.txt b/background/function/requirements.txt index 5735e2cf..73cdba4e 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ google-cloud-translate==3.0.1 -google-cloud-firestore==1.8.1 +google-cloud-firestore==1.9.0 diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 9352a7de..3e1e849b 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,5 +1,5 @@ Flask==1.1.2 -google-cloud-firestore==1.8.1 +google-cloud-firestore==1.9.0 google-cloud-storage==1.31.0 google-cloud-logging==1.15.1 google-cloud-error-reporting==0.34.0 diff --git a/sessions/requirements.txt b/sessions/requirements.txt index 985af74e..cdd61b7c 100644 --- a/sessions/requirements.txt +++ b/sessions/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-firestore==1.8.1 +google-cloud-firestore==1.9.0 flask==1.1.2 From 3e3ef539ff4f516b28eddd02f779dbc5994beefc Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 3 Sep 2020 18:13:14 +0000 Subject: [PATCH 154/395] chore(deps): update dependency google-cloud-error-reporting to v1 --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 7a46564e..4aa61090 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -2,6 +2,6 @@ Flask==1.1.2 google-cloud-firestore==1.8.1 google-cloud-storage==1.31.0 google-cloud-logging==1.15.1 -google-cloud-error-reporting==0.34.0 +google-cloud-error-reporting==1.0.0 gunicorn==20.0.4 six==1.15.0 From 7960d51a58f3cdcad55ec5a813d691c07d553c9e Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Fri, 11 Sep 2020 23:53:26 +0000 Subject: [PATCH 155/395] chore(deps): update dependency pytest to v6.0.2 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4bc8d779..f116c0f2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ flake8==3.8.3 nox==2020.8.22 -pytest==6.0.1 +pytest==6.0.2 requests==2.24.0 From c04a6fa951cad3c938e112d6b1f137fe22775c27 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Tue, 22 Sep 2020 17:37:40 +0000 Subject: [PATCH 156/395] chore(deps): update dependency cryptography to v3.1.1 --- authenticating-users/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index a18ebe86..316dfbdc 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -1,6 +1,6 @@ # [START getting_started_requirements] Flask==1.1.2 -cryptography==3.1 +cryptography==3.1.1 python-jose[cryptography]==3.2.0 requests==2.24.0 # [END getting_started_requirements] From 673641a87358035eecc65e88cf7436d00acc1c32 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Wed, 23 Sep 2020 16:14:03 +0000 Subject: [PATCH 157/395] chore(deps): update dependency google-cloud-storage to v1.31.2 --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 86984eb4..8d515f08 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,6 +1,6 @@ Flask==1.1.2 google-cloud-firestore==1.9.0 -google-cloud-storage==1.31.0 +google-cloud-storage==1.31.2 google-cloud-logging==1.15.1 google-cloud-error-reporting==1.0.0 gunicorn==20.0.4 From e62077436d710646a178351c69354ab7271ed8d6 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sat, 3 Oct 2020 00:21:38 +0000 Subject: [PATCH 158/395] chore(deps): update dependency flake8 to v3.8.4 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f116c0f2..1f94ee74 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -flake8==3.8.3 +flake8==3.8.4 nox==2020.8.22 pytest==6.0.2 requests==2.24.0 From f7a992de31df9fe6ca93c26c7981b93eeb3419fc Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sat, 3 Oct 2020 20:10:12 +0000 Subject: [PATCH 159/395] chore(deps): update dependency pytest to v6.1.1 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f116c0f2..544b6a94 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ flake8==3.8.3 nox==2020.8.22 -pytest==6.0.2 +pytest==6.1.1 requests==2.24.0 From bbb1997f4cdd0fa53a9641d61c19b128f718670c Mon Sep 17 00:00:00 2001 From: akshaykumarpatil-tudip Date: Tue, 13 Oct 2020 15:51:43 +0530 Subject: [PATCH 160/395] Updated apiVersion and added selector in the following files: 1. bookshelf-frontend.yaml 2. bookshelf-worker.yaml --- optional-kubernetes-engine/bookshelf-frontend.yaml | 6 +++++- optional-kubernetes-engine/bookshelf-worker.yaml | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/optional-kubernetes-engine/bookshelf-frontend.yaml b/optional-kubernetes-engine/bookshelf-frontend.yaml index 265c8e35..5db2d638 100644 --- a/optional-kubernetes-engine/bookshelf-frontend.yaml +++ b/optional-kubernetes-engine/bookshelf-frontend.yaml @@ -15,7 +15,7 @@ # This file configures the bookshelf application frontend. The frontend serves # public web traffic. -apiVersion: extensions/v1beta1 +apiVersion: apps/v1 kind: Deployment metadata: name: bookshelf-frontend @@ -27,6 +27,10 @@ metadata: # https://cloud.google.com/kubernetes-engine/docs/pods/ spec: replicas: 3 + selector: + matchLabels: + app: bookshelf + tier: frontend template: metadata: labels: diff --git a/optional-kubernetes-engine/bookshelf-worker.yaml b/optional-kubernetes-engine/bookshelf-worker.yaml index c30df89b..979437b6 100644 --- a/optional-kubernetes-engine/bookshelf-worker.yaml +++ b/optional-kubernetes-engine/bookshelf-worker.yaml @@ -15,7 +15,7 @@ # This file configures the bookshelf task worker. The worker is responsible # for processing book requests and updating book information. -apiVersion: extensions/v1beta1 +apiVersion: apps/v1 kind: Deployment metadata: name: bookshelf-worker @@ -27,6 +27,10 @@ metadata: # https://cloud.google.com/kubernetes-engine/docs/pods/ spec: replicas: 2 + selector: + matchLabels: + app: bookshelf + tier: worker template: metadata: labels: From 7eee64ff3dcdff73fe77b19b039b02071701aa20 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 19 Oct 2020 18:19:33 +0000 Subject: [PATCH 161/395] chore(deps): update dependency google-cloud-storage to v1.32.0 --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 8d515f08..5b68e4fb 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,6 +1,6 @@ Flask==1.1.2 google-cloud-firestore==1.9.0 -google-cloud-storage==1.31.2 +google-cloud-storage==1.32.0 google-cloud-logging==1.15.1 google-cloud-error-reporting==1.0.0 gunicorn==20.0.4 From d4ba0048c1c422f9d85719535d9f40afdbc5dfdd Mon Sep 17 00:00:00 2001 From: Takashi Matsuo Date: Fri, 23 Oct 2020 21:00:40 +0000 Subject: [PATCH 162/395] testing: use secret manager We also moved to python-ubuntu pool so that we don't need the legacy service account any more. --- .kokoro/common.cfg | 6 ------ decrypt-secrets.sh | 21 ++++++++++++++++++--- encrypt-secrets.sh | 16 +++++++++++----- 3 files changed, 29 insertions(+), 14 deletions(-) diff --git a/.kokoro/common.cfg b/.kokoro/common.cfg index 3957c279..0446da72 100644 --- a/.kokoro/common.cfg +++ b/.kokoro/common.cfg @@ -20,9 +20,3 @@ env_vars: { key: "TRAMPOLINE_BUILD_FILE" value: ".kokoro/system_tests.sh" } - -# We still rely on the legacy service account. -env_vars: { - key: "TRAMPOLINE_USE_LEGACY_SERVICE_ACCOUNT" - value: "true" -} diff --git a/decrypt-secrets.sh b/decrypt-secrets.sh index 991919ce..7df5428a 100755 --- a/decrypt-secrets.sh +++ b/decrypt-secrets.sh @@ -14,7 +14,22 @@ # See the License for the specific language governing permissions and # limitations under the License. -password=$1 +set -euo pipefail -openssl aes-256-cbc -k "$password" -in secrets.tar.enc -out secrets.tar -d -tar xvf secrets.tar +# Always cd to the project root. +readonly root="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +cd ${root} + +# Use SECRET_MANAGER_PROJECT if set, fallback to cloud-devrel-kokoro-resources. +readonly project_id="${SECRET_MANAGER_PROJECT:-cloud-devrel-kokoro-resources}" + +# If there's already a secret file, skip retrieving the secret. +if [[ -f "service-account.json" ]]; then + echo "The secret already exists, skipping." + exit 0 +fi + +gcloud secrets versions access latest \ + --secret="getting-started-python-service-account" \ + --project="${project_id}" \ + > service-account.json diff --git a/encrypt-secrets.sh b/encrypt-secrets.sh index 5faf4a41..ddad76db 100755 --- a/encrypt-secrets.sh +++ b/encrypt-secrets.sh @@ -14,9 +14,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -read -s -p "Enter password for encryption: " password -echo +set -euo pipefail -tar cvf secrets.tar service-account.json -openssl aes-256-cbc -k "$password" -in secrets.tar -out secrets.tar.enc -rm secrets.tar +# Always cd to the project root. +readonly root="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +cd ${root} + +# Use SECRET_MANAGER_PROJECT if set, fallback to cloud-devrel-kokoro-resources. +readonly project_id="${SECRET_MANAGER_PROJECT:-cloud-devrel-kokoro-resources}" + +gcloud secrets versions add "getting-started-python-service-account" \ + --project="${project_id}" \ + --data-file="service-account.json" From 18b221ab0057509244385fe3f2551b33c1699f0b Mon Sep 17 00:00:00 2001 From: Takashi Matsuo Date: Fri, 23 Oct 2020 21:30:05 +0000 Subject: [PATCH 163/395] testing: include Dockerfile with a newer gcloud command --- .kokoro/common.cfg | 2 +- .kokoro/docker/Dockerfile | 56 +++++++++++++++++++++++++++++++++++++++ .trampolinerc | 50 ++++++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 .kokoro/docker/Dockerfile create mode 100644 .trampolinerc diff --git a/.kokoro/common.cfg b/.kokoro/common.cfg index 0446da72..a9aad275 100644 --- a/.kokoro/common.cfg +++ b/.kokoro/common.cfg @@ -12,7 +12,7 @@ build_file: "getting-started-python/.kokoro/trampoline_v2.sh" # Use the Python worker docker iamge. env_vars: { key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/python@sha256:4b6ba8c199e96248980db4538065cddeea594138b9b9fb2d0388603922087747" + value: "gcr.io/cloud-devrel-kokoro-resources/python/getting-started-python" } # Tell the trampoline which build file to use. diff --git a/.kokoro/docker/Dockerfile b/.kokoro/docker/Dockerfile new file mode 100644 index 00000000..338603c5 --- /dev/null +++ b/.kokoro/docker/Dockerfile @@ -0,0 +1,56 @@ +# Copyright 2018 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +FROM gcr.io/cloud-devrel-kokoro-resources/python-base:latest + +# Install libraries needed by third-party python packages that we depend on. +RUN apt update \ + && apt install -y \ + graphviz \ + libcurl4-openssl-dev \ + libffi-dev \ + libjpeg-dev \ + libmagickwand-dev \ + libmemcached-dev \ + libmysqlclient-dev \ + libpng12-dev \ + libpq-dev \ + libssl-dev \ + libxml2-dev \ + libxslt1-dev \ + openssl \ + portaudio19-dev \ + python-pyaudio \ + zlib1g-dev \ + && apt clean + +RUN python --version +RUN which python + +# Setup Cloud SDK +ENV CLOUD_SDK_VERSION 315.0.0 +# Use system python for cloud sdk. +ENV CLOUDSDK_PYTHON /usr/bin/python +RUN wget https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-sdk-$CLOUD_SDK_VERSION-linux-x86_64.tar.gz +RUN tar xzf google-cloud-sdk-$CLOUD_SDK_VERSION-linux-x86_64.tar.gz +RUN /google-cloud-sdk/install.sh +ENV PATH /google-cloud-sdk/bin:$PATH + +# Setup the user profile for pip +ENV PATH ~/.local/bin:/root/.local/bin:$PATH + +# Install the current version of nox. +RUN python3 -m pip install --user --no-cache-dir nox==2020.8.22 + +CMD ["nox"] diff --git a/.trampolinerc b/.trampolinerc new file mode 100644 index 00000000..17f21195 --- /dev/null +++ b/.trampolinerc @@ -0,0 +1,50 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Add required env vars here. +required_envvars+=( +) + +# Add env vars which are passed down into the container here. +pass_down_envvars+=( + # We test this envvar in tests/python/test_envvar.py. + "TEST_ENV" +) + +# Prevent unintentional override on the default image. +if [[ "${TRAMPOLINE_IMAGE_UPLOAD:-false}" == "true" ]] && \ + [[ -z "${TRAMPOLINE_IMAGE:-}" ]]; then + echo "Please set TRAMPOLINE_IMAGE if you want to upload the Docker image." + exit 1 +fi + +# Define the default value if it makes sense. +if [[ -z "${TRAMPOLINE_IMAGE_UPLOAD:-}" ]]; then + TRAMPOLINE_IMAGE_UPLOAD="" +fi + +if [[ -z "${TRAMPOLINE_IMAGE:-}" ]]; then + TRAMPOLINE_IMAGE="" +fi + +if [[ -z "${TRAMPOLINE_DOCKERFILE:-}" ]]; then + TRAMPOLINE_DOCKERFILE=".kokoro/docker/Dockerfile" +fi + +if [[ -z "${TRAMPOLINE_BUILD_FILE:-}" ]]; then + TRAMPOLINE_BUILD_FILE="" +fi + +# The build will show some commands and docker build logs. +TRAMPOLINE_VERBOSE="true" From 3c85982fb84db8502f69bf930f9f6f95fbbaa8f8 Mon Sep 17 00:00:00 2001 From: Takashi Matsuo Date: Fri, 23 Oct 2020 21:36:42 +0000 Subject: [PATCH 164/395] testing: upload the docker image after successful periodic builds. --- .kokoro/system_tests.cfg | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.kokoro/system_tests.cfg b/.kokoro/system_tests.cfg index e69de29b..8250ca9c 100644 --- a/.kokoro/system_tests.cfg +++ b/.kokoro/system_tests.cfg @@ -0,0 +1,5 @@ +# Upload the docker image after successful builds. +env_vars: { + key: "TRAMPOLINE_IMAGE_UPLOAD" + value: "true" +} From b893f2188c002ea93fad6c8d6a4a34106e511fc9 Mon Sep 17 00:00:00 2001 From: Takashi Matsuo Date: Fri, 23 Oct 2020 21:38:18 +0000 Subject: [PATCH 165/395] testing: presubmit builds also upload the docker image --- .kokoro/common.cfg | 6 ++++++ .kokoro/system_tests.cfg | 5 ----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.kokoro/common.cfg b/.kokoro/common.cfg index a9aad275..f58e4f76 100644 --- a/.kokoro/common.cfg +++ b/.kokoro/common.cfg @@ -20,3 +20,9 @@ env_vars: { key: "TRAMPOLINE_BUILD_FILE" value: ".kokoro/system_tests.sh" } + +# Upload the docker image after successful builds. +env_vars: { + key: "TRAMPOLINE_IMAGE_UPLOAD" + value: "true" +} diff --git a/.kokoro/system_tests.cfg b/.kokoro/system_tests.cfg index 8250ca9c..e69de29b 100644 --- a/.kokoro/system_tests.cfg +++ b/.kokoro/system_tests.cfg @@ -1,5 +0,0 @@ -# Upload the docker image after successful builds. -env_vars: { - key: "TRAMPOLINE_IMAGE_UPLOAD" - value: "true" -} From cbfaa574c7d9da3943a2417ad719ce3a02da8214 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 26 Oct 2020 02:44:03 +0000 Subject: [PATCH 166/395] chore(deps): update dependency cryptography to v3.2 --- authenticating-users/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index 316dfbdc..5f7881a1 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -1,6 +1,6 @@ # [START getting_started_requirements] Flask==1.1.2 -cryptography==3.1.1 +cryptography==3.2 python-jose[cryptography]==3.2.0 requests==2.24.0 # [END getting_started_requirements] From b1254e701f855b78f293b905fe53f36de37072e7 Mon Sep 17 00:00:00 2001 From: Takashi Matsuo Date: Mon, 26 Oct 2020 17:28:35 +0000 Subject: [PATCH 167/395] fix(bookshelf): explicitly make the uploaded image publicly readable I also removed `allUser:Storage.viewer` permission from the testing bucket. --- bookshelf/storage.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bookshelf/storage.py b/bookshelf/storage.py index f6f9b1d3..f18b78df 100644 --- a/bookshelf/storage.py +++ b/bookshelf/storage.py @@ -63,6 +63,8 @@ def upload_file(file_stream, filename, content_type): blob.upload_from_string( file_stream, content_type=content_type) + # Ensure the file is publicly readable. + blob.make_public() url = blob.public_url # [END bookshelf_cloud_storage_client] From 1b9f28f7e2534fc2e4c2bb8345721b23af7145d2 Mon Sep 17 00:00:00 2001 From: Justin Beckwith Date: Tue, 10 Nov 2020 14:30:09 -0800 Subject: [PATCH 168/395] chore: add CODEOWNERS (#301) --- CODEOWNERS | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 CODEOWNERS diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 00000000..0480d02a --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,10 @@ +# Code owners file. +# This file controls who is tagged for review for any given pull request. +# +# For syntax help see: +# https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners#codeowners-syntax + + +# The python-samples-owners team is the default owner for anything not +# explicitly taken by someone else. +* @GoogleCloudPlatform/python-samples-owners From d030220b07c401a056535e8552de225dff1ab17c Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 25 Nov 2020 20:34:11 +0100 Subject: [PATCH 169/395] chore(deps): update dependency requests to v2.25.0 (#302) --- authenticating-users/requirements.txt | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index 5f7881a1..90a6a7c0 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -2,5 +2,5 @@ Flask==1.1.2 cryptography==3.2 python-jose[cryptography]==3.2.0 -requests==2.24.0 +requests==2.25.0 # [END getting_started_requirements] diff --git a/requirements.txt b/requirements.txt index 102e4189..88aa13c4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ flake8==3.8.4 nox==2020.8.22 pytest==6.1.1 -requests==2.24.0 +requests==2.25.0 From 7926664700dcd0a3b950c00e000b4ff0969013ce Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 25 Nov 2020 20:38:03 +0100 Subject: [PATCH 170/395] chore(deps): update dependency google-cloud-firestore to v2 (#300) --- background/app/requirements.txt | 2 +- background/function/requirements.txt | 2 +- bookshelf/requirements.txt | 2 +- sessions/requirements.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index d05bdb07..62fea908 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ -google-cloud-firestore==1.9.0 +google-cloud-firestore==2.0.1 google-cloud-pubsub==1.7.0 flask==1.1.2 diff --git a/background/function/requirements.txt b/background/function/requirements.txt index 73cdba4e..4221f697 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ google-cloud-translate==3.0.1 -google-cloud-firestore==1.9.0 +google-cloud-firestore==2.0.1 diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 5b68e4fb..fdb46160 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,5 +1,5 @@ Flask==1.1.2 -google-cloud-firestore==1.9.0 +google-cloud-firestore==2.0.1 google-cloud-storage==1.32.0 google-cloud-logging==1.15.1 google-cloud-error-reporting==1.0.0 diff --git a/sessions/requirements.txt b/sessions/requirements.txt index cdd61b7c..eca1c3ac 100644 --- a/sessions/requirements.txt +++ b/sessions/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-firestore==1.9.0 +google-cloud-firestore==2.0.1 flask==1.1.2 From d4adbe8f3d7ee1435fa8097d1ec6325e857e9254 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 25 Nov 2020 20:46:49 +0100 Subject: [PATCH 171/395] chore(deps): update dependency google-cloud-storage to v1.33.0 (#303) --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index fdb46160..9e877332 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,6 +1,6 @@ Flask==1.1.2 google-cloud-firestore==2.0.1 -google-cloud-storage==1.32.0 +google-cloud-storage==1.33.0 google-cloud-logging==1.15.1 google-cloud-error-reporting==1.0.0 gunicorn==20.0.4 From c05a5ae89f089927f2d7fae71b315430eb56c6ed Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 25 Nov 2020 22:36:19 +0100 Subject: [PATCH 172/395] chore(deps): update dependency pytest to v6.1.2 (#299) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 88aa13c4..3de1b229 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ flake8==3.8.4 nox==2020.8.22 -pytest==6.1.1 +pytest==6.1.2 requests==2.25.0 From d0c87be36df7048f12560a53bc578e8dfcdc75de Mon Sep 17 00:00:00 2001 From: Takashi Matsuo Date: Fri, 22 Jan 2021 09:53:18 -0800 Subject: [PATCH 173/395] fix: update Dockerfile for green builds (#313) --- .kokoro/docker/Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.kokoro/docker/Dockerfile b/.kokoro/docker/Dockerfile index 338603c5..2830f0af 100644 --- a/.kokoro/docker/Dockerfile +++ b/.kokoro/docker/Dockerfile @@ -24,7 +24,7 @@ RUN apt update \ libmagickwand-dev \ libmemcached-dev \ libmysqlclient-dev \ - libpng12-dev \ + libpng-dev \ libpq-dev \ libssl-dev \ libxml2-dev \ @@ -39,7 +39,7 @@ RUN python --version RUN which python # Setup Cloud SDK -ENV CLOUD_SDK_VERSION 315.0.0 +ENV CLOUD_SDK_VERSION 324.0.0 # Use system python for cloud sdk. ENV CLOUDSDK_PYTHON /usr/bin/python RUN wget https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-sdk-$CLOUD_SDK_VERSION-linux-x86_64.tar.gz @@ -51,6 +51,6 @@ ENV PATH /google-cloud-sdk/bin:$PATH ENV PATH ~/.local/bin:/root/.local/bin:$PATH # Install the current version of nox. -RUN python3 -m pip install --user --no-cache-dir nox==2020.8.22 +RUN python3 -m pip install --user --no-cache-dir nox==2020.12.31 CMD ["nox"] From 6207477dcc7afa6d6095e22a14358ca4ed2bb1d9 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 22 Jan 2021 19:00:08 +0100 Subject: [PATCH 174/395] chore(deps): update dependency cryptography to v3.3.1 (#298) [![WhiteSource Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [cryptography](https://togithub.com/pyca/cryptography) | `==3.2` -> `==3.3.1` | [![age](https://badges.renovateapi.com/packages/pypi/cryptography/3.3.1/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/pypi/cryptography/3.3.1/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/pypi/cryptography/3.3.1/compatibility-slim/3.2)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/pypi/cryptography/3.3.1/confidence-slim/3.2)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
pyca/cryptography ### [`v3.3.1`](https://togithub.com/pyca/cryptography/compare/3.3...3.3.1) [Compare Source](https://togithub.com/pyca/cryptography/compare/3.3...3.3.1) ### [`v3.3`](https://togithub.com/pyca/cryptography/compare/3.2.1...3.3) [Compare Source](https://togithub.com/pyca/cryptography/compare/3.2.1...3.3) ### [`v3.2.1`](https://togithub.com/pyca/cryptography/compare/3.2...3.2.1) [Compare Source](https://togithub.com/pyca/cryptography/compare/3.2...3.2.1)
--- ### Renovate configuration :date: **Schedule**: At any time (no schedule defined). :vertical_traffic_light: **Automerge**: Disabled by config. Please merge this manually once you are satisfied. :recycle: **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. :no_bell: **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#github/GoogleCloudPlatform/getting-started-python). --- authenticating-users/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index 90a6a7c0..88d30586 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -1,6 +1,6 @@ # [START getting_started_requirements] Flask==1.1.2 -cryptography==3.2 +cryptography==3.3.1 python-jose[cryptography]==3.2.0 requests==2.25.0 # [END getting_started_requirements] From dc7eb04299feb3a27a94465090df4f52745dad4d Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 22 Jan 2021 19:07:19 +0100 Subject: [PATCH 175/395] chore(deps): update dependency requests to v2.25.1 (#310) --- authenticating-users/requirements.txt | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index 88d30586..693d0851 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -2,5 +2,5 @@ Flask==1.1.2 cryptography==3.3.1 python-jose[cryptography]==3.2.0 -requests==2.25.0 +requests==2.25.1 # [END getting_started_requirements] diff --git a/requirements.txt b/requirements.txt index 3de1b229..b490e8fd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ flake8==3.8.4 nox==2020.8.22 pytest==6.1.2 -requests==2.25.0 +requests==2.25.1 From b64b4f4b8ea21bea3f6434a6eae80059dbf6f8d7 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 22 Jan 2021 19:13:21 +0100 Subject: [PATCH 176/395] chore(deps): update dependency nox to v2020.12.31 (#311) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b490e8fd..e4d2e813 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ flake8==3.8.4 -nox==2020.8.22 +nox==2020.12.31 pytest==6.1.2 requests==2.25.1 From 0b79d4ef5f13d78b2c38a23e5fed024be68faec9 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 22 Jan 2021 19:21:13 +0100 Subject: [PATCH 177/395] chore(deps): update dependency google-cloud-storage to v1.35.0 (#308) --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 9e877332..90de3ae3 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,6 +1,6 @@ Flask==1.1.2 google-cloud-firestore==2.0.1 -google-cloud-storage==1.33.0 +google-cloud-storage==1.35.0 google-cloud-logging==1.15.1 google-cloud-error-reporting==1.0.0 gunicorn==20.0.4 From 1e715d3cca42fe16a9f895f73cd243c05cb526be Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 22 Jan 2021 19:34:02 +0100 Subject: [PATCH 178/395] chore(deps): update dependency google-cloud-translate to v3.0.2 (#307) --- background/function/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/function/requirements.txt b/background/function/requirements.txt index 4221f697..60ca4301 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-translate==3.0.1 +google-cloud-translate==3.0.2 google-cloud-firestore==2.0.1 From c3d07bb744597593cf6138bb1789a441fb397538 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 22 Jan 2021 19:42:15 +0100 Subject: [PATCH 179/395] chore(deps): update dependency google-cloud-logging to v2 (#304) * chore(deps): update dependency google-cloud-logging to v2 * fix the build Co-authored-by: Takashi Matsuo --- bookshelf/main.py | 2 +- bookshelf/requirements.txt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bookshelf/main.py b/bookshelf/main.py index 1962c485..789bec2e 100644 --- a/bookshelf/main.py +++ b/bookshelf/main.py @@ -59,7 +59,7 @@ def upload_image_file(img): logging.basicConfig(level=logging.INFO) client = google.cloud.logging.Client() # Attaches a Google Stackdriver logging handler to the root logger - client.setup_logging(logging.INFO) + client.setup_logging() @app.route('/') diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 90de3ae3..4a865fe5 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,7 +1,7 @@ Flask==1.1.2 google-cloud-firestore==2.0.1 google-cloud-storage==1.35.0 -google-cloud-logging==1.15.1 -google-cloud-error-reporting==1.0.0 +google-cloud-logging==2.1.1 +google-cloud-error-reporting==1.1.0 gunicorn==20.0.4 six==1.15.0 From a273a9e81ca26d1fcc825fc39bf8d1f26347e1dc Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 22 Jan 2021 19:43:21 +0100 Subject: [PATCH 180/395] chore(deps): update dependency google-cloud-firestore to v2.0.2 (#306) --- background/app/requirements.txt | 2 +- background/function/requirements.txt | 2 +- bookshelf/requirements.txt | 2 +- sessions/requirements.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 62fea908..95fd2e05 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ -google-cloud-firestore==2.0.1 +google-cloud-firestore==2.0.2 google-cloud-pubsub==1.7.0 flask==1.1.2 diff --git a/background/function/requirements.txt b/background/function/requirements.txt index 60ca4301..f8b183e9 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ google-cloud-translate==3.0.2 -google-cloud-firestore==2.0.1 +google-cloud-firestore==2.0.2 diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 4a865fe5..927e9c97 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,5 +1,5 @@ Flask==1.1.2 -google-cloud-firestore==2.0.1 +google-cloud-firestore==2.0.2 google-cloud-storage==1.35.0 google-cloud-logging==2.1.1 google-cloud-error-reporting==1.1.0 diff --git a/sessions/requirements.txt b/sessions/requirements.txt index eca1c3ac..43c7ea10 100644 --- a/sessions/requirements.txt +++ b/sessions/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-firestore==2.0.1 +google-cloud-firestore==2.0.2 flask==1.1.2 From 171bf18cbb39a54d5d144be41a7e5a7aebc41447 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 22 Jan 2021 20:22:19 +0100 Subject: [PATCH 181/395] chore(deps): update dependency google-cloud-pubsub to v2 (#288) * chore(deps): update dependency google-cloud-pubsub to v2 * run pubsub fixup * fix lint * refactor publish method Co-authored-by: Leah Cole Co-authored-by: Leah E. Cole <6719667+leahecole@users.noreply.github.com> --- background/app/main.py | 50 +++++++++++++++------------ background/app/main_test.py | 61 ++++++++++++++++++--------------- background/app/requirements.txt | 2 +- 3 files changed, 62 insertions(+), 51 deletions(-) diff --git a/background/app/main.py b/background/app/main.py index 487e10a4..5474d54c 100644 --- a/background/app/main.py +++ b/background/app/main.py @@ -32,53 +32,59 @@ publisher = pubsub.PublisherClient() # Keep this list of supported languages up to date -ACCEPTABLE_LANGUAGES = ('de', 'en', 'es', 'fr', 'ja', 'sw') +ACCEPTABLE_LANGUAGES = ("de", "en", "es", "fr", "ja", "sw") # [END getting_started_background_app_main] # [START getting_started_background_app_list] -@app.route('/', methods=['GET']) +@app.route("/", methods=["GET"]) def index(): - """ The home page has a list of prior translations and a form to - ask for a new translation. + """The home page has a list of prior translations and a form to + ask for a new translation. """ doc_list = [] - docs = db.collection('translations').stream() + docs = db.collection("translations").stream() for doc in docs: doc_list.append(doc.to_dict()) - return render_template('index.html', translations=doc_list) + return render_template("index.html", translations=doc_list) + + # [END getting_started_background_app_list] # [START getting_started_background_app_request] -@app.route('/request-translation', methods=['POST']) +@app.route("/request-translation", methods=["POST"]) def translate(): - """ Handle a request to translate a string (form field 'v') to a given - language (form field 'lang'), by sending a PubSub message to a topic. + """Handle a request to translate a string (form field 'v') to a given + language (form field 'lang'), by sending a PubSub message to a topic. """ - source_string = request.form.get('v', '') - to_language = request.form.get('lang', '') + source_string = request.form.get("v", "") + to_language = request.form.get("lang", "") - if source_string == '': - error_message = 'Empty value' + if source_string == "": + error_message = "Empty value" return error_message, 400 if to_language not in ACCEPTABLE_LANGUAGES: - error_message = 'Unsupported language: {}'.format(to_language) + error_message = "Unsupported language: {}".format(to_language) return error_message, 400 message = { - 'Original': source_string, - 'Language': to_language, - 'Translated': '', - 'OriginalLanguage': '', + "Original": source_string, + "Language": to_language, + "Translated": "", + "OriginalLanguage": "", } - topic_name = 'projects/{}/topics/{}'.format( - os.getenv('GOOGLE_CLOUD_PROJECT'), 'translate' + topic_name = "projects/{}/topics/{}".format( + os.getenv("GOOGLE_CLOUD_PROJECT"), "translate" + ) + publisher.publish( + topic=topic_name, data=json.dumps(message).encode("utf8") ) - publisher.publish(topic_name, json.dumps(message).encode('utf8')) - return redirect('/') + return redirect("/") + + # [END getting_started_background_app_request] diff --git a/background/app/main_test.py b/background/app/main_test.py index 0832fb3b..f0dc9fe7 100644 --- a/background/app/main_test.py +++ b/background/app/main_test.py @@ -23,34 +23,33 @@ credentials, project_id = google.auth.default() -os.environ['GOOGLE_CLOUD_PROJECT'] = project_id -SUBSCRIPTION_NAME = 'projects/{}/subscriptions/{}'.format( - project_id, 'test-' + str(uuid.uuid4()) -) -TOPIC_NAME = 'projects/{}/topics/{}'.format( - project_id, 'translate' +os.environ["GOOGLE_CLOUD_PROJECT"] = project_id +SUBSCRIPTION_NAME = "projects/{}/subscriptions/{}".format( + project_id, "test-" + str(uuid.uuid4()) ) +TOPIC_NAME = "projects/{}/topics/{}".format(project_id, "translate") @pytest.yield_fixture def db(): def clear_collection(collection): - """ Removes every document from the collection, to make it easy to see - what has been added by the current test run. + """Removes every document from the collection, to make it easy to see + what has been added by the current test run. """ for doc in collection.stream(): doc.reference.delete() client = firestore.Client() - translations = client.collection('translations') + translations = client.collection("translations") clear_collection(translations) - translations.add({ - 'Original': 'A testing message', - 'Language': 'fr', - 'Translated': '"A testing message", but in French', - 'OriginalLanguage': 'en', + translations.add( + { + "Original": "A testing message", + "Language": "fr", + "Translated": '"A testing message", but in French', + "OriginalLanguage": "en", }, - document_id='test translation' + document_id="test translation", ) yield client @@ -65,10 +64,10 @@ def publisher(): def subscriber(): subscriber = pubsub.SubscriberClient() subscriber.create_subscription( - SUBSCRIPTION_NAME, TOPIC_NAME + request={"name": SUBSCRIPTION_NAME, "topic": TOPIC_NAME} ) yield subscriber - subscriber.delete_subscription(SUBSCRIPTION_NAME) + subscriber.delete_subscription(request={"subscription": SUBSCRIPTION_NAME}) def test_index(db, publisher): @@ -77,11 +76,11 @@ def test_index(db, publisher): main.publisher = publisher client = main.app.test_client() - r = client.get('/') + r = client.get("/") assert r.status_code == 200 - response_text = r.data.decode('utf-8') - assert 'Text to translate' in response_text - assert 'but in French' in response_text + response_text = r.data.decode("utf-8") + assert "Text to translate" in response_text + assert "but in French" in response_text def test_translate(db, publisher, subscriber): @@ -90,14 +89,20 @@ def test_translate(db, publisher, subscriber): main.publisher = publisher client = main.app.test_client() - r = client.post('/request-translation', data={ - 'v': 'This is a test', - 'lang': 'fr', - }) + r = client.post( + "/request-translation", + data={ + "v": "This is a test", + "lang": "fr", + }, + ) assert r.status_code < 400 - response = subscriber.pull(SUBSCRIPTION_NAME, 1, timeout=10.0) + response = subscriber.pull( + request={"subscription": SUBSCRIPTION_NAME, "max_messages": 1}, + timeout=10.0, + ) assert len(response.received_messages) == 1 - assert b'This is a test' in response.received_messages[0].message.data - assert b'fr' in response.received_messages[0].message.data + assert b"This is a test" in response.received_messages[0].message.data + assert b"fr" in response.received_messages[0].message.data diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 95fd2e05..3ba144cd 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ google-cloud-firestore==2.0.2 -google-cloud-pubsub==1.7.0 +google-cloud-pubsub==2.2.0 flask==1.1.2 From 5b87779f87cdc6e2d5f6cbaa99cb328c1eeb37d5 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 22 Jan 2021 23:58:53 +0100 Subject: [PATCH 182/395] chore(deps): update dependency pytest to v6.2.1 (#309) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e4d2e813..43cd9314 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ flake8==3.8.4 nox==2020.12.31 -pytest==6.1.2 +pytest==6.2.1 requests==2.25.1 From a7f54e60cf57135bca22d39398af0f5b2bb54a42 Mon Sep 17 00:00:00 2001 From: Takashi Matsuo Date: Mon, 25 Jan 2021 14:52:38 -0800 Subject: [PATCH 183/395] fix(inclusive-language): update the bot name (#314) * fix(inclusive-language): update the bot name * update trampoline_v2 to 2.0.10 * Use the new binary --- .kokoro/system_tests.sh | 8 ++-- .kokoro/trampoline_v2.sh | 97 +++++++++++++++++++++++----------------- 2 files changed, 59 insertions(+), 46 deletions(-) diff --git a/.kokoro/system_tests.sh b/.kokoro/system_tests.sh index fae2d232..29bb4ae5 100755 --- a/.kokoro/system_tests.sh +++ b/.kokoro/system_tests.sh @@ -39,9 +39,9 @@ fi nox -s lint nox -s run_tests -# If this is a nightly build, send the test log to the Build Cop Bot. -# See https://github.com/googleapis/repo-automation-bots/tree/master/packages/buildcop. +# If this is a nightly build, send the test log to the Flaky Bot. +# See https://github.com/googleapis/repo-automation-bots/tree/HEAD/packages/flakybot. if [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"system_tests"* ]]; then - chmod +x $KOKORO_GFILE_DIR/linux_amd64/buildcop - $KOKORO_GFILE_DIR/linux_amd64/buildcop + chmod +x $KOKORO_GFILE_DIR/linux_amd64/flakybot + $KOKORO_GFILE_DIR/linux_amd64/flakybot fi diff --git a/.kokoro/trampoline_v2.sh b/.kokoro/trampoline_v2.sh index 8a97c4cd..ef6972b4 100755 --- a/.kokoro/trampoline_v2.sh +++ b/.kokoro/trampoline_v2.sh @@ -15,6 +15,12 @@ # trampoline_v2.sh # +# If you want to make a change to this file, consider doing so at: +# https://github.com/googlecloudplatform/docker-ci-helper +# +# This script is for running CI builds. For Kokoro builds, we +# set this script to `build_file` field in the Kokoro configuration. + # This script does 3 things. # # 1. Prepare the Docker image for the test @@ -23,15 +29,6 @@ # # in a way that is somewhat compatible with trampoline_v1. # -# To run this script, first download few files from gcs to /dev/shm. -# (/dev/shm is passed into the container as KOKORO_GFILE_DIR). -# -# gsutil cp gs://cloud-devrel-kokoro-resources/python-docs-samples/secrets_viewer_service_account.json /dev/shm -# gsutil cp gs://cloud-devrel-kokoro-resources/python-docs-samples/automl_secrets.txt /dev/shm -# -# Then run the script. -# .kokoro/trampoline_v2.sh -# # These environment variables are required: # TRAMPOLINE_IMAGE: The docker image to use. # TRAMPOLINE_DOCKERFILE: The location of the Dockerfile. @@ -43,16 +40,17 @@ # TRAMPOLINE_BUILD_FILE: The script to run in the docker container. # TRAMPOLINE_WORKSPACE: The workspace path in the docker container. # Defaults to /workspace. -# TRAMPOLINE_SKIP_DOWNLOAD_IMAGE: Skip downloading the image when you -# know you have the image locally. -# # Potentially there are some repo specific envvars in .trampolinerc in # the project root. - +# +# Here is an example for running this script. +# TRAMPOLINE_IMAGE=gcr.io/cloud-devrel-kokoro-resources/node:10-user \ +# TRAMPOLINE_BUILD_FILE=.kokoro/system-test.sh \ +# .kokoro/trampoline_v2.sh set -euo pipefail -TRAMPOLINE_VERSION="2.0.2" +TRAMPOLINE_VERSION="2.0.10" if command -v tput >/dev/null && [[ -n "${TERM:-}" ]]; then readonly IO_COLOR_RED="$(tput setaf 1)" @@ -162,9 +160,10 @@ if [[ -n "${KOKORO_BUILD_ID:-}" ]]; then "KOKORO_GITHUB_COMMIT" "KOKORO_GITHUB_PULL_REQUEST_NUMBER" "KOKORO_GITHUB_PULL_REQUEST_COMMIT" - # For Build Cop Bot + # For Flaky Bot "KOKORO_GITHUB_COMMIT_URL" "KOKORO_GITHUB_PULL_REQUEST_URL" + "KOKORO_BUILD_ARTIFACTS_SUBDIR" ) elif [[ "${TRAVIS:-}" == "true" ]]; then RUNNING_IN_CI="true" @@ -232,21 +231,6 @@ elif [[ "${CIRCLECI:-}" == "true" ]]; then fi # Configure the service account for pulling the docker image. -if [[ -n "${TRAMPOLINE_SERVICE_ACCOUNT:-}" ]]; then - - mkdir -p "${tmpdir}/gcloud" - gcloud_config_dir="${tmpdir}/gcloud" - - log_yellow "Using isolated gcloud config: ${gcloud_config_dir}." - export CLOUDSDK_CONFIG="${gcloud_config_dir}" - - log_yellow "Using ${TRAMPOLINE_SERVICE_ACCOUNT} for authentication." - gcloud auth activate-service-account \ - --key-file "${TRAMPOLINE_SERVICE_ACCOUNT}" - log_yellow "Configuring Container Registry access" - gcloud auth configure-docker --quiet -fi - function repo_root() { local dir="$1" while [[ ! -d "${dir}/.git" ]]; do @@ -269,6 +253,23 @@ fi log_yellow "Changing to the project root: ${PROJECT_ROOT}." cd "${PROJECT_ROOT}" +# To support relative path for `TRAMPOLINE_SERVICE_ACCOUNT`, we need +# to use this environment variable in `PROJECT_ROOT`. +if [[ -n "${TRAMPOLINE_SERVICE_ACCOUNT:-}" ]]; then + + mkdir -p "${tmpdir}/gcloud" + gcloud_config_dir="${tmpdir}/gcloud" + + log_yellow "Using isolated gcloud config: ${gcloud_config_dir}." + export CLOUDSDK_CONFIG="${gcloud_config_dir}" + + log_yellow "Using ${TRAMPOLINE_SERVICE_ACCOUNT} for authentication." + gcloud auth activate-service-account \ + --key-file "${TRAMPOLINE_SERVICE_ACCOUNT}" + log_yellow "Configuring Container Registry access" + gcloud auth configure-docker --quiet +fi + required_envvars=( # The basic trampoline configurations. "TRAMPOLINE_IMAGE" @@ -288,23 +289,35 @@ do fi done -if [[ "${TRAMPOLINE_SKIP_DOWNLOAD_IMAGE:-false}" == "true" ]]; then - log_yellow "Re-using the local Docker image." - has_cache="true" -else - log_yellow "Preparing Docker image." +# We want to support legacy style TRAMPOLINE_BUILD_FILE used with V1 +# script: e.g. "github/repo-name/.kokoro/run_tests.sh" +TRAMPOLINE_BUILD_FILE="${TRAMPOLINE_BUILD_FILE#github/*/}" +log_yellow "Using TRAMPOLINE_BUILD_FILE: ${TRAMPOLINE_BUILD_FILE}" + +# ignore error on docker operations and test execution +set +e + +log_yellow "Preparing Docker image." +# We only download the docker image in CI builds. +if [[ "${RUNNING_IN_CI:-}" == "true" ]]; then # Download the docker image specified by `TRAMPOLINE_IMAGE` - set +e # ignore error on docker operations # We may want to add --max-concurrent-downloads flag. log_yellow "Start pulling the Docker image: ${TRAMPOLINE_IMAGE}." if docker pull "${TRAMPOLINE_IMAGE}"; then log_green "Finished pulling the Docker image: ${TRAMPOLINE_IMAGE}." - has_cache="true" + has_image="true" else log_red "Failed pulling the Docker image: ${TRAMPOLINE_IMAGE}." - has_cache="false" + has_image="false" + fi +else + # For local run, check if we have the image. + if docker images "${TRAMPOLINE_IMAGE}" | grep "${TRAMPOLINE_IMAGE%:*}"; then + has_image="true" + else + has_image="false" fi fi @@ -330,12 +343,12 @@ if [[ "${TRAMPOLINE_DOCKERFILE:-none}" != "none" ]]; then "--build-arg" "UID=${user_uid}" "--build-arg" "USERNAME=${user_name}" ) - if [[ "${has_cache}" == "true" ]]; then + if [[ "${has_image}" == "true" ]]; then docker_build_flags+=("--cache-from" "${TRAMPOLINE_IMAGE}") fi log_yellow "Start building the docker image." - if [[ "${TRAMPOLINE_SHOW_COMMAND:-false}" == "true" ]]; then + if [[ "${TRAMPOLINE_VERBOSE:-false}" == "true" ]]; then echo "docker build" "${docker_build_flags[@]}" "${context_dir}" fi @@ -366,8 +379,8 @@ if [[ "${TRAMPOLINE_DOCKERFILE:-none}" != "none" ]]; then fi fi else - if [[ "${has_cache}" != "true" ]]; then - log_red "failed to download the image ${TRAMPOLINE_IMAGE}, aborting." + if [[ "${has_image}" != "true" ]]; then + log_red "We do not have ${TRAMPOLINE_IMAGE} locally, aborting." exit 1 fi fi From ad5d31cd6d64d965af24cf9af444f3a589127b96 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 3 Feb 2021 17:27:45 +0100 Subject: [PATCH 184/395] chore(deps): update dependency google-cloud-storage to v1.35.1 (#317) --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 927e9c97..7a4dbcab 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,6 +1,6 @@ Flask==1.1.2 google-cloud-firestore==2.0.2 -google-cloud-storage==1.35.0 +google-cloud-storage==1.35.1 google-cloud-logging==2.1.1 google-cloud-error-reporting==1.1.0 gunicorn==20.0.4 From 55ed914cf2e33eeb46d19a8d6862bcbeef260f5a Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 3 Feb 2021 18:39:59 +0100 Subject: [PATCH 185/395] chore(deps): update dependency google-cloud-logging to v2.2.0 (#316) --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 7a4dbcab..48868732 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,7 +1,7 @@ Flask==1.1.2 google-cloud-firestore==2.0.2 google-cloud-storage==1.35.1 -google-cloud-logging==2.1.1 +google-cloud-logging==2.2.0 google-cloud-error-reporting==1.1.0 gunicorn==20.0.4 six==1.15.0 From 16aa958ca4eb1961fc3a2fc30b105021a9a7c036 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Mon, 8 Feb 2021 18:55:58 +0100 Subject: [PATCH 186/395] chore(deps): update dependency cryptography to v3.4.2 (#319) --- authenticating-users/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index 693d0851..54a7fe5d 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -1,6 +1,6 @@ # [START getting_started_requirements] Flask==1.1.2 -cryptography==3.3.1 +cryptography==3.4.2 python-jose[cryptography]==3.2.0 requests==2.25.1 # [END getting_started_requirements] From 10a43cfb67d3ac7623575a60ab22666cbe83dcd9 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 9 Feb 2021 01:11:25 +0100 Subject: [PATCH 187/395] chore(deps): update dependency google-cloud-pubsub to v2.3.0 (#320) --- background/app/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 3ba144cd..7af8751e 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ google-cloud-firestore==2.0.2 -google-cloud-pubsub==2.2.0 +google-cloud-pubsub==2.3.0 flask==1.1.2 From 3006d3526bc5edb72c358282e3a86ca187f8602a Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 9 Feb 2021 19:01:19 +0100 Subject: [PATCH 188/395] chore(deps): update dependency pytest to v6.2.2 (#318) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 43cd9314..ba57ac10 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ flake8==3.8.4 nox==2020.12.31 -pytest==6.2.1 +pytest==6.2.2 requests==2.25.1 From 5ee7bad6f3251ca348d6702d7c27da4fe3938fa9 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 9 Feb 2021 19:01:44 +0100 Subject: [PATCH 189/395] chore(deps): update dependency cryptography to v3.4.3 (#321) --- authenticating-users/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index 54a7fe5d..a605c537 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -1,6 +1,6 @@ # [START getting_started_requirements] Flask==1.1.2 -cryptography==3.4.2 +cryptography==3.4.3 python-jose[cryptography]==3.2.0 requests==2.25.1 # [END getting_started_requirements] From 7c2f1e60277aacfd42ee4cad333b0ce79abaf256 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 12 Feb 2021 07:25:01 +0100 Subject: [PATCH 190/395] chore(deps): update dependency cryptography to v3.4.4 (#322) --- authenticating-users/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index a605c537..95baa931 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -1,6 +1,6 @@ # [START getting_started_requirements] Flask==1.1.2 -cryptography==3.4.3 +cryptography==3.4.4 python-jose[cryptography]==3.2.0 requests==2.25.1 # [END getting_started_requirements] From 5bb0f877ea55dd860b267f2e2bcc1e31b9b10533 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 16 Feb 2021 16:50:27 +0100 Subject: [PATCH 191/395] chore(deps): update dependency cryptography to v3.4.5 (#324) --- authenticating-users/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index 95baa931..8e0dbf7b 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -1,6 +1,6 @@ # [START getting_started_requirements] Flask==1.1.2 -cryptography==3.4.4 +cryptography==3.4.5 python-jose[cryptography]==3.2.0 requests==2.25.1 # [END getting_started_requirements] From c491464a7bf3b0734d6f8e200b0642194776d25e Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 16 Feb 2021 20:36:06 +0100 Subject: [PATCH 192/395] chore(deps): update dependency google-cloud-storage to v1.36.0 (#323) --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 48868732..47a1bf13 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,6 +1,6 @@ Flask==1.1.2 google-cloud-firestore==2.0.2 -google-cloud-storage==1.35.1 +google-cloud-storage==1.36.0 google-cloud-logging==2.2.0 google-cloud-error-reporting==1.1.0 gunicorn==20.0.4 From eefb3c322692376962a6e4993b8895dca5dfff46 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Sat, 20 Feb 2021 07:01:09 +0100 Subject: [PATCH 193/395] chore(deps): update dependency cryptography to v3.4.6 (#325) --- authenticating-users/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index 8e0dbf7b..59aa79ce 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -1,6 +1,6 @@ # [START getting_started_requirements] Flask==1.1.2 -cryptography==3.4.5 +cryptography==3.4.6 python-jose[cryptography]==3.2.0 requests==2.25.1 # [END getting_started_requirements] From ac29572bb0b57f974495851b29735721f356a274 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 24 Feb 2021 05:39:59 +0100 Subject: [PATCH 194/395] chore(deps): update dependency google-cloud-storage to v1.36.1 (#327) --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 47a1bf13..ffef2016 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,6 +1,6 @@ Flask==1.1.2 google-cloud-firestore==2.0.2 -google-cloud-storage==1.36.0 +google-cloud-storage==1.36.1 google-cloud-logging==2.2.0 google-cloud-error-reporting==1.1.0 gunicorn==20.0.4 From 69c662dc72963bbb907b03543d0c2340889f0e68 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 24 Feb 2021 05:41:47 +0100 Subject: [PATCH 195/395] chore(deps): update dependency google-cloud-pubsub to v2.4.0 (#326) --- background/app/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 7af8751e..c9ba5643 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ google-cloud-firestore==2.0.2 -google-cloud-pubsub==2.3.0 +google-cloud-pubsub==2.4.0 flask==1.1.2 From e2d37a8dbaabc525eaf468b0c538198d57f66e2e Mon Sep 17 00:00:00 2001 From: Takashi Matsuo Date: Tue, 2 Mar 2021 17:11:06 -0800 Subject: [PATCH 196/395] fix(bookshelf): update requirements.txt (#329) fixes #328 --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index ffef2016..9c5a1960 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -2,6 +2,6 @@ Flask==1.1.2 google-cloud-firestore==2.0.2 google-cloud-storage==1.36.1 google-cloud-logging==2.2.0 -google-cloud-error-reporting==1.1.0 +google-cloud-error-reporting<=2.0 gunicorn==20.0.4 six==1.15.0 From 567c94613b1b9d52d027943464f28236ef8c199c Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 16 Mar 2021 21:42:15 +0100 Subject: [PATCH 197/395] chore(deps): update dependency google-cloud-logging to v2.3.0 (#333) --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 9c5a1960..bc5844f8 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,7 +1,7 @@ Flask==1.1.2 google-cloud-firestore==2.0.2 google-cloud-storage==1.36.1 -google-cloud-logging==2.2.0 +google-cloud-logging==2.3.0 google-cloud-error-reporting<=2.0 gunicorn==20.0.4 six==1.15.0 From 0d21152c79993f1d8b3c167a4d75ef9160a1a0a2 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 17 Mar 2021 18:18:22 +0100 Subject: [PATCH 198/395] chore(deps): update dependency flake8 to v3.9.0 (#332) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ba57ac10..18b5d38a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -flake8==3.8.4 +flake8==3.9.0 nox==2020.12.31 pytest==6.2.2 requests==2.25.1 From 211ea7fda9dd4653fb6e70c31f60e577ad19d0e8 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 17 Mar 2021 18:18:34 +0100 Subject: [PATCH 199/395] chore(deps): update dependency google-cloud-storage to v1.36.2 (#331) --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index bc5844f8..0d485394 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,6 +1,6 @@ Flask==1.1.2 google-cloud-firestore==2.0.2 -google-cloud-storage==1.36.1 +google-cloud-storage==1.36.2 google-cloud-logging==2.3.0 google-cloud-error-reporting<=2.0 gunicorn==20.0.4 From 467d407b5718110a3eeb62f053aec01c08104b2f Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Thu, 1 Apr 2021 19:27:01 +0200 Subject: [PATCH 200/395] chore(deps): update dependency google-cloud-pubsub to v2.4.1 (#339) --- background/app/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index c9ba5643..43abd811 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ google-cloud-firestore==2.0.2 -google-cloud-pubsub==2.4.0 +google-cloud-pubsub==2.4.1 flask==1.1.2 From ebcffd71e67ba44e2849be6d35d79e5d031e4d11 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 13 Apr 2021 19:21:03 +0200 Subject: [PATCH 201/395] chore(deps): update dependency pytest to v6.2.3 (#341) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 18b5d38a..6d334e52 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ flake8==3.9.0 nox==2020.12.31 -pytest==6.2.2 +pytest==6.2.3 requests==2.25.1 From e95903494dec0e4b1f0ff11646a63a1480bedec8 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Mon, 19 Apr 2021 06:18:41 +0200 Subject: [PATCH 202/395] chore(deps): update dependency flake8 to v3.9.1 (#342) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6d334e52..a21b9542 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -flake8==3.9.0 +flake8==3.9.1 nox==2020.12.31 pytest==6.2.3 requests==2.25.1 From 7d96eb8d340cb66098c7f3574fd290bf4cff1132 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Mon, 19 Apr 2021 06:18:58 +0200 Subject: [PATCH 203/395] chore(deps): update dependency google-cloud-translate to v3.1.0 (#340) --- background/function/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/function/requirements.txt b/background/function/requirements.txt index f8b183e9..6945047e 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-translate==3.0.2 +google-cloud-translate==3.1.0 google-cloud-firestore==2.0.2 From f3fc133444d25d6d38145d1034f432c1d162523e Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Mon, 19 Apr 2021 06:20:24 +0200 Subject: [PATCH 204/395] chore(deps): update dependency google-cloud-logging to v2.3.1 (#334) --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 0d485394..27ce963a 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,7 +1,7 @@ Flask==1.1.2 google-cloud-firestore==2.0.2 google-cloud-storage==1.36.2 -google-cloud-logging==2.3.0 +google-cloud-logging==2.3.1 google-cloud-error-reporting<=2.0 gunicorn==20.0.4 six==1.15.0 From bcce9e454661546ad3e929173921b9c8a39acc09 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Mon, 19 Apr 2021 06:22:47 +0200 Subject: [PATCH 205/395] chore(deps): update dependency gunicorn to v20.1.0 (#337) --- bookshelf/requirements.txt | 2 +- gce/requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 27ce963a..e11a0f35 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -3,5 +3,5 @@ google-cloud-firestore==2.0.2 google-cloud-storage==1.36.2 google-cloud-logging==2.3.1 google-cloud-error-reporting<=2.0 -gunicorn==20.0.4 +gunicorn==20.1.0 six==1.15.0 diff --git a/gce/requirements.txt b/gce/requirements.txt index 1eb18140..9f06a741 100644 --- a/gce/requirements.txt +++ b/gce/requirements.txt @@ -1,3 +1,3 @@ flask==1.1.2 honcho==1.0.1 -gunicorn==20.0.4 +gunicorn==20.1.0 From b02dec9f50bc2721caabb01692cb39c5d4d5911e Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 20 Apr 2021 04:01:53 +0200 Subject: [PATCH 206/395] chore(deps): update dependency google-cloud-firestore to v2.1.0 (#338) --- background/app/requirements.txt | 2 +- background/function/requirements.txt | 2 +- bookshelf/requirements.txt | 2 +- sessions/requirements.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 43abd811..6ca5c941 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ -google-cloud-firestore==2.0.2 +google-cloud-firestore==2.1.0 google-cloud-pubsub==2.4.1 flask==1.1.2 diff --git a/background/function/requirements.txt b/background/function/requirements.txt index 6945047e..8c7cd3b3 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ google-cloud-translate==3.1.0 -google-cloud-firestore==2.0.2 +google-cloud-firestore==2.1.0 diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index e11a0f35..c87e002e 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,5 +1,5 @@ Flask==1.1.2 -google-cloud-firestore==2.0.2 +google-cloud-firestore==2.1.0 google-cloud-storage==1.36.2 google-cloud-logging==2.3.1 google-cloud-error-reporting<=2.0 diff --git a/sessions/requirements.txt b/sessions/requirements.txt index 43c7ea10..072e36b7 100644 --- a/sessions/requirements.txt +++ b/sessions/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-firestore==2.0.2 +google-cloud-firestore==2.1.0 flask==1.1.2 From 091fc49e80ca9bdd91b8b71920bb0b96a30bd2ca Mon Sep 17 00:00:00 2001 From: Justin Beckwith Date: Fri, 23 Apr 2021 12:13:48 -0700 Subject: [PATCH 207/395] chore: add sync-repo-settings config (#343) --- .github/sync-repo-settings.yaml | 40 +++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .github/sync-repo-settings.yaml diff --git a/.github/sync-repo-settings.yaml b/.github/sync-repo-settings.yaml new file mode 100644 index 00000000..ad730388 --- /dev/null +++ b/.github/sync-repo-settings.yaml @@ -0,0 +1,40 @@ +# Whether or not rebase-merging is enabled on this repository. +# Defaults to `true` +rebaseMergeAllowed: true + +# Whether or not squash-merging is enabled on this repository. +# Defaults to `true` +squashMergeAllowed: true + +# Whether or not PRs are merged with a merge commit on this repository. +# Defaults to `false` +mergeCommitAllowed: false + +# Rules for master branch protection +branchProtectionRules: +# Identifies the protection rule pattern. Name of the branch to be protected. +# Defaults to `master` +- pattern: main + # Can admins overwrite branch protection. + # Defaults to `true` + isAdminEnforced: false + # Number of approving reviews required to update matching branches. + # Defaults to `1` + requiredApprovingReviewCount: 1 + # Are reviews from code owners required to update matching branches. + # Defaults to `false` + requiresCodeOwnerReviews: true + # Require up to date branches + requiresStrictStatusChecks: true + # List of required status check contexts that must pass for commits to be accepted to matching branches. + requiredStatusCheckContexts: + - "kokoro" + - "cla/google" +# List of explicit permissions to add (additive only) +permissionRules: + # Team slug to add to repository permissions + - team: yoshi-admins + # Access level required, one of push|pull|admin + permission: admin + - team: python-samples-owners + permission: admin From 965291f4b28d942d040825932d1f3615f669ebb1 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 27 Apr 2021 22:49:52 +0200 Subject: [PATCH 208/395] chore(deps): update dependency cryptography to v3.4.7 (#335) Co-authored-by: Dan Lee <71398022+dandhlee@users.noreply.github.com> --- authenticating-users/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index 59aa79ce..90a407bb 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -1,6 +1,6 @@ # [START getting_started_requirements] Flask==1.1.2 -cryptography==3.4.6 +cryptography==3.4.7 python-jose[cryptography]==3.2.0 requests==2.25.1 # [END getting_started_requirements] From 879519f2586c87ed4a06f58de0cb983a429c858f Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Mon, 3 May 2021 21:29:22 +0200 Subject: [PATCH 209/395] chore(deps): pin dependency google-cloud-error-reporting to ==1.1.2 (#330) --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index c87e002e..727dc299 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -2,6 +2,6 @@ Flask==1.1.2 google-cloud-firestore==2.1.0 google-cloud-storage==1.36.2 google-cloud-logging==2.3.1 -google-cloud-error-reporting<=2.0 +google-cloud-error-reporting==1.1.2 gunicorn==20.1.0 six==1.15.0 From 7590bf9c9cfa095ad8210c0ec814a1c15761c4d8 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Mon, 3 May 2021 23:22:00 +0200 Subject: [PATCH 210/395] chore(deps): update dependency google-cloud-firestore to v2.1.1 (#345) Co-authored-by: Dan Lee <71398022+dandhlee@users.noreply.github.com> --- background/app/requirements.txt | 2 +- background/function/requirements.txt | 2 +- bookshelf/requirements.txt | 2 +- sessions/requirements.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 6ca5c941..22b2676b 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ -google-cloud-firestore==2.1.0 +google-cloud-firestore==2.1.1 google-cloud-pubsub==2.4.1 flask==1.1.2 diff --git a/background/function/requirements.txt b/background/function/requirements.txt index 8c7cd3b3..d1fad8d5 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ google-cloud-translate==3.1.0 -google-cloud-firestore==2.1.0 +google-cloud-firestore==2.1.1 diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 727dc299..f72c9b36 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,5 +1,5 @@ Flask==1.1.2 -google-cloud-firestore==2.1.0 +google-cloud-firestore==2.1.1 google-cloud-storage==1.36.2 google-cloud-logging==2.3.1 google-cloud-error-reporting==1.1.2 diff --git a/sessions/requirements.txt b/sessions/requirements.txt index 072e36b7..5e553da3 100644 --- a/sessions/requirements.txt +++ b/sessions/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-firestore==2.1.0 +google-cloud-firestore==2.1.1 flask==1.1.2 From eeef6a008730550cb47e51008b0f2c76c875c287 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 5 May 2021 18:11:10 +0200 Subject: [PATCH 211/395] chore(deps): update dependency six to v1.16.0 (#347) --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index f72c9b36..70ac74b4 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -4,4 +4,4 @@ google-cloud-storage==1.36.2 google-cloud-logging==2.3.1 google-cloud-error-reporting==1.1.2 gunicorn==20.1.0 -six==1.15.0 +six==1.16.0 From 03edb7949e087118b71bdaec264f80d721db4afc Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Sun, 9 May 2021 03:49:10 +0200 Subject: [PATCH 212/395] chore(deps): update dependency flake8 to v3.9.2 (#348) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a21b9542..5284a3a9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -flake8==3.9.1 +flake8==3.9.2 nox==2020.12.31 pytest==6.2.3 requests==2.25.1 From 8fb5d063421d71cadb0f47f1223870788601ddbb Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Thu, 13 May 2021 16:29:26 +0200 Subject: [PATCH 213/395] chore(deps): update dependency google-cloud-logging to v2.4.0 (#351) --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 70ac74b4..503ede52 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,7 +1,7 @@ Flask==1.1.2 google-cloud-firestore==2.1.1 google-cloud-storage==1.36.2 -google-cloud-logging==2.3.1 +google-cloud-logging==2.4.0 google-cloud-error-reporting==1.1.2 gunicorn==20.1.0 six==1.16.0 From 397d245e9a187152c8580d8c9b91a6ec2dec3d0c Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 14 May 2021 02:15:06 +0200 Subject: [PATCH 214/395] chore(deps): update dependency flask to v1.1.3 (#352) --- authenticating-users/requirements.txt | 2 +- background/app/requirements.txt | 2 +- bookshelf/requirements.txt | 2 +- gce/requirements.txt | 2 +- sessions/requirements.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index 90a407bb..3b55eae2 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -1,5 +1,5 @@ # [START getting_started_requirements] -Flask==1.1.2 +Flask==1.1.3 cryptography==3.4.7 python-jose[cryptography]==3.2.0 requests==2.25.1 diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 22b2676b..d45bf42d 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ google-cloud-firestore==2.1.1 google-cloud-pubsub==2.4.1 -flask==1.1.2 +flask==1.1.3 diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 503ede52..e28e5cb7 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,4 +1,4 @@ -Flask==1.1.2 +Flask==1.1.3 google-cloud-firestore==2.1.1 google-cloud-storage==1.36.2 google-cloud-logging==2.4.0 diff --git a/gce/requirements.txt b/gce/requirements.txt index 9f06a741..2e00c644 100644 --- a/gce/requirements.txt +++ b/gce/requirements.txt @@ -1,3 +1,3 @@ -flask==1.1.2 +flask==1.1.3 honcho==1.0.1 gunicorn==20.1.0 diff --git a/sessions/requirements.txt b/sessions/requirements.txt index 5e553da3..6a454e7d 100644 --- a/sessions/requirements.txt +++ b/sessions/requirements.txt @@ -1,2 +1,2 @@ google-cloud-firestore==2.1.1 -flask==1.1.2 +flask==1.1.3 From de14931cbdd6e5372c575250ce2e5f7247be4744 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 14 May 2021 02:46:59 +0200 Subject: [PATCH 215/395] chore(deps): update dependency flask to v2 (#350) Co-authored-by: Dan Lee <71398022+dandhlee@users.noreply.github.com> --- authenticating-users/requirements.txt | 2 +- background/app/requirements.txt | 2 +- bookshelf/requirements.txt | 2 +- gce/requirements.txt | 2 +- sessions/requirements.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index 3b55eae2..10b83724 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -1,5 +1,5 @@ # [START getting_started_requirements] -Flask==1.1.3 +Flask==2.0.0 cryptography==3.4.7 python-jose[cryptography]==3.2.0 requests==2.25.1 diff --git a/background/app/requirements.txt b/background/app/requirements.txt index d45bf42d..49148b95 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ google-cloud-firestore==2.1.1 google-cloud-pubsub==2.4.1 -flask==1.1.3 +flask==2.0.0 diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index e28e5cb7..04a3f02d 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,4 +1,4 @@ -Flask==1.1.3 +Flask==2.0.0 google-cloud-firestore==2.1.1 google-cloud-storage==1.36.2 google-cloud-logging==2.4.0 diff --git a/gce/requirements.txt b/gce/requirements.txt index 2e00c644..942a81a2 100644 --- a/gce/requirements.txt +++ b/gce/requirements.txt @@ -1,3 +1,3 @@ -flask==1.1.3 +flask==2.0.0 honcho==1.0.1 gunicorn==20.1.0 diff --git a/sessions/requirements.txt b/sessions/requirements.txt index 6a454e7d..fb178b62 100644 --- a/sessions/requirements.txt +++ b/sessions/requirements.txt @@ -1,2 +1,2 @@ google-cloud-firestore==2.1.1 -flask==1.1.3 +flask==2.0.0 From 3862a5854d8bdb3fc3b6daf08b52befc81fe99dc Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 14 May 2021 02:58:17 +0200 Subject: [PATCH 216/395] chore(deps): update dependency google-cloud-pubsub to v2.4.2 (#349) Co-authored-by: Dan Lee <71398022+dandhlee@users.noreply.github.com> --- background/app/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 49148b95..04d70b1a 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ google-cloud-firestore==2.1.1 -google-cloud-pubsub==2.4.1 +google-cloud-pubsub==2.4.2 flask==2.0.0 From 539f00ccfcfe77a41943cd756074ae99da418763 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 14 May 2021 03:07:36 +0200 Subject: [PATCH 217/395] chore(deps): update dependency pytest to v6.2.4 (#346) Co-authored-by: Dan Lee <71398022+dandhlee@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5284a3a9..15eafbdc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ flake8==3.9.2 nox==2020.12.31 -pytest==6.2.3 +pytest==6.2.4 requests==2.25.1 From 71ffecd2bbb6db69cedf02b273681c9dd445b433 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 14 May 2021 03:15:12 +0200 Subject: [PATCH 218/395] chore(deps): update dependency google-cloud-storage to v1.38.0 (#336) --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 04a3f02d..facd75c6 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,6 +1,6 @@ Flask==2.0.0 google-cloud-firestore==2.1.1 -google-cloud-storage==1.36.2 +google-cloud-storage==1.38.0 google-cloud-logging==2.4.0 google-cloud-error-reporting==1.1.2 gunicorn==20.1.0 From 7e692bfa1620c043193985ca323dc5a807a518ca Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 18 May 2021 16:44:51 +0200 Subject: [PATCH 219/395] chore(deps): update dependency google-cloud-pubsub to v2.5.0 (#353) --- background/app/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 04d70b1a..f9ad5573 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ google-cloud-firestore==2.1.1 -google-cloud-pubsub==2.4.2 +google-cloud-pubsub==2.5.0 flask==2.0.0 From 5dd8077c80b83c81ee4f40813d6b62830a83c9ea Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 21 May 2021 18:42:09 +0200 Subject: [PATCH 220/395] chore(deps): update dependency flask to v2.0.1 (#354) --- authenticating-users/requirements.txt | 2 +- background/app/requirements.txt | 2 +- bookshelf/requirements.txt | 2 +- gce/requirements.txt | 2 +- sessions/requirements.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index 10b83724..f9462ca1 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -1,5 +1,5 @@ # [START getting_started_requirements] -Flask==2.0.0 +Flask==2.0.1 cryptography==3.4.7 python-jose[cryptography]==3.2.0 requests==2.25.1 diff --git a/background/app/requirements.txt b/background/app/requirements.txt index f9ad5573..92410fba 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ google-cloud-firestore==2.1.1 google-cloud-pubsub==2.5.0 -flask==2.0.0 +flask==2.0.1 diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index facd75c6..128380a1 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,4 +1,4 @@ -Flask==2.0.0 +Flask==2.0.1 google-cloud-firestore==2.1.1 google-cloud-storage==1.38.0 google-cloud-logging==2.4.0 diff --git a/gce/requirements.txt b/gce/requirements.txt index 942a81a2..90e463ee 100644 --- a/gce/requirements.txt +++ b/gce/requirements.txt @@ -1,3 +1,3 @@ -flask==2.0.0 +flask==2.0.1 honcho==1.0.1 gunicorn==20.1.0 diff --git a/sessions/requirements.txt b/sessions/requirements.txt index fb178b62..4377e11c 100644 --- a/sessions/requirements.txt +++ b/sessions/requirements.txt @@ -1,2 +1,2 @@ google-cloud-firestore==2.1.1 -flask==2.0.0 +flask==2.0.1 From 68c83f906a6282cdbc5fcdf63c5830ec77fc5adc Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 1 Jun 2021 18:31:54 +0200 Subject: [PATCH 221/395] chore(deps): update dependency google-cloud-translate to v3.2.0 (#355) --- background/function/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/function/requirements.txt b/background/function/requirements.txt index d1fad8d5..80606f77 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-translate==3.1.0 +google-cloud-translate==3.2.0 google-cloud-firestore==2.1.1 From 28803be2555d868a5b94e627704c3c0ced55ec43 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Sun, 6 Jun 2021 22:15:32 +0200 Subject: [PATCH 222/395] chore(deps): update dependency python-jose to v3.3.0 (#358) --- authenticating-users/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index f9462ca1..5654d0b9 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -1,6 +1,6 @@ # [START getting_started_requirements] Flask==2.0.1 cryptography==3.4.7 -python-jose[cryptography]==3.2.0 +python-jose[cryptography]==3.3.0 requests==2.25.1 # [END getting_started_requirements] From 5ce74f25325fb11319004c074439427bf96d999e Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Sun, 6 Jun 2021 22:25:39 +0200 Subject: [PATCH 223/395] chore(deps): update dependency nox to v2021 (#360) Co-authored-by: Dan Lee <71398022+dandhlee@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 15eafbdc..1e17bf26 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ flake8==3.9.2 -nox==2020.12.31 +nox==2021.6.6 pytest==6.2.4 requests==2.25.1 From eeb32647b0bc0d034fc0306588f0615928d3b01e Mon Sep 17 00:00:00 2001 From: Dan Lee <71398022+dandhlee@users.noreply.github.com> Date: Mon, 7 Jun 2021 12:00:22 -0400 Subject: [PATCH 224/395] chore(deps): pin google-cloud-logging version (#361) --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 128380a1..df29f64f 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,7 +1,7 @@ Flask==2.0.1 google-cloud-firestore==2.1.1 google-cloud-storage==1.38.0 -google-cloud-logging==2.4.0 +google-cloud-logging==2.3.1 #DO NOT UPGRADE: google-cloud-error-reporting 1.1.2 requires google-cloud-logging<2.4,>=1.14.0 google-cloud-error-reporting==1.1.2 gunicorn==20.1.0 six==1.16.0 From 6147936db0587764040a2a905d6edd5dcbdce83b Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Sun, 13 Jun 2021 20:35:17 +0200 Subject: [PATCH 225/395] chore(deps): update dependency nox to v2021.6.12 (#362) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1e17bf26..b667a1f4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ flake8==3.9.2 -nox==2021.6.6 +nox==2021.6.12 pytest==6.2.4 requests==2.25.1 From 66ebd786cccf1c84a7dd7da7101c454a92019a2e Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 15 Jun 2021 00:09:58 +0200 Subject: [PATCH 226/395] chore(deps): update dependency google-cloud-firestore to v2.1.2 (#363) --- background/app/requirements.txt | 2 +- background/function/requirements.txt | 2 +- bookshelf/requirements.txt | 2 +- sessions/requirements.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 92410fba..595f0f83 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ -google-cloud-firestore==2.1.1 +google-cloud-firestore==2.1.2 google-cloud-pubsub==2.5.0 flask==2.0.1 diff --git a/background/function/requirements.txt b/background/function/requirements.txt index 80606f77..f983e87a 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ google-cloud-translate==3.2.0 -google-cloud-firestore==2.1.1 +google-cloud-firestore==2.1.2 diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index df29f64f..2c33dee1 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,5 +1,5 @@ Flask==2.0.1 -google-cloud-firestore==2.1.1 +google-cloud-firestore==2.1.2 google-cloud-storage==1.38.0 google-cloud-logging==2.3.1 #DO NOT UPGRADE: google-cloud-error-reporting 1.1.2 requires google-cloud-logging<2.4,>=1.14.0 google-cloud-error-reporting==1.1.2 diff --git a/sessions/requirements.txt b/sessions/requirements.txt index 4377e11c..ca9979bb 100644 --- a/sessions/requirements.txt +++ b/sessions/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-firestore==2.1.1 +google-cloud-firestore==2.1.2 flask==2.0.1 From 888c70e939e0e9b4911c0b3c960ee63de2363593 Mon Sep 17 00:00:00 2001 From: Mark Brockett Robson <47262668+markbrockettrobson@users.noreply.github.com> Date: Fri, 18 Jun 2021 18:50:33 +1000 Subject: [PATCH 227/395] chore: add common directories to dockerignore Co-authored-by: Dan Lee <71398022+dandhlee@users.noreply.github.com> --- optional-kubernetes-engine/.dockerignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/optional-kubernetes-engine/.dockerignore b/optional-kubernetes-engine/.dockerignore index fdc976d4..d130cf3b 100644 --- a/optional-kubernetes-engine/.dockerignore +++ b/optional-kubernetes-engine/.dockerignore @@ -15,3 +15,6 @@ coverage.xml *,cover *.log .git +.mypy_cache +.pytest_cache +.hypothesis From d1fbadb80a7dcb9285eb8e09346d18526b51ca94 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Mon, 21 Jun 2021 17:51:49 +0200 Subject: [PATCH 228/395] chore(deps): update dependency google-cloud-pubsub to v2.6.0 (#366) --- background/app/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 595f0f83..3a3c0ea7 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ google-cloud-firestore==2.1.2 -google-cloud-pubsub==2.5.0 +google-cloud-pubsub==2.6.0 flask==2.0.1 From d990693e99290b39d4813a75d85ea771c123e02f Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Mon, 21 Jun 2021 17:58:15 +0200 Subject: [PATCH 229/395] chore(deps): update dependency google-cloud-firestore to v2.1.3 (#364) --- background/app/requirements.txt | 2 +- background/function/requirements.txt | 2 +- bookshelf/requirements.txt | 2 +- sessions/requirements.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 3a3c0ea7..6d59a055 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ -google-cloud-firestore==2.1.2 +google-cloud-firestore==2.1.3 google-cloud-pubsub==2.6.0 flask==2.0.1 diff --git a/background/function/requirements.txt b/background/function/requirements.txt index f983e87a..0067c26d 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ google-cloud-translate==3.2.0 -google-cloud-firestore==2.1.2 +google-cloud-firestore==2.1.3 diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 2c33dee1..7e918c04 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,5 +1,5 @@ Flask==2.0.1 -google-cloud-firestore==2.1.2 +google-cloud-firestore==2.1.3 google-cloud-storage==1.38.0 google-cloud-logging==2.3.1 #DO NOT UPGRADE: google-cloud-error-reporting 1.1.2 requires google-cloud-logging<2.4,>=1.14.0 google-cloud-error-reporting==1.1.2 diff --git a/sessions/requirements.txt b/sessions/requirements.txt index ca9979bb..454d6915 100644 --- a/sessions/requirements.txt +++ b/sessions/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-firestore==2.1.2 +google-cloud-firestore==2.1.3 flask==2.0.1 From aee9d3aeb16b4655583d34585a4da0583d44c609 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 22 Jun 2021 06:41:07 +0200 Subject: [PATCH 230/395] chore(deps): update dependency google-cloud-translate to v3.2.1 (#367) --- background/function/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/function/requirements.txt b/background/function/requirements.txt index 0067c26d..a4ac2c95 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-translate==3.2.0 +google-cloud-translate==3.2.1 google-cloud-firestore==2.1.3 From d60aa4ac1c2942e75c528b2e06a2477d28143dd7 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 22 Jun 2021 17:37:18 +0200 Subject: [PATCH 231/395] chore(deps): update dependency google-cloud-storage to v1.39.0 (#368) --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 7e918c04..a8f8df1f 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,6 +1,6 @@ Flask==2.0.1 google-cloud-firestore==2.1.3 -google-cloud-storage==1.38.0 +google-cloud-storage==1.39.0 google-cloud-logging==2.3.1 #DO NOT UPGRADE: google-cloud-error-reporting 1.1.2 requires google-cloud-logging<2.4,>=1.14.0 google-cloud-error-reporting==1.1.2 gunicorn==20.1.0 From 969322ca8687c8a42e6fe4b94cabb465ab67b007 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Thu, 1 Jul 2021 04:58:51 +0200 Subject: [PATCH 232/395] chore(deps): update dependency google-cloud-storage to v1.40.0 (#369) --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index a8f8df1f..2de48241 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,6 +1,6 @@ Flask==2.0.1 google-cloud-firestore==2.1.3 -google-cloud-storage==1.39.0 +google-cloud-storage==1.40.0 google-cloud-logging==2.3.1 #DO NOT UPGRADE: google-cloud-error-reporting 1.1.2 requires google-cloud-logging<2.4,>=1.14.0 google-cloud-error-reporting==1.1.2 gunicorn==20.1.0 From 4bb51e65bb6d63c002ca290e069e66f3250f7d92 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Mon, 12 Jul 2021 18:13:04 +0200 Subject: [PATCH 233/395] chore(deps): update dependency google-cloud-pubsub to v2.6.1 (#370) --- background/app/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 6d59a055..4038aa08 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ google-cloud-firestore==2.1.3 -google-cloud-pubsub==2.6.0 +google-cloud-pubsub==2.6.1 flask==2.0.1 From ba783790937a27b3bcb224897909545255c54d04 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 13 Jul 2021 15:54:22 +0200 Subject: [PATCH 234/395] chore(deps): update dependency google-cloud-error-reporting to v1.2.0 (#371) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![WhiteSource Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [google-cloud-error-reporting](https://togithub.com/googleapis/python-error-reporting) | `==1.1.2` -> `==1.2.0` | [![age](https://badges.renovateapi.com/packages/pypi/google-cloud-error-reporting/1.2.0/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/pypi/google-cloud-error-reporting/1.2.0/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/pypi/google-cloud-error-reporting/1.2.0/compatibility-slim/1.1.2)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/pypi/google-cloud-error-reporting/1.2.0/confidence-slim/1.1.2)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
googleapis/python-error-reporting ### [`v1.2.0`](https://togithub.com/googleapis/python-error-reporting/blob/master/CHANGELOG.md#​120-httpswwwgithubcomgoogleapispython-error-reportingcomparev112v120-2021-07-01) [Compare Source](https://togithub.com/googleapis/python-error-reporting/compare/v1.1.2...v1.2.0) ##### Features - add always_use_jwt_access ([#​140](https://www.github.com/googleapis/python-error-reporting/issues/140)) ([2f8f790](https://www.github.com/googleapis/python-error-reporting/commit/2f8f790dd70646848afd5e5209a147b01c6f388b)) ##### Bug Fixes - **deps:** add packaging requirement ([#​124](https://www.github.com/googleapis/python-error-reporting/issues/124)) ([ed36389](https://www.github.com/googleapis/python-error-reporting/commit/ed3638946f8829c4d2b379785e38cc2a2228c688)) - **deps:** widen version range on google-cloud-logging ([#​133](https://www.github.com/googleapis/python-error-reporting/issues/133)) ([c5e87bb](https://www.github.com/googleapis/python-error-reporting/commit/c5e87bb8c177688833ca9524daa4e6229875627d)) - disable always_use_jwt_access ([8aff323](https://www.github.com/googleapis/python-error-reporting/commit/8aff32330ebf544cc043bec0003140d61df7e7a1)) - disable always_use_jwt_access ([#​144](https://www.github.com/googleapis/python-error-reporting/issues/144)) ([8aff323](https://www.github.com/googleapis/python-error-reporting/commit/8aff32330ebf544cc043bec0003140d61df7e7a1)) ##### Documentation - omit mention of Python 2.7 in 'CONTRIBUTING.rst' ([#​1127](https://www.github.com/googleapis/python-error-reporting/issues/1127)) ([#​137](https://www.github.com/googleapis/python-error-reporting/issues/137)) ([0ca7d43](https://www.github.com/googleapis/python-error-reporting/commit/0ca7d4385de86e7ff462322807a717345774c017)) ##### [1.1.2](https://www.github.com/googleapis/python-error-reporting/compare/v1.1.1...v1.1.2) (2021-04-05) ##### Dependencies - upgrade sphinx ([#​99](https://www.github.com/googleapis/python-error-reporting/issues/99)) ([a118123](https://www.github.com/googleapis/python-error-reporting/commit/a118123cbfe8b5dd2a7ba260631b248c351cb116)) ##### [1.1.1](https://www.github.com/googleapis/python-error-reporting/compare/v1.1.0...v1.1.1) (2021-02-25) ##### Bug Fixes - remove gRPC send/recv limit; add enums to `types/__init__.py` ([#​57](https://www.github.com/googleapis/python-error-reporting/issues/57)) ([e23e5ba](https://www.github.com/googleapis/python-error-reporting/commit/e23e5ba7eba4482735d5f4897210a1ebb81bfb8a))
--- ### Configuration 📅 **Schedule**: At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box. --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#github/GoogleCloudPlatform/getting-started-python). --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 2de48241..be2cb53c 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -2,6 +2,6 @@ Flask==2.0.1 google-cloud-firestore==2.1.3 google-cloud-storage==1.40.0 google-cloud-logging==2.3.1 #DO NOT UPGRADE: google-cloud-error-reporting 1.1.2 requires google-cloud-logging<2.4,>=1.14.0 -google-cloud-error-reporting==1.1.2 +google-cloud-error-reporting==1.2.0 gunicorn==20.1.0 six==1.16.0 From 9990a6707533a21645eff2c57ee00f57db46fe16 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 14 Jul 2021 22:25:37 +0200 Subject: [PATCH 235/395] chore(deps): update dependency requests to v2.26.0 (#372) --- authenticating-users/requirements.txt | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index 5654d0b9..362eaf57 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -2,5 +2,5 @@ Flask==2.0.1 cryptography==3.4.7 python-jose[cryptography]==3.3.0 -requests==2.25.1 +requests==2.26.0 # [END getting_started_requirements] diff --git a/requirements.txt b/requirements.txt index b667a1f4..73c2d852 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ flake8==3.9.2 nox==2021.6.12 pytest==6.2.4 -requests==2.25.1 +requests==2.26.0 From 5999a574250859035d3bdd8e020b45628960a27f Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 14 Jul 2021 22:30:24 +0200 Subject: [PATCH 236/395] chore(deps): update dependency google-cloud-storage to v1.41.0 (#373) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![WhiteSource Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [google-cloud-storage](https://togithub.com/googleapis/python-storage) | `==1.40.0` -> `==1.41.0` | [![age](https://badges.renovateapi.com/packages/pypi/google-cloud-storage/1.41.0/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/pypi/google-cloud-storage/1.41.0/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/pypi/google-cloud-storage/1.41.0/compatibility-slim/1.40.0)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/pypi/google-cloud-storage/1.41.0/confidence-slim/1.40.0)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
googleapis/python-storage ### [`v1.41.0`](https://togithub.com/googleapis/python-storage/blob/master/CHANGELOG.md#​1410-httpswwwgithubcomgoogleapispython-storagecomparev1400v1410-2021-07-13) [Compare Source](https://togithub.com/googleapis/python-storage/compare/v1.40.0...v1.41.0) ##### Features - add support for Etag headers on reads ([#​489](https://www.github.com/googleapis/python-storage/issues/489)) ([741d3fd](https://www.github.com/googleapis/python-storage/commit/741d3fda4e4280022cede29ebeb7c2ea09e73b6f)) ##### Bug Fixes - **deps:** update minimum dependency versions to pick up bugfixes ([#​496](https://www.github.com/googleapis/python-storage/issues/496)) ([92251a5](https://www.github.com/googleapis/python-storage/commit/92251a5c8ea4d663773506eb1c630201a657aa69)), closes [#​494](https://www.github.com/googleapis/python-storage/issues/494) - populate etag / generation / metageneration properties during download ([#​488](https://www.github.com/googleapis/python-storage/issues/488)) ([49ba14c](https://www.github.com/googleapis/python-storage/commit/49ba14c9c47dbe6bc2bb45d53bbe5621c131fbcb)) - revise and rename is_etag_in_json(data) ([#​483](https://www.github.com/googleapis/python-storage/issues/483)) ([0a52546](https://www.github.com/googleapis/python-storage/commit/0a5254647bf1155874fe48f3891bcc34a76b0b81))
--- ### Configuration 📅 **Schedule**: At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box. --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#github/GoogleCloudPlatform/getting-started-python). --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index be2cb53c..ec51e517 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,6 +1,6 @@ Flask==2.0.1 google-cloud-firestore==2.1.3 -google-cloud-storage==1.40.0 +google-cloud-storage==1.41.0 google-cloud-logging==2.3.1 #DO NOT UPGRADE: google-cloud-error-reporting 1.1.2 requires google-cloud-logging<2.4,>=1.14.0 google-cloud-error-reporting==1.2.0 gunicorn==20.1.0 From b62f48b8dba0067f53260aa037bba2f6ce2cc577 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 21 Jul 2021 15:27:43 +0200 Subject: [PATCH 237/395] chore(deps): update dependency google-cloud-storage to v1.41.1 (#374) --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index ec51e517..4337a338 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,6 +1,6 @@ Flask==2.0.1 google-cloud-firestore==2.1.3 -google-cloud-storage==1.41.0 +google-cloud-storage==1.41.1 google-cloud-logging==2.3.1 #DO NOT UPGRADE: google-cloud-error-reporting 1.1.2 requires google-cloud-logging<2.4,>=1.14.0 google-cloud-error-reporting==1.2.0 gunicorn==20.1.0 From 43562f4110d3959289998167101cc38161d4e405 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Thu, 22 Jul 2021 02:28:31 +0200 Subject: [PATCH 238/395] chore(deps): update dependency google-cloud-translate to v3.3.1 (#375) --- background/function/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/function/requirements.txt b/background/function/requirements.txt index a4ac2c95..f1e834cb 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-translate==3.2.1 +google-cloud-translate==3.3.1 google-cloud-firestore==2.1.3 From 232aa75d4fdcb7dbe96036ec2a305c5412423d4e Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Thu, 22 Jul 2021 16:00:12 +0200 Subject: [PATCH 239/395] chore(deps): update dependency google-cloud-error-reporting to v1.2.1 (#376) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![WhiteSource Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [google-cloud-error-reporting](https://togithub.com/googleapis/python-error-reporting) | `==1.2.0` -> `==1.2.1` | [![age](https://badges.renovateapi.com/packages/pypi/google-cloud-error-reporting/1.2.1/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/pypi/google-cloud-error-reporting/1.2.1/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/pypi/google-cloud-error-reporting/1.2.1/compatibility-slim/1.2.0)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/pypi/google-cloud-error-reporting/1.2.1/confidence-slim/1.2.0)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
googleapis/python-error-reporting ### [`v1.2.1`](https://togithub.com/googleapis/python-error-reporting/blob/master/CHANGELOG.md#​121-httpswwwgithubcomgoogleapispython-error-reportingcomparev120v121-2021-07-20) [Compare Source](https://togithub.com/googleapis/python-error-reporting/compare/v1.2.0...v1.2.1)
--- ### Configuration 📅 **Schedule**: At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box. --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#github/GoogleCloudPlatform/getting-started-python). --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 4337a338..94ad6ecc 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -2,6 +2,6 @@ Flask==2.0.1 google-cloud-firestore==2.1.3 google-cloud-storage==1.41.1 google-cloud-logging==2.3.1 #DO NOT UPGRADE: google-cloud-error-reporting 1.1.2 requires google-cloud-logging<2.4,>=1.14.0 -google-cloud-error-reporting==1.2.0 +google-cloud-error-reporting==1.2.1 gunicorn==20.1.0 six==1.16.0 From f0c8c9ff316ba4586738760ab6ae25a7fe8f9300 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Thu, 22 Jul 2021 19:54:04 +0200 Subject: [PATCH 240/395] chore(deps): update dependency google-cloud-firestore to v2.2.0 (#377) --- background/app/requirements.txt | 2 +- background/function/requirements.txt | 2 +- bookshelf/requirements.txt | 2 +- sessions/requirements.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 4038aa08..946b0417 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ -google-cloud-firestore==2.1.3 +google-cloud-firestore==2.2.0 google-cloud-pubsub==2.6.1 flask==2.0.1 diff --git a/background/function/requirements.txt b/background/function/requirements.txt index f1e834cb..2df6a63b 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ google-cloud-translate==3.3.1 -google-cloud-firestore==2.1.3 +google-cloud-firestore==2.2.0 diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 94ad6ecc..17b977ac 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,5 +1,5 @@ Flask==2.0.1 -google-cloud-firestore==2.1.3 +google-cloud-firestore==2.2.0 google-cloud-storage==1.41.1 google-cloud-logging==2.3.1 #DO NOT UPGRADE: google-cloud-error-reporting 1.1.2 requires google-cloud-logging<2.4,>=1.14.0 google-cloud-error-reporting==1.2.1 diff --git a/sessions/requirements.txt b/sessions/requirements.txt index 454d6915..66c1c1e2 100644 --- a/sessions/requirements.txt +++ b/sessions/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-firestore==2.1.3 +google-cloud-firestore==2.2.0 flask==2.0.1 From cc2f2349856d5cc2788b801ca89f17483022ff5c Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 28 Jul 2021 18:05:19 +0200 Subject: [PATCH 241/395] chore(deps): update dependency google-cloud-pubsub to v2.7.0 (#379) --- background/app/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 946b0417..1bba79d6 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ google-cloud-firestore==2.2.0 -google-cloud-pubsub==2.6.1 +google-cloud-pubsub==2.7.0 flask==2.0.1 From 340cf1c898323df938eb3a587ee4dc2ff4c6a11d Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 28 Jul 2021 18:12:18 +0200 Subject: [PATCH 242/395] chore(deps): update dependency google-cloud-error-reporting to v1.2.2 (#378) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![WhiteSource Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [google-cloud-error-reporting](https://togithub.com/googleapis/python-error-reporting) | `==1.2.1` -> `==1.2.2` | [![age](https://badges.renovateapi.com/packages/pypi/google-cloud-error-reporting/1.2.2/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/pypi/google-cloud-error-reporting/1.2.2/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/pypi/google-cloud-error-reporting/1.2.2/compatibility-slim/1.2.1)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/pypi/google-cloud-error-reporting/1.2.2/confidence-slim/1.2.1)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
googleapis/python-error-reporting ### [`v1.2.2`](https://togithub.com/googleapis/python-error-reporting/blob/master/CHANGELOG.md#​122-httpswwwgithubcomgoogleapispython-error-reportingcomparev121v122-2021-07-28) [Compare Source](https://togithub.com/googleapis/python-error-reporting/compare/v1.2.1...v1.2.2)
--- ### Configuration 📅 **Schedule**: At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box. --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#github/GoogleCloudPlatform/getting-started-python). --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 17b977ac..dcb07f4f 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -2,6 +2,6 @@ Flask==2.0.1 google-cloud-firestore==2.2.0 google-cloud-storage==1.41.1 google-cloud-logging==2.3.1 #DO NOT UPGRADE: google-cloud-error-reporting 1.1.2 requires google-cloud-logging<2.4,>=1.14.0 -google-cloud-error-reporting==1.2.1 +google-cloud-error-reporting==1.2.2 gunicorn==20.1.0 six==1.16.0 From 5935754567e635975134f3dcc020ceae8b50baf5 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 28 Jul 2021 23:12:18 +0200 Subject: [PATCH 243/395] chore(deps): update dependency google-cloud-logging to v2.6.0 (#380) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![WhiteSource Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [google-cloud-logging](https://togithub.com/googleapis/python-logging) | `==2.3.1` -> `==2.6.0` | [![age](https://badges.renovateapi.com/packages/pypi/google-cloud-logging/2.6.0/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/pypi/google-cloud-logging/2.6.0/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/pypi/google-cloud-logging/2.6.0/compatibility-slim/2.3.1)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/pypi/google-cloud-logging/2.6.0/confidence-slim/2.3.1)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
googleapis/python-logging ### [`v2.6.0`](https://togithub.com/googleapis/python-logging/blob/master/CHANGELOG.md#​260-httpswwwgithubcomgoogleapispython-loggingcomparev250v260-2021-07-28) [Compare Source](https://togithub.com/googleapis/python-logging/compare/v2.5.0...v2.6.0) ##### Features - add always_use_jwt_access ([#​334](https://www.github.com/googleapis/python-logging/issues/334)) ([ae67d10](https://www.github.com/googleapis/python-logging/commit/ae67d10a661a3561b366bb05f5cf6d34520164b4)) ##### Bug Fixes - **deps:** pin 'google-{api,cloud}-core', 'google-auth' to allow 2.x versions ([#​356](https://www.github.com/googleapis/python-logging/issues/356)) ([a970dd2](https://www.github.com/googleapis/python-logging/commit/a970dd293d4fddc983946cb1c362f487a82d9609)) - disable always_use_jwt_access ([#​342](https://www.github.com/googleapis/python-logging/issues/342)) ([a95e401](https://www.github.com/googleapis/python-logging/commit/a95e40188c9483310fb1dce9242c7c66721a6b7f)) - enable self signed jwt for grpc ([#​360](https://www.github.com/googleapis/python-logging/issues/360)) ([707fad1](https://www.github.com/googleapis/python-logging/commit/707fad1a714d951727336b03f4444f53199737e3)) ##### Documentation - omit mention of Python 2.7 in 'CONTRIBUTING.rst' ([#​1127](https://www.github.com/googleapis/python-logging/issues/1127)) ([#​327](https://www.github.com/googleapis/python-logging/issues/327)) ([faa6fb9](https://www.github.com/googleapis/python-logging/commit/faa6fb9a86c82b99b529e979160bfdd7a505793b)), closes [#​1126](https://www.github.com/googleapis/python-logging/issues/1126) - add Samples section to CONTRIBUTING.rst ([#​357](https://www.github.com/googleapis/python-logging/issues/357)) ([8771716](https://www.github.com/googleapis/python-logging/commit/8771716cae7cf67d710b3741b8b718fc4a8aa2b6)) ### [`v2.5.0`](https://togithub.com/googleapis/python-logging/blob/master/CHANGELOG.md#​250-httpswwwgithubcomgoogleapispython-loggingcomparev240v250-2021-06-10) [Compare Source](https://togithub.com/googleapis/python-logging/compare/v2.4.0...v2.5.0) ##### Features - support AuditLog and RequestLog protos ([#​274](https://www.github.com/googleapis/python-logging/issues/274)) ([5d91be9](https://www.github.com/googleapis/python-logging/commit/5d91be9f121c364cbd53c6a9fffc4fb6ca6bd324)) ##### Bug Fixes - **deps:** add packaging requirement ([#​300](https://www.github.com/googleapis/python-logging/issues/300)) ([68c5cec](https://www.github.com/googleapis/python-logging/commit/68c5ceced3288253af8e3c6013a35fa3954b37bc)) - structured log handler formatting issues ([#​319](https://www.github.com/googleapis/python-logging/issues/319)) ([db9da37](https://www.github.com/googleapis/python-logging/commit/db9da3700511b5a24c3c44c9f4377705937caf46)) ### [`v2.4.0`](https://togithub.com/googleapis/python-logging/blob/master/CHANGELOG.md#​240-httpswwwgithubcomgoogleapispython-loggingcomparev231v240-2021-05-12) [Compare Source](https://togithub.com/googleapis/python-logging/compare/v2.3.1...v2.4.0) ##### Features - allow custom labels with standard library logging ([#​264](https://www.github.com/googleapis/python-logging/issues/264)) ([fe4de39](https://www.github.com/googleapis/python-logging/commit/fe4de39a87581a9e9f2cee62462ae2f26176194f)) - Improve source location overrides ([#​258](https://www.github.com/googleapis/python-logging/issues/258)) ([6b10b74](https://www.github.com/googleapis/python-logging/commit/6b10b74e2bf65ea406b10585a4c24078348483d2)) - record source locations ([#​254](https://www.github.com/googleapis/python-logging/issues/254)) ([a5c2f8e](https://www.github.com/googleapis/python-logging/commit/a5c2f8e948bb116cbce313f063643aec02d06a84)) - support span inference ([#​267](https://www.github.com/googleapis/python-logging/issues/267)) ([fcd26eb](https://www.github.com/googleapis/python-logging/commit/fcd26eb0ff4f97c097ca33b2d212d8f83e56686e)) - use standard output logs on serverless environments ([#​228](https://www.github.com/googleapis/python-logging/issues/228)) ([a78f577](https://www.github.com/googleapis/python-logging/commit/a78f577bda17d758551237be84182035ed7b9cce)) ##### Bug Fixes - changed region format on serverless ([#​291](https://www.github.com/googleapis/python-logging/issues/291)) ([8872d6f](https://www.github.com/googleapis/python-logging/commit/8872d6f6b2bb979adffad0b054fa40306b68cfc0)) - changed region format on serverless ([#​291](https://www.github.com/googleapis/python-logging/issues/291)) ([360d3d2](https://www.github.com/googleapis/python-logging/commit/360d3d23db7709b7c3946c092ef373f888f47c3d)) - **deps:** fix minimum required version of google-api-core ([#​244](https://www.github.com/googleapis/python-logging/issues/244)) ([874fdfa](https://www.github.com/googleapis/python-logging/commit/874fdfa809063c2bfb33e59aded553e098601876)) - **deps:** fix minimum required version of google-api-core ([#​244](https://www.github.com/googleapis/python-logging/issues/244)) ([37d33fc](https://www.github.com/googleapis/python-logging/commit/37d33fcd8402b973377486a572c04ba6d4029b58)) - improve API compatibility for next release ([#​292](https://www.github.com/googleapis/python-logging/issues/292)) ([1f9517d](https://www.github.com/googleapis/python-logging/commit/1f9517da7302e19198e598d452df58238d4e6306)) - remove noisy logs ([#​290](https://www.github.com/googleapis/python-logging/issues/290)) ([bdf8273](https://www.github.com/googleapis/python-logging/commit/bdf827358de5935f736ecd73ab10b2d861daf690)) ##### [2.3.1](https://www.github.com/googleapis/python-logging/compare/v2.3.0...v2.3.1) (2021-03-24) ##### Bug Fixes - detect project from environment instead of from logger ([#​238](https://www.github.com/googleapis/python-logging/issues/238)) ([813b97c](https://www.github.com/googleapis/python-logging/commit/813b97cb936fa5acc2a4de567e2c84d746527e98)) - revert default resource behavior to avoid breaking changes ([#​237](https://www.github.com/googleapis/python-logging/issues/237)) ([24a0a5e](https://www.github.com/googleapis/python-logging/commit/24a0a5e674430e97a3a2e3b54477d8f95fa08ec6))
--- ### Configuration 📅 **Schedule**: At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box. --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#github/GoogleCloudPlatform/getting-started-python). --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index dcb07f4f..6b13ddd9 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,7 +1,7 @@ Flask==2.0.1 google-cloud-firestore==2.2.0 google-cloud-storage==1.41.1 -google-cloud-logging==2.3.1 #DO NOT UPGRADE: google-cloud-error-reporting 1.1.2 requires google-cloud-logging<2.4,>=1.14.0 +google-cloud-logging==2.6.0 google-cloud-error-reporting==1.2.2 gunicorn==20.1.0 six==1.16.0 From 81c94a2ecba6e026649ee9574cd8e8c214252566 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Thu, 29 Jul 2021 19:58:46 +0200 Subject: [PATCH 244/395] chore(deps): update dependency google-cloud-translate to v3.3.2 (#381) --- background/function/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/function/requirements.txt b/background/function/requirements.txt index 2df6a63b..07a57425 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-translate==3.3.1 +google-cloud-translate==3.3.2 google-cloud-firestore==2.2.0 From cdc5b00a60b168cb514d888b3bdbb09531917439 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 11 Aug 2021 21:48:39 +0200 Subject: [PATCH 245/395] chore(deps): update dependency google-cloud-storage to v1.42.0 (#384) --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 6b13ddd9..8e61f7f8 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,6 +1,6 @@ Flask==2.0.1 google-cloud-firestore==2.2.0 -google-cloud-storage==1.41.1 +google-cloud-storage==1.42.0 google-cloud-logging==2.6.0 google-cloud-error-reporting==1.2.2 gunicorn==20.1.0 From 2a46f1bb973a535d05e4645f34497d4849bc3181 Mon Sep 17 00:00:00 2001 From: Austin Pray <71290498+austinpray-mixpanel@users.noreply.github.com> Date: Wed, 11 Aug 2021 14:56:10 -0500 Subject: [PATCH 246/395] dockerignore: *,cover -> *.cover (#383) Seemed like a typo --- optional-kubernetes-engine/.dockerignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/optional-kubernetes-engine/.dockerignore b/optional-kubernetes-engine/.dockerignore index d130cf3b..c4730648 100644 --- a/optional-kubernetes-engine/.dockerignore +++ b/optional-kubernetes-engine/.dockerignore @@ -12,7 +12,7 @@ pip-delete-this-directory.txt .cache nosetests.xml coverage.xml -*,cover +*.cover *.log .git .mypy_cache From 7e1900779312415c538d643e0eed9de42f920fac Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Mon, 23 Aug 2021 22:45:31 +0200 Subject: [PATCH 247/395] chore(deps): update dependency google-cloud-pubsub to v2.7.1 (#385) --- background/app/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 1bba79d6..01abeebf 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ google-cloud-firestore==2.2.0 -google-cloud-pubsub==2.7.0 +google-cloud-pubsub==2.7.1 flask==2.0.1 From def8160614334d2b1d787166912302524cdd4e2c Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Mon, 23 Aug 2021 22:52:08 +0200 Subject: [PATCH 248/395] chore(deps): update dependency google-cloud-firestore to v2.3.0 (#386) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![WhiteSource Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [google-cloud-firestore](https://togithub.com/googleapis/python-firestore) | `==2.2.0` -> `==2.3.0` | [![age](https://badges.renovateapi.com/packages/pypi/google-cloud-firestore/2.3.0/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/pypi/google-cloud-firestore/2.3.0/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/pypi/google-cloud-firestore/2.3.0/compatibility-slim/2.2.0)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/pypi/google-cloud-firestore/2.3.0/confidence-slim/2.2.0)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
googleapis/python-firestore ### [`v2.3.0`](https://togithub.com/googleapis/python-firestore/blob/master/CHANGELOG.md#​230-httpswwwgithubcomgoogleapispython-firestorecomparev220v230-2021-08-18) [Compare Source](https://togithub.com/googleapis/python-firestore/compare/v2.2.0...v2.3.0) ##### Features - add bulk writer ([#​396](https://www.togithub.com/googleapis/python-firestore/issues/396)) ([98a7753](https://www.github.com/googleapis/python-firestore/commit/98a7753f05240a2a75b9ffd42b7a148c65a6e87f)) - add recursive delete ([#​420](https://www.togithub.com/googleapis/python-firestore/issues/420)) ([813a57b](https://www.github.com/googleapis/python-firestore/commit/813a57b1070a1f6ac41d02897fab33f8039b83f9)) - add support for recursive queries ([#​407](https://www.togithub.com/googleapis/python-firestore/issues/407)) ([eb45a36](https://www.github.com/googleapis/python-firestore/commit/eb45a36e6c06b642106e061a32bfc119eb7e5bf0)) ##### Bug Fixes - enable self signed jwt for grpc ([#​405](https://www.togithub.com/googleapis/python-firestore/issues/405)) ([8703b48](https://www.github.com/googleapis/python-firestore/commit/8703b48c45e7bb742a794cad9597740c44182f81)) - use insecure grpc channels with emulator ([#​402](https://www.togithub.com/googleapis/python-firestore/issues/402)) ([4381ad5](https://www.github.com/googleapis/python-firestore/commit/4381ad503ca3e83510b876281fc768d00d40d499)) - remove unused requirement pytz ([#​422](https://www.togithub.com/googleapis/python-firestore/issues/422)) ([539c1d7](https://www.github.com/googleapis/python-firestore/commit/539c1d719191eb0ae3a49290c26b628de7c27cd5)) ##### Documentation - added generated docs for Bundles ([#​416](https://www.togithub.com/googleapis/python-firestore/issues/416)) ([0176cc7](https://www.github.com/googleapis/python-firestore/commit/0176cc7fef8752433b5c2496046d3a56557eb824)) - fixed broken links to devsite ([#​417](https://www.togithub.com/googleapis/python-firestore/issues/417)) ([1adfc81](https://www.github.com/googleapis/python-firestore/commit/1adfc81237c4ddee665e81f1beaef808cddb860e))
--- ### Configuration 📅 **Schedule**: At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box. --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#github/GoogleCloudPlatform/getting-started-python). --- background/app/requirements.txt | 2 +- background/function/requirements.txt | 2 +- bookshelf/requirements.txt | 2 +- sessions/requirements.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 01abeebf..676ff9fb 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ -google-cloud-firestore==2.2.0 +google-cloud-firestore==2.3.0 google-cloud-pubsub==2.7.1 flask==2.0.1 diff --git a/background/function/requirements.txt b/background/function/requirements.txt index 07a57425..2cc65bee 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ google-cloud-translate==3.3.2 -google-cloud-firestore==2.2.0 +google-cloud-firestore==2.3.0 diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 8e61f7f8..66f5db8a 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,5 +1,5 @@ Flask==2.0.1 -google-cloud-firestore==2.2.0 +google-cloud-firestore==2.3.0 google-cloud-storage==1.42.0 google-cloud-logging==2.6.0 google-cloud-error-reporting==1.2.2 diff --git a/sessions/requirements.txt b/sessions/requirements.txt index 66c1c1e2..6fdd3bdb 100644 --- a/sessions/requirements.txt +++ b/sessions/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-firestore==2.2.0 +google-cloud-firestore==2.3.0 flask==2.0.1 From 8a6cd8630de695c1be7a6c56edce35ac06625c00 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Mon, 30 Aug 2021 20:34:12 +0200 Subject: [PATCH 249/395] chore(deps): update dependency cryptography to v3.4.8 (#387) --- authenticating-users/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index 362eaf57..5634ea1b 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -1,6 +1,6 @@ # [START getting_started_requirements] Flask==2.0.1 -cryptography==3.4.7 +cryptography==3.4.8 python-jose[cryptography]==3.3.0 requests==2.26.0 # [END getting_started_requirements] From cce8ab96553adf3ac1fc09150caf86e8e01d93df Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Mon, 30 Aug 2021 20:40:11 +0200 Subject: [PATCH 250/395] chore(deps): update dependency pytest to v6.2.5 (#389) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![WhiteSource Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [pytest](https://docs.pytest.org/en/latest/) ([source](https://togithub.com/pytest-dev/pytest), [changelog](https://docs.pytest.org/en/stable/changelog.html)) | `==6.2.4` -> `==6.2.5` | [![age](https://badges.renovateapi.com/packages/pypi/pytest/6.2.5/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/pypi/pytest/6.2.5/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/pypi/pytest/6.2.5/compatibility-slim/6.2.4)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/pypi/pytest/6.2.5/confidence-slim/6.2.4)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
pytest-dev/pytest ### [`v6.2.5`](https://togithub.com/pytest-dev/pytest/releases/6.2.5) [Compare Source](https://togithub.com/pytest-dev/pytest/compare/6.2.4...6.2.5) # pytest 6.2.5 (2021-08-29) ## Trivial/Internal Changes - [#​8494](https://togithub.com/pytest-dev/pytest/issues/8494): Python 3.10 is now supported. - [#​9040](https://togithub.com/pytest-dev/pytest/issues/9040): Enable compatibility with `pluggy 1.0` or later.
--- ### Configuration 📅 **Schedule**: At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box. --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#github/GoogleCloudPlatform/getting-started-python). --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 73c2d852..69dfa485 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ flake8==3.9.2 nox==2021.6.12 -pytest==6.2.4 +pytest==6.2.5 requests==2.26.0 From 65bec63a2b8459f34aacfc0ca593fc8bb509b136 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 1 Sep 2021 11:35:20 +0200 Subject: [PATCH 251/395] chore(deps): update dependency google-cloud-translate to v3.4.0 (#391) --- background/function/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/function/requirements.txt b/background/function/requirements.txt index 2cc65bee..6f6fc2d6 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-translate==3.3.2 +google-cloud-translate==3.4.0 google-cloud-firestore==2.3.0 From 6b6d23e1ae0bff47cdad669df26fd5ca1fe2b46f Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 1 Sep 2021 22:53:14 +0200 Subject: [PATCH 252/395] chore(deps): update dependency google-cloud-firestore to v2.3.1 (#390) Co-authored-by: Anthonios Partheniou --- background/app/requirements.txt | 2 +- background/function/requirements.txt | 2 +- bookshelf/requirements.txt | 2 +- sessions/requirements.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 676ff9fb..5fa7ab7c 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ -google-cloud-firestore==2.3.0 +google-cloud-firestore==2.3.1 google-cloud-pubsub==2.7.1 flask==2.0.1 diff --git a/background/function/requirements.txt b/background/function/requirements.txt index 6f6fc2d6..1e23ebc8 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ google-cloud-translate==3.4.0 -google-cloud-firestore==2.3.0 +google-cloud-firestore==2.3.1 diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 66f5db8a..7fd6638c 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,5 +1,5 @@ Flask==2.0.1 -google-cloud-firestore==2.3.0 +google-cloud-firestore==2.3.1 google-cloud-storage==1.42.0 google-cloud-logging==2.6.0 google-cloud-error-reporting==1.2.2 diff --git a/sessions/requirements.txt b/sessions/requirements.txt index 6fdd3bdb..7e5a40da 100644 --- a/sessions/requirements.txt +++ b/sessions/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-firestore==2.3.0 +google-cloud-firestore==2.3.1 flask==2.0.1 From 661351514a08446f4c2432de64476bf93079157e Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 7 Sep 2021 17:39:26 +0200 Subject: [PATCH 253/395] chore(deps): update dependency google-cloud-pubsub to v2.8.0 (#393) --- background/app/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 5fa7ab7c..5708b808 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ google-cloud-firestore==2.3.1 -google-cloud-pubsub==2.7.1 +google-cloud-pubsub==2.8.0 flask==2.0.1 From 23d8a84948a6bec8f35229043dc25c32ddd8a0e5 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Thu, 9 Sep 2021 16:42:18 +0200 Subject: [PATCH 254/395] chore(deps): update dependency google-cloud-storage to v1.42.1 (#394) --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 7fd6638c..73f35edd 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,6 +1,6 @@ Flask==2.0.1 google-cloud-firestore==2.3.1 -google-cloud-storage==1.42.0 +google-cloud-storage==1.42.1 google-cloud-logging==2.6.0 google-cloud-error-reporting==1.2.2 gunicorn==20.1.0 From 93d1e0488a8b9721a09d1d5e87acb01b6af87584 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 15 Sep 2021 16:59:56 +0200 Subject: [PATCH 255/395] chore(deps): update dependency google-cloud-firestore to v2.3.2 (#395) --- background/app/requirements.txt | 2 +- background/function/requirements.txt | 2 +- bookshelf/requirements.txt | 2 +- sessions/requirements.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 5708b808..be797854 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ -google-cloud-firestore==2.3.1 +google-cloud-firestore==2.3.2 google-cloud-pubsub==2.8.0 flask==2.0.1 diff --git a/background/function/requirements.txt b/background/function/requirements.txt index 1e23ebc8..6581605d 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ google-cloud-translate==3.4.0 -google-cloud-firestore==2.3.1 +google-cloud-firestore==2.3.2 diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 73f35edd..c9c3cb7f 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,5 +1,5 @@ Flask==2.0.1 -google-cloud-firestore==2.3.1 +google-cloud-firestore==2.3.2 google-cloud-storage==1.42.1 google-cloud-logging==2.6.0 google-cloud-error-reporting==1.2.2 diff --git a/sessions/requirements.txt b/sessions/requirements.txt index 7e5a40da..8d103489 100644 --- a/sessions/requirements.txt +++ b/sessions/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-firestore==2.3.1 +google-cloud-firestore==2.3.2 flask==2.0.1 From 1cc2e993f916dbed84cb55508e4b26d4d8509525 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Thu, 16 Sep 2021 23:34:09 +0200 Subject: [PATCH 256/395] chore(deps): update dependency google-cloud-storage to v1.42.2 (#396) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![WhiteSource Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [google-cloud-storage](https://togithub.com/googleapis/python-storage) | `==1.42.1` -> `==1.42.2` | [![age](https://badges.renovateapi.com/packages/pypi/google-cloud-storage/1.42.2/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/pypi/google-cloud-storage/1.42.2/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/pypi/google-cloud-storage/1.42.2/compatibility-slim/1.42.1)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/pypi/google-cloud-storage/1.42.2/confidence-slim/1.42.1)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
googleapis/python-storage ### [`v1.42.2`](https://togithub.com/googleapis/python-storage/blob/master/CHANGELOG.md#​1422-httpswwwgithubcomgoogleapispython-storagecomparev1421v1422-2021-09-16) [Compare Source](https://togithub.com/googleapis/python-storage/compare/v1.42.1...v1.42.2)
--- ### Configuration 📅 **Schedule**: At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box. --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#github/GoogleCloudPlatform/getting-started-python). --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index c9c3cb7f..1a5a493e 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,6 +1,6 @@ Flask==2.0.1 google-cloud-firestore==2.3.2 -google-cloud-storage==1.42.1 +google-cloud-storage==1.42.2 google-cloud-logging==2.6.0 google-cloud-error-reporting==1.2.2 gunicorn==20.1.0 From 37820aa66174cd5f2c3ab40d5cc8ccb4083de104 Mon Sep 17 00:00:00 2001 From: Dan Lee <71398022+dandhlee@users.noreply.github.com> Date: Fri, 17 Sep 2021 11:26:44 -0400 Subject: [PATCH 257/395] chore: move Renovate config to .github/, turn off dashboard (#397) * chore: move Renovate config to .github/, turn off dashboard * chore: delete renovate from top directory --- .github/renovate.json | 10 ++++++++++ renovate.json | 8 -------- 2 files changed, 10 insertions(+), 8 deletions(-) create mode 100644 .github/renovate.json delete mode 100644 renovate.json diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 00000000..091cca06 --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,10 @@ +{ + "extends": [ + "config:base", + ":preserveSemverRanges", + ":disableDependencyDashboard" + ], + "ignorePaths": [ + "optional-kubernetes-engine" + ] +} diff --git a/renovate.json b/renovate.json deleted file mode 100644 index 07729f34..00000000 --- a/renovate.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": [ - "config:base" - ], - "ignorePaths": [ - "optional-kubernetes-engine" - ] -} From b76bce637e55aa3b0345dbe0f50667c854e87d98 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Mon, 27 Sep 2021 17:42:12 +0200 Subject: [PATCH 258/395] chore(deps): update dependency google-cloud-firestore to v2.3.3 (#398) --- background/app/requirements.txt | 2 +- background/function/requirements.txt | 2 +- bookshelf/requirements.txt | 2 +- sessions/requirements.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index be797854..a6ee7fe1 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ -google-cloud-firestore==2.3.2 +google-cloud-firestore==2.3.3 google-cloud-pubsub==2.8.0 flask==2.0.1 diff --git a/background/function/requirements.txt b/background/function/requirements.txt index 6581605d..83c8aced 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ google-cloud-translate==3.4.0 -google-cloud-firestore==2.3.2 +google-cloud-firestore==2.3.3 diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 1a5a493e..aa0bc164 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,5 +1,5 @@ Flask==2.0.1 -google-cloud-firestore==2.3.2 +google-cloud-firestore==2.3.3 google-cloud-storage==1.42.2 google-cloud-logging==2.6.0 google-cloud-error-reporting==1.2.2 diff --git a/sessions/requirements.txt b/sessions/requirements.txt index 8d103489..c22bc0ba 100644 --- a/sessions/requirements.txt +++ b/sessions/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-firestore==2.3.2 +google-cloud-firestore==2.3.3 flask==2.0.1 From 6c7029d1e5b2c3f5ba4536ee02cee88240222fdf Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 28 Sep 2021 16:43:21 +0200 Subject: [PATCH 259/395] chore(deps): update dependency google-cloud-error-reporting to v1.2.3 (#399) --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index aa0bc164..73e2903c 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -2,6 +2,6 @@ Flask==2.0.1 google-cloud-firestore==2.3.3 google-cloud-storage==1.42.2 google-cloud-logging==2.6.0 -google-cloud-error-reporting==1.2.2 +google-cloud-error-reporting==1.2.3 gunicorn==20.1.0 six==1.16.0 From d60f6ed29c3da96faf4d5cf85e1268a2cc879330 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Thu, 30 Sep 2021 22:27:42 +0200 Subject: [PATCH 260/395] chore(deps): update dependency google-cloud-translate to v3.4.1 (#402) --- background/function/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/function/requirements.txt b/background/function/requirements.txt index 83c8aced..e0b02213 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-translate==3.4.0 +google-cloud-translate==3.4.1 google-cloud-firestore==2.3.3 From 6fe5caabec3db0cba3dd02955386a300ec055fb8 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 1 Oct 2021 01:20:52 +0200 Subject: [PATCH 261/395] chore(deps): update dependency google-cloud-firestore to v2.3.4 (#401) --- background/app/requirements.txt | 2 +- background/function/requirements.txt | 2 +- bookshelf/requirements.txt | 2 +- sessions/requirements.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index a6ee7fe1..df881a59 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ -google-cloud-firestore==2.3.3 +google-cloud-firestore==2.3.4 google-cloud-pubsub==2.8.0 flask==2.0.1 diff --git a/background/function/requirements.txt b/background/function/requirements.txt index e0b02213..850a571e 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ google-cloud-translate==3.4.1 -google-cloud-firestore==2.3.3 +google-cloud-firestore==2.3.4 diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 73e2903c..4e9a99da 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,5 +1,5 @@ Flask==2.0.1 -google-cloud-firestore==2.3.3 +google-cloud-firestore==2.3.4 google-cloud-storage==1.42.2 google-cloud-logging==2.6.0 google-cloud-error-reporting==1.2.3 diff --git a/sessions/requirements.txt b/sessions/requirements.txt index c22bc0ba..55f852ba 100644 --- a/sessions/requirements.txt +++ b/sessions/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-firestore==2.3.3 +google-cloud-firestore==2.3.4 flask==2.0.1 From f04d491cce517d2516709d2bf84a2f9783729bb5 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 1 Oct 2021 16:47:25 +0200 Subject: [PATCH 262/395] chore(deps): update dependency nox to v2021.10.1 (#404) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 69dfa485..1f2cee7d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ flake8==3.9.2 -nox==2021.6.12 +nox==2021.10.1 pytest==6.2.5 requests==2.26.0 From e2a9e530230c1e75b0114db04fda5099eb905c7f Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 5 Oct 2021 07:58:07 +0200 Subject: [PATCH 263/395] chore(deps): update dependency flask to v2.0.2 (#405) --- authenticating-users/requirements.txt | 2 +- background/app/requirements.txt | 2 +- bookshelf/requirements.txt | 2 +- gce/requirements.txt | 2 +- sessions/requirements.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index 5634ea1b..7d206ff7 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -1,5 +1,5 @@ # [START getting_started_requirements] -Flask==2.0.1 +Flask==2.0.2 cryptography==3.4.8 python-jose[cryptography]==3.3.0 requests==2.26.0 diff --git a/background/app/requirements.txt b/background/app/requirements.txt index df881a59..cb31418f 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ google-cloud-firestore==2.3.4 google-cloud-pubsub==2.8.0 -flask==2.0.1 +flask==2.0.2 diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 4e9a99da..e7926a5f 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,4 +1,4 @@ -Flask==2.0.1 +Flask==2.0.2 google-cloud-firestore==2.3.4 google-cloud-storage==1.42.2 google-cloud-logging==2.6.0 diff --git a/gce/requirements.txt b/gce/requirements.txt index 90e463ee..d34ac776 100644 --- a/gce/requirements.txt +++ b/gce/requirements.txt @@ -1,3 +1,3 @@ -flask==2.0.1 +flask==2.0.2 honcho==1.0.1 gunicorn==20.1.0 diff --git a/sessions/requirements.txt b/sessions/requirements.txt index 55f852ba..99d776e5 100644 --- a/sessions/requirements.txt +++ b/sessions/requirements.txt @@ -1,2 +1,2 @@ google-cloud-firestore==2.3.4 -flask==2.0.1 +flask==2.0.2 From ac11437cabbb74e53e9420c1473d02c46e514d60 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 5 Oct 2021 08:06:11 +0200 Subject: [PATCH 264/395] chore(deps): update dependency cryptography to v35 (#400) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![WhiteSource Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [cryptography](https://togithub.com/pyca/cryptography) ([changelog](https://cryptography.io/en/latest/changelog/)) | `==3.4.8` -> `==35.0.0` | [![age](https://badges.renovateapi.com/packages/pypi/cryptography/35.0.0/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/pypi/cryptography/35.0.0/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/pypi/cryptography/35.0.0/compatibility-slim/3.4.8)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/pypi/cryptography/35.0.0/confidence-slim/3.4.8)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
pyca/cryptography ### [`v35.0.0`](https://togithub.com/pyca/cryptography/compare/3.4.8...35.0.0) [Compare Source](https://togithub.com/pyca/cryptography/compare/3.4.8...35.0.0)
--- ### Configuration 📅 **Schedule**: At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Renovate will not automatically rebase this PR, because other commits have been found. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box. --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#github/GoogleCloudPlatform/getting-started-python). --- authenticating-users/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index 7d206ff7..67034c7f 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -1,6 +1,6 @@ # [START getting_started_requirements] Flask==2.0.2 -cryptography==3.4.8 +cryptography==35.0.0 python-jose[cryptography]==3.3.0 requests==2.26.0 # [END getting_started_requirements] From c0316d34502b20615c8b4f9631a8c7e6e7b058a5 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 5 Oct 2021 08:14:10 +0200 Subject: [PATCH 265/395] chore(deps): update dependency google-cloud-storage to v1.42.3 (#403) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![WhiteSource Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [google-cloud-storage](https://togithub.com/googleapis/python-storage) | `==1.42.2` -> `==1.42.3` | [![age](https://badges.renovateapi.com/packages/pypi/google-cloud-storage/1.42.3/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/pypi/google-cloud-storage/1.42.3/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/pypi/google-cloud-storage/1.42.3/compatibility-slim/1.42.2)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/pypi/google-cloud-storage/1.42.3/confidence-slim/1.42.2)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
googleapis/python-storage ### [`v1.42.3`](https://togithub.com/googleapis/python-storage/blob/master/CHANGELOG.md#​1423-httpswwwgithubcomgoogleapispython-storagecomparev1422v1423-2021-09-30) [Compare Source](https://togithub.com/googleapis/python-storage/compare/v1.42.2...v1.42.3)
--- ### Configuration 📅 **Schedule**: At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Renovate will not automatically rebase this PR, because other commits have been found. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box. --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#github/GoogleCloudPlatform/getting-started-python). --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index e7926a5f..39eb3076 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,6 +1,6 @@ Flask==2.0.2 google-cloud-firestore==2.3.4 -google-cloud-storage==1.42.2 +google-cloud-storage==1.42.3 google-cloud-logging==2.6.0 google-cloud-error-reporting==1.2.3 gunicorn==20.1.0 From 1f3e1cb839fd5d54d40e3f269ed2a18064953b8b Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Mon, 11 Oct 2021 17:59:14 +0200 Subject: [PATCH 266/395] chore(deps): update dependency flake8 to v4 (#406) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1f2cee7d..6fa64bdd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -flake8==3.9.2 +flake8==4.0.1 nox==2021.10.1 pytest==6.2.5 requests==2.26.0 From f637994fe51ae28c7aa3f873442bab0267e2d619 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 12 Oct 2021 01:26:41 +0200 Subject: [PATCH 267/395] chore(deps): update dependency google-cloud-error-reporting to v1.3.0 (#407) --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 39eb3076..14a42b1a 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -2,6 +2,6 @@ Flask==2.0.2 google-cloud-firestore==2.3.4 google-cloud-storage==1.42.3 google-cloud-logging==2.6.0 -google-cloud-error-reporting==1.2.3 +google-cloud-error-reporting==1.3.0 gunicorn==20.1.0 six==1.16.0 From b37753bfb3cf1a75f8dad0410ee4444d41548912 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Sun, 31 Oct 2021 02:58:30 +0100 Subject: [PATCH 268/395] chore(deps): update dependency honcho to v1.1.0 (#410) --- gce/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gce/requirements.txt b/gce/requirements.txt index d34ac776..c67339e8 100644 --- a/gce/requirements.txt +++ b/gce/requirements.txt @@ -1,3 +1,3 @@ flask==2.0.2 -honcho==1.0.1 +honcho==1.1.0 gunicorn==20.1.0 From 4d9f880f3ed90988b047fab69126e5b1086f1714 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Mon, 1 Nov 2021 03:28:28 +0100 Subject: [PATCH 269/395] chore(deps): update dependency google-cloud-error-reporting to v1.4.0 (#409) Co-authored-by: Dan Lee <71398022+dandhlee@users.noreply.github.com> --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 14a42b1a..86a9525d 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -2,6 +2,6 @@ Flask==2.0.2 google-cloud-firestore==2.3.4 google-cloud-storage==1.42.3 google-cloud-logging==2.6.0 -google-cloud-error-reporting==1.3.0 +google-cloud-error-reporting==1.4.0 gunicorn==20.1.0 six==1.16.0 From 146e6ea98833bc317bc3034992eedfcac737154d Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 3 Nov 2021 01:28:30 +0100 Subject: [PATCH 270/395] chore(deps): update dependency google-cloud-error-reporting to v1.4.1 (#411) --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 86a9525d..6b2050ac 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -2,6 +2,6 @@ Flask==2.0.2 google-cloud-firestore==2.3.4 google-cloud-storage==1.42.3 google-cloud-logging==2.6.0 -google-cloud-error-reporting==1.4.0 +google-cloud-error-reporting==1.4.1 gunicorn==20.1.0 six==1.16.0 From 8cd31e3c94af0aec16da55148eeb6a186b642923 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 3 Nov 2021 01:34:14 +0100 Subject: [PATCH 271/395] chore(deps): update dependency google-cloud-logging to v2.7.0 (#412) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![WhiteSource Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [google-cloud-logging](https://togithub.com/googleapis/python-logging) | `==2.6.0` -> `==2.7.0` | [![age](https://badges.renovateapi.com/packages/pypi/google-cloud-logging/2.7.0/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/pypi/google-cloud-logging/2.7.0/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/pypi/google-cloud-logging/2.7.0/compatibility-slim/2.6.0)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/pypi/google-cloud-logging/2.7.0/confidence-slim/2.6.0)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
googleapis/python-logging ### [`v2.7.0`](https://togithub.com/googleapis/python-logging/blob/master/CHANGELOG.md#​270-httpswwwgithubcomgoogleapispython-loggingcomparev260v270-2021-11-02) [Compare Source](https://togithub.com/googleapis/python-logging/compare/v2.6.0...v2.7.0) ##### Features - add context manager support in client ([#​415](https://www.togithub.com/googleapis/python-logging/issues/415)) ([f5af164](https://www.github.com/googleapis/python-logging/commit/f5af16439807a0954ee78fa91cb69b9493b80176)) - added support for iam AuditData proto ([#​396](https://www.togithub.com/googleapis/python-logging/issues/396)) ([e3a1eba](https://www.github.com/googleapis/python-logging/commit/e3a1eba74dd8b67bcc73a78f784189ef2a9927c2)) - use structured logging on GCF with python 3.7 ([#​434](https://www.togithub.com/googleapis/python-logging/issues/434)) ([5055919](https://www.github.com/googleapis/python-logging/commit/5055919f70c82b38de6d1fa7f1df6006865a857b)) ##### Bug Fixes - add 'dict' annotation type to 'request' ([76ac729](https://www.github.com/googleapis/python-logging/commit/76ac729e42a782524be87ad71745aad37bbe1653)) - add 'dict' annotation type to 'request' ([23f9e1f](https://www.github.com/googleapis/python-logging/commit/23f9e1f6e9af30c4e65578edbf73c8c893b35285)) - **deps:** drop packaging dependency ([9d38995](https://www.github.com/googleapis/python-logging/commit/9d389958c7de31ae9e21228eaf965762b31d5e48)) - **deps:** require google-api-core >= 1.28.0 ([9d38995](https://www.github.com/googleapis/python-logging/commit/9d389958c7de31ae9e21228eaf965762b31d5e48)) - **deps:** require proto-plus==1.15.0 ([76ac729](https://www.github.com/googleapis/python-logging/commit/76ac729e42a782524be87ad71745aad37bbe1653)) - exception log message format ([#​394](https://www.togithub.com/googleapis/python-logging/issues/394)) ([c426bf5](https://www.github.com/googleapis/python-logging/commit/c426bf56787fa02140e8aa142ecd4d4e45432697)) - improper types in pagers generation ([76ac729](https://www.github.com/googleapis/python-logging/commit/76ac729e42a782524be87ad71745aad37bbe1653)) - improper types in pagers generation ([6a837a5](https://www.github.com/googleapis/python-logging/commit/6a837a5d1faab1f9fa8ac94e424e847821a0069f)) ##### Documentation - list oneofs in docstring ([9d38995](https://www.github.com/googleapis/python-logging/commit/9d389958c7de31ae9e21228eaf965762b31d5e48))
--- ### Configuration 📅 **Schedule**: At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, click this checkbox. --- This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#github/GoogleCloudPlatform/getting-started-python). --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 6b2050ac..84c95003 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,7 +1,7 @@ Flask==2.0.2 google-cloud-firestore==2.3.4 google-cloud-storage==1.42.3 -google-cloud-logging==2.6.0 +google-cloud-logging==2.7.0 google-cloud-error-reporting==1.4.1 gunicorn==20.1.0 six==1.16.0 From 569667d830e215ca502b8b333b9064eaf4849321 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 12 Nov 2021 02:20:00 +0100 Subject: [PATCH 272/395] chore(deps): update dependency google-cloud-pubsub to v2.9.0 (#413) --- background/app/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index cb31418f..0154d37c 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ google-cloud-firestore==2.3.4 -google-cloud-pubsub==2.8.0 +google-cloud-pubsub==2.9.0 flask==2.0.2 From 5a6559197444298d097bfc78b03fdb718178d437 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 23 Nov 2021 16:30:03 +0100 Subject: [PATCH 273/395] chore(deps): update dependency cryptography to v36 (#415) --- authenticating-users/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index 67034c7f..76c82081 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -1,6 +1,6 @@ # [START getting_started_requirements] Flask==2.0.2 -cryptography==35.0.0 +cryptography==36.0.0 python-jose[cryptography]==3.3.0 requests==2.26.0 # [END getting_started_requirements] From 45b5774f0648f31340028148b9f7a022d2ce52b2 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 23 Nov 2021 16:57:53 +0100 Subject: [PATCH 274/395] chore(deps): update dependency google-cloud-storage to v1.43.0 (#414) Co-authored-by: Anthonios Partheniou --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 84c95003..964507ab 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,6 +1,6 @@ Flask==2.0.2 google-cloud-firestore==2.3.4 -google-cloud-storage==1.42.3 +google-cloud-storage==1.43.0 google-cloud-logging==2.7.0 google-cloud-error-reporting==1.4.1 gunicorn==20.1.0 From 45ea0734cf10c69b9e54baf2670d4feb8b6d6745 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Thu, 25 Nov 2021 17:46:08 +0100 Subject: [PATCH 275/395] chore(deps): update dependency google-cloud-translate to v3.6.0 (#408) Co-authored-by: Dan Lee <71398022+dandhlee@users.noreply.github.com> --- background/function/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/function/requirements.txt b/background/function/requirements.txt index 850a571e..041dfc16 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-translate==3.4.1 +google-cloud-translate==3.6.0 google-cloud-firestore==2.3.4 From e5dfea6d2583fb9e4b341165728a96c68e01d0c7 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Thu, 25 Nov 2021 17:51:30 +0100 Subject: [PATCH 276/395] chore(deps): update dependency google-cloud-translate to v3.6.1 (#416) --- background/function/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/function/requirements.txt b/background/function/requirements.txt index 041dfc16..625ae405 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-translate==3.6.0 +google-cloud-translate==3.6.1 google-cloud-firestore==2.3.4 From 212ae7b1558c72291ad763cdd8df39ff4ef967ab Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Thu, 20 Jan 2022 23:33:19 +0100 Subject: [PATCH 277/395] chore(deps): update dependency nox to v2022 (#420) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6fa64bdd..d303d64a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ flake8==4.0.1 -nox==2021.10.1 +nox==2022.1.7 pytest==6.2.5 requests==2.26.0 From 2ac94716f41967630f2c4ac2ec1e553fe2f640c2 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Thu, 20 Jan 2022 23:55:34 +0100 Subject: [PATCH 278/395] chore(deps): update dependency google-cloud-storage to v2 (#421) * chore(deps): update dependency google-cloud-storage to v2 * chore(deps): pin to 2.0.0 Co-authored-by: Dan Lee <71398022+dandhlee@users.noreply.github.com> --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 964507ab..a8ed1206 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,6 +1,6 @@ Flask==2.0.2 google-cloud-firestore==2.3.4 -google-cloud-storage==1.43.0 +google-cloud-storage==2.0.0 google-cloud-logging==2.7.0 google-cloud-error-reporting==1.4.1 gunicorn==20.1.0 From 05b9f092461dfc4937e6a61db1e7ae655d14b643 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Sun, 23 Jan 2022 09:37:02 +0100 Subject: [PATCH 279/395] chore(deps): update dependency requests to v2.27.1 (#418) Co-authored-by: Dan Lee <71398022+dandhlee@users.noreply.github.com> --- authenticating-users/requirements.txt | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index 76c82081..7c9592b1 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -2,5 +2,5 @@ Flask==2.0.2 cryptography==36.0.0 python-jose[cryptography]==3.3.0 -requests==2.26.0 +requests==2.27.1 # [END getting_started_requirements] diff --git a/requirements.txt b/requirements.txt index d303d64a..a74c8c01 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ flake8==4.0.1 nox==2022.1.7 pytest==6.2.5 -requests==2.26.0 +requests==2.27.1 From ada42dc97b736216e7f634d826ba248462a2b17b Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Sun, 23 Jan 2022 09:46:37 +0100 Subject: [PATCH 280/395] chore(deps): update dependency cryptography to v36.0.1 (#417) Co-authored-by: Dan Lee <71398022+dandhlee@users.noreply.github.com> --- authenticating-users/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index 7c9592b1..459a6da4 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -1,6 +1,6 @@ # [START getting_started_requirements] Flask==2.0.2 -cryptography==36.0.0 +cryptography==36.0.1 python-jose[cryptography]==3.3.0 requests==2.27.1 # [END getting_started_requirements] From 63f1fa389be3cef0c7ed8661c40f1c7961abda94 Mon Sep 17 00:00:00 2001 From: Dan Lee <71398022+dandhlee@users.noreply.github.com> Date: Sun, 23 Jan 2022 06:12:46 -0500 Subject: [PATCH 281/395] fix: update deprecated methods (#424) --- background/app/main_test.py | 6 +++--- bookshelf/main_test.py | 4 ++-- optional-kubernetes-engine/tests/conftest.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/background/app/main_test.py b/background/app/main_test.py index f0dc9fe7..f3bf28c2 100644 --- a/background/app/main_test.py +++ b/background/app/main_test.py @@ -30,7 +30,7 @@ TOPIC_NAME = "projects/{}/topics/{}".format(project_id, "translate") -@pytest.yield_fixture +@pytest.fixture def db(): def clear_collection(collection): """Removes every document from the collection, to make it easy to see @@ -54,13 +54,13 @@ def clear_collection(collection): yield client -@pytest.yield_fixture +@pytest.fixture def publisher(): client = pubsub.PublisherClient() yield client -@pytest.yield_fixture +@pytest.fixture def subscriber(): subscriber = pubsub.SubscriberClient() subscriber.create_subscription( diff --git a/bookshelf/main_test.py b/bookshelf/main_test.py index 404de778..00c613fc 100644 --- a/bookshelf/main_test.py +++ b/bookshelf/main_test.py @@ -26,7 +26,7 @@ os.environ['GOOGLE_CLOUD_PROJECT'] = project_id -@pytest.yield_fixture +@pytest.fixture def app(request): """This fixture provides a Flask app instance configured for testing. @@ -38,7 +38,7 @@ def app(request): yield app -@pytest.yield_fixture +@pytest.fixture def firestore(): import firestore diff --git a/optional-kubernetes-engine/tests/conftest.py b/optional-kubernetes-engine/tests/conftest.py index d098db9d..57a50a1c 100644 --- a/optional-kubernetes-engine/tests/conftest.py +++ b/optional-kubernetes-engine/tests/conftest.py @@ -22,7 +22,7 @@ from retrying import retry -@pytest.yield_fixture(params=['datastore', 'mongodb']) +@pytest.fixture(params=['datastore', 'mongodb']) def app(request): """This fixtures provides a Flask app instance configured for testing. @@ -43,7 +43,7 @@ def app(request): yield app -@pytest.yield_fixture +@pytest.fixture def model(monkeypatch, app): """This fixture provides a modified version of the app's model that tracks all created items and deletes them at the end of the test. From 8720d044353c724a5083f3a8d583e73ba7f1653c Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Sun, 23 Jan 2022 06:51:22 -0500 Subject: [PATCH 282/395] ci: update Dockerfile used for running tests to python 3.7 (#425) --- .kokoro/docker/Dockerfile | 18 ++++++++++++++++-- noxfile.py | 2 +- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/.kokoro/docker/Dockerfile b/.kokoro/docker/Dockerfile index 2830f0af..6b6c19f1 100644 --- a/.kokoro/docker/Dockerfile +++ b/.kokoro/docker/Dockerfile @@ -35,11 +35,25 @@ RUN apt update \ zlib1g-dev \ && apt clean +###################### Install python 3.7.12 + +# Download python 3.7.12 +RUN wget https://www.python.org/ftp/python/3.7.12/Python-3.7.12.tgz + +# Extract files +RUN tar -xvf Python-3.7.12.tgz + +# Install python 3.7.12 +RUN ./Python-3.7.12/configure --enable-optimizations +RUN make altinstall + +###################### Check python version + RUN python --version RUN which python # Setup Cloud SDK -ENV CLOUD_SDK_VERSION 324.0.0 +ENV CLOUD_SDK_VERSION 369.0.0 # Use system python for cloud sdk. ENV CLOUDSDK_PYTHON /usr/bin/python RUN wget https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-sdk-$CLOUD_SDK_VERSION-linux-x86_64.tar.gz @@ -51,6 +65,6 @@ ENV PATH /google-cloud-sdk/bin:$PATH ENV PATH ~/.local/bin:/root/.local/bin:$PATH # Install the current version of nox. -RUN python3 -m pip install --user --no-cache-dir nox==2020.12.31 +RUN python3 -m pip install --user --no-cache-dir nox==2022.1.7 CMD ["nox"] diff --git a/noxfile.py b/noxfile.py index 8d7c7d5e..68fb5a37 100644 --- a/noxfile.py +++ b/noxfile.py @@ -55,7 +55,7 @@ def run_test(session, dir): success_codes=[0, 5]) -@nox.session +@nox.session(python="3.7") @nox.parametrize('dir', DIRS) def run_tests(session, dir=None): """Run all tests for all directories (slow!)""" From 11326abd6c2ba9ec71b8bbc29331d7b5e7930f49 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Sun, 23 Jan 2022 13:03:50 +0100 Subject: [PATCH 283/395] chore(deps): update dependency google-cloud-storage to v2.1.0 (#423) Co-authored-by: Anthonios Partheniou --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index a8ed1206..702c779b 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,6 +1,6 @@ Flask==2.0.2 google-cloud-firestore==2.3.4 -google-cloud-storage==2.0.0 +google-cloud-storage==2.1.0 google-cloud-logging==2.7.0 google-cloud-error-reporting==1.4.1 gunicorn==20.1.0 From e2deaf4ab2770aa3ff9d28499c62165dacc97811 Mon Sep 17 00:00:00 2001 From: Dan Lee <71398022+dandhlee@users.noreply.github.com> Date: Sun, 23 Jan 2022 07:09:50 -0500 Subject: [PATCH 284/395] chore: update references to master (#427) --- .github/sync-repo-settings.yaml | 4 ++-- README.md | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/sync-repo-settings.yaml b/.github/sync-repo-settings.yaml index ad730388..4dfda801 100644 --- a/.github/sync-repo-settings.yaml +++ b/.github/sync-repo-settings.yaml @@ -10,10 +10,10 @@ squashMergeAllowed: true # Defaults to `false` mergeCommitAllowed: false -# Rules for master branch protection +# Rules for main branch protection branchProtectionRules: # Identifies the protection rule pattern. Name of the branch to be protected. -# Defaults to `master` +# Defaults to `main` - pattern: main # Can admins overwrite branch protection. # Defaults to `true` diff --git a/README.md b/README.md index 0e5b3292..a347e9bb 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,12 @@ The code for the samples is contained in individual folders in this repository. Tutorial | Folder ---------|------- -[Getting Started](https://cloud.google.com/python/getting-started/) | [bookshelf](https://github.com/GoogleCloudPlatform/getting-started-python/tree/master/bookshelf) -[Background Processing](https://cloud.google.com/python/getting-started/background-processing) | [background](https://github.com/GoogleCloudPlatform/getting-started-python/tree/master/background) -[Deploying to Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/docs/quickstarts/deploying-a-language-specific-app) | [in "kubernetes-engine-samples" repo](https://github.com/GoogleCloudPlatform/kubernetes-engine-samples/tree/master/quickstart/python) -[Deploying to Google Compute Engine](https://cloud.google.com/python/tutorials/getting-started-on-compute-engine) | [gce](https://github.com/GoogleCloudPlatform/getting-started-python/tree/master/gce) -[Handling Sessions with Firestore](https://cloud.google.com/python/getting-started/session-handling-with-firestore) | [sessions](https://github.com/GoogleCloudPlatform/getting-started-python/tree/master/sessions) -[Authenticating Users with IAP](https://cloud.google.com/python/getting-started/authenticate-users) | [authenticating-users](https://github.com/GoogleCloudPlatform/getting-started-python/tree/master/authenticating-users) +[Getting Started](https://cloud.google.com/python/getting-started/) | [bookshelf](https://github.com/GoogleCloudPlatform/getting-started-python/tree/main/bookshelf) +[Background Processing](https://cloud.google.com/python/getting-started/background-processing) | [background](https://github.com/GoogleCloudPlatform/getting-started-python/tree/main/background) +[Deploying to Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/docs/quickstarts/deploying-a-language-specific-app) | [in "kubernetes-engine-samples" repo](https://github.com/GoogleCloudPlatform/kubernetes-engine-samples/tree/main/quickstart/python) +[Deploying to Google Compute Engine](https://cloud.google.com/python/tutorials/getting-started-on-compute-engine) | [gce](https://github.com/GoogleCloudPlatform/getting-started-python/tree/main/gce) +[Handling Sessions with Firestore](https://cloud.google.com/python/getting-started/session-handling-with-firestore) | [sessions](https://github.com/GoogleCloudPlatform/getting-started-python/tree/main/sessions) +[Authenticating Users with IAP](https://cloud.google.com/python/getting-started/authenticate-users) | [authenticating-users](https://github.com/GoogleCloudPlatform/getting-started-python/tree/main/authenticating-users) ## Contributing changes From 34a517706d28e084c9726b9755d2604525f293a6 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Mon, 7 Feb 2022 20:20:56 -0500 Subject: [PATCH 285/395] fix: fix gce sample (#430) --- gce/deploy.sh | 2 +- gce/startup-script.sh | 4 ++-- gce/teardown.sh | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/gce/deploy.sh b/gce/deploy.sh index b3449837..36ed75ca 100644 --- a/gce/deploy.sh +++ b/gce/deploy.sh @@ -19,7 +19,7 @@ MY_INSTANCE_NAME="my-app-instance" ZONE=us-central1-a gcloud compute instances create $MY_INSTANCE_NAME \ - --image-family=debian-9 \ + --image-family=debian-10 \ --image-project=debian-cloud \ --machine-type=g1-small \ --scopes userinfo-email,cloud-platform \ diff --git a/gce/startup-script.sh b/gce/startup-script.sh index 2068606e..2c1712bc 100644 --- a/gce/startup-script.sh +++ b/gce/startup-script.sh @@ -22,7 +22,7 @@ sudo bash install-logging-agent.sh # Install or update needed software apt-get update -apt-get install -yq git supervisor python python-pip +apt-get install -yq git supervisor python python-pip python3-distutils pip install --upgrade pip virtualenv # Account to own server process @@ -34,7 +34,7 @@ git clone https://github.com/GoogleCloudPlatform/getting-started-python.git /opt # Python environment setup virtualenv -p python3 /opt/app/gce/env -source /opt/app/gce/env/bin/activate +/bin/bash -c "source /opt/app/gce/env/bin/activate" /opt/app/gce/env/bin/pip install -r /opt/app/gce/requirements.txt # Set ownership to newly created account diff --git a/gce/teardown.sh b/gce/teardown.sh index b31d1139..668b1218 100644 --- a/gce/teardown.sh +++ b/gce/teardown.sh @@ -16,8 +16,8 @@ set -x -MY_INSTANCE_NAME="my-instance-name" -ZONE=us-central1-f +MY_INSTANCE_NAME="my-app-instance" +ZONE=us-central1-a gcloud compute instances delete $MY_INSTANCE_NAME \ --zone=$ZONE --delete-disks=all From da174dce6e9a8bfa0b644c294068d91d232c3d3d Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 8 Feb 2022 02:29:23 +0100 Subject: [PATCH 286/395] chore(deps): update dependency pytest to v7 (#429) Co-authored-by: Anthonios Partheniou --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a74c8c01..af6800bf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ flake8==4.0.1 nox==2022.1.7 -pytest==6.2.5 +pytest==7.0.0 requests==2.27.1 From a3e6cdab97dc0676d6bcfe168f07b071e022bdfb Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 11 Feb 2022 22:35:06 +0100 Subject: [PATCH 287/395] chore(deps): update dependency pytest to v7.0.1 (#431) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index af6800bf..5dfcb673 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ flake8==4.0.1 nox==2022.1.7 -pytest==7.0.0 +pytest==7.0.1 requests==2.27.1 From 42ce3ecf26ec478651c31faae02a186d2fbaa900 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Mon, 14 Feb 2022 22:16:53 +0100 Subject: [PATCH 288/395] chore(deps): update dependency flask to v2.0.3 (#432) --- authenticating-users/requirements.txt | 2 +- background/app/requirements.txt | 2 +- bookshelf/requirements.txt | 2 +- gce/requirements.txt | 2 +- sessions/requirements.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index 459a6da4..46d63f7d 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -1,5 +1,5 @@ # [START getting_started_requirements] -Flask==2.0.2 +Flask==2.0.3 cryptography==36.0.1 python-jose[cryptography]==3.3.0 requests==2.27.1 diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 0154d37c..73a71d18 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ google-cloud-firestore==2.3.4 google-cloud-pubsub==2.9.0 -flask==2.0.2 +flask==2.0.3 diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 702c779b..f59726a8 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,4 +1,4 @@ -Flask==2.0.2 +Flask==2.0.3 google-cloud-firestore==2.3.4 google-cloud-storage==2.1.0 google-cloud-logging==2.7.0 diff --git a/gce/requirements.txt b/gce/requirements.txt index c67339e8..ec5299a7 100644 --- a/gce/requirements.txt +++ b/gce/requirements.txt @@ -1,3 +1,3 @@ -flask==2.0.2 +flask==2.0.3 honcho==1.1.0 gunicorn==20.1.0 diff --git a/sessions/requirements.txt b/sessions/requirements.txt index 99d776e5..4c8b43fc 100644 --- a/sessions/requirements.txt +++ b/sessions/requirements.txt @@ -1,2 +1,2 @@ google-cloud-firestore==2.3.4 -flask==2.0.2 +flask==2.0.3 From 5135cad4cb1714f001a85377790eb79ab5e29838 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Mon, 21 Feb 2022 15:35:12 +0100 Subject: [PATCH 289/395] chore(deps): update dependency google-cloud-error-reporting to v1.5.0 (#433) --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index f59726a8..a055cf0c 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -2,6 +2,6 @@ Flask==2.0.3 google-cloud-firestore==2.3.4 google-cloud-storage==2.1.0 google-cloud-logging==2.7.0 -google-cloud-error-reporting==1.4.1 +google-cloud-error-reporting==1.5.0 gunicorn==20.1.0 six==1.16.0 From e1fe7c95ca00522273dd3b389c31226e51d6db9a Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Mon, 21 Feb 2022 15:41:14 +0100 Subject: [PATCH 290/395] chore(deps): update dependency google-cloud-logging to v3 (#428) Co-authored-by: Anthonios Partheniou Co-authored-by: Dan Lee <71398022+dandhlee@users.noreply.github.com> --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index a055cf0c..b9927282 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,7 +1,7 @@ Flask==2.0.3 google-cloud-firestore==2.3.4 google-cloud-storage==2.1.0 -google-cloud-logging==2.7.0 +google-cloud-logging==3.0.0 google-cloud-error-reporting==1.5.0 gunicorn==20.1.0 six==1.16.0 From eadfaad8b21dac7edb827b539d16f110d978cdc9 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 25 Feb 2022 19:31:14 +0100 Subject: [PATCH 291/395] chore(deps): update dependency google-cloud-translate to v3.7.0 (#434) --- background/function/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/function/requirements.txt b/background/function/requirements.txt index 625ae405..da36a6a3 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-translate==3.6.1 +google-cloud-translate==3.7.0 google-cloud-firestore==2.3.4 From caba978fc4dc180b9cf9b736da4ab961a8612e46 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Sat, 5 Mar 2022 17:55:22 +0100 Subject: [PATCH 292/395] chore(deps): update dependency google-cloud-pubsub to v2.10.0 (#437) --- background/app/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 73a71d18..d1a4d53e 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ google-cloud-firestore==2.3.4 -google-cloud-pubsub==2.9.0 +google-cloud-pubsub==2.10.0 flask==2.0.3 From 82fa9397387878ddd9c221e7ed61794d83e2a7c4 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Sat, 5 Mar 2022 18:02:51 +0100 Subject: [PATCH 293/395] chore(deps): update dependency google-cloud-translate to v3.7.1 (#435) Co-authored-by: Anthonios Partheniou --- background/function/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/function/requirements.txt b/background/function/requirements.txt index da36a6a3..25b5435d 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-translate==3.7.0 +google-cloud-translate==3.7.1 google-cloud-firestore==2.3.4 From ab1773166d5657265693c5d16d748bc7d9e77be2 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Mon, 7 Mar 2022 20:05:22 +0100 Subject: [PATCH 294/395] chore(deps): update dependency google-cloud-translate to v3.7.2 (#438) --- background/function/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/function/requirements.txt b/background/function/requirements.txt index 25b5435d..d51fbeaa 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-translate==3.7.1 +google-cloud-translate==3.7.2 google-cloud-firestore==2.3.4 From 0032fa48e6d23f4d4e57aa9d591dc6770886b6a6 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 8 Mar 2022 18:40:13 +0100 Subject: [PATCH 295/395] chore(deps): update dependency google-cloud-error-reporting to v1.5.1 (#439) --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index b9927282..5eb89669 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -2,6 +2,6 @@ Flask==2.0.3 google-cloud-firestore==2.3.4 google-cloud-storage==2.1.0 google-cloud-logging==3.0.0 -google-cloud-error-reporting==1.5.0 +google-cloud-error-reporting==1.5.1 gunicorn==20.1.0 six==1.16.0 From 4ca81cc2057c96081549f957869e74e916c82fa7 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 9 Mar 2022 04:44:50 +0100 Subject: [PATCH 296/395] chore(deps): update dependency google-cloud-firestore to v2.4.0 (#440) --- background/app/requirements.txt | 2 +- background/function/requirements.txt | 2 +- bookshelf/requirements.txt | 2 +- sessions/requirements.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index d1a4d53e..4e3f6ee7 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ -google-cloud-firestore==2.3.4 +google-cloud-firestore==2.4.0 google-cloud-pubsub==2.10.0 flask==2.0.3 diff --git a/background/function/requirements.txt b/background/function/requirements.txt index d51fbeaa..f45b8bf1 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ google-cloud-translate==3.7.2 -google-cloud-firestore==2.3.4 +google-cloud-firestore==2.4.0 diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 5eb89669..63b1f909 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,5 +1,5 @@ Flask==2.0.3 -google-cloud-firestore==2.3.4 +google-cloud-firestore==2.4.0 google-cloud-storage==2.1.0 google-cloud-logging==3.0.0 google-cloud-error-reporting==1.5.1 diff --git a/sessions/requirements.txt b/sessions/requirements.txt index 4c8b43fc..968ae7e4 100644 --- a/sessions/requirements.txt +++ b/sessions/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-firestore==2.3.4 +google-cloud-firestore==2.4.0 flask==2.0.3 From 29100241707ea374190104277460b233bd9d94d3 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Thu, 10 Mar 2022 11:38:53 +0100 Subject: [PATCH 297/395] chore(deps): update dependency google-cloud-pubsub to v2.11.0 (#441) --- background/app/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 4e3f6ee7..1f22a80e 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ google-cloud-firestore==2.4.0 -google-cloud-pubsub==2.10.0 +google-cloud-pubsub==2.11.0 flask==2.0.3 From 226ff0249cff430e21e40d28795e8fa19e97ddbc Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Sun, 13 Mar 2022 23:41:19 +0100 Subject: [PATCH 298/395] chore(deps): update dependency pytest to v7.1.0 (#442) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5dfcb673..43f55a8d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ flake8==4.0.1 nox==2022.1.7 -pytest==7.0.1 +pytest==7.1.0 requests==2.27.1 From c6772d686bdd625232712e5d4eb303495b489a8f Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Mon, 14 Mar 2022 19:53:08 +0100 Subject: [PATCH 299/395] chore(deps): update dependency google-cloud-storage to v2.2.0 (#443) --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 63b1f909..2b9ac0c9 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,6 +1,6 @@ Flask==2.0.3 google-cloud-firestore==2.4.0 -google-cloud-storage==2.1.0 +google-cloud-storage==2.2.0 google-cloud-logging==3.0.0 google-cloud-error-reporting==1.5.1 gunicorn==20.1.0 From 3028df4e944fb8bd462fa19f185afb78de2af922 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 15 Mar 2022 23:32:39 +0100 Subject: [PATCH 300/395] chore(deps): update dependency google-cloud-storage to v2.2.1 (#444) --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 2b9ac0c9..c8b8d579 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,6 +1,6 @@ Flask==2.0.3 google-cloud-firestore==2.4.0 -google-cloud-storage==2.2.0 +google-cloud-storage==2.2.1 google-cloud-logging==3.0.0 google-cloud-error-reporting==1.5.1 gunicorn==20.1.0 From 5db28a565e725ca8cfee0fac5099660d45b09327 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 18 Mar 2022 13:31:53 +0100 Subject: [PATCH 301/395] chore(deps): update dependency pytest to v7.1.1 (#446) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 43f55a8d..2f6c1476 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ flake8==4.0.1 nox==2022.1.7 -pytest==7.1.0 +pytest==7.1.1 requests==2.27.1 From b5a670b9f3312b3550cfffa3b978a0014a66ca76 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 18 Mar 2022 13:39:54 +0100 Subject: [PATCH 302/395] chore(deps): update dependency cryptography to v36.0.2 (#445) Co-authored-by: Dan Lee <71398022+dandhlee@users.noreply.github.com> --- authenticating-users/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index 46d63f7d..1dffe5e5 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -1,6 +1,6 @@ # [START getting_started_requirements] Flask==2.0.3 -cryptography==36.0.1 +cryptography==36.0.2 python-jose[cryptography]==3.3.0 requests==2.27.1 # [END getting_started_requirements] From 21aa26c8b8df5279a20c68bf8cf4db76f137b4ee Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Thu, 31 Mar 2022 01:14:44 +0200 Subject: [PATCH 303/395] chore(deps): update dependency flask (#447) --- authenticating-users/requirements.txt | 2 +- background/app/requirements.txt | 2 +- bookshelf/requirements.txt | 2 +- gce/requirements.txt | 2 +- sessions/requirements.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index 1dffe5e5..b81097e1 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -1,5 +1,5 @@ # [START getting_started_requirements] -Flask==2.0.3 +Flask==2.1.0 cryptography==36.0.2 python-jose[cryptography]==3.3.0 requests==2.27.1 diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 1f22a80e..752a5556 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ google-cloud-firestore==2.4.0 google-cloud-pubsub==2.11.0 -flask==2.0.3 +flask==2.1.1 diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index c8b8d579..de166b57 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,4 +1,4 @@ -Flask==2.0.3 +Flask==2.1.0 google-cloud-firestore==2.4.0 google-cloud-storage==2.2.1 google-cloud-logging==3.0.0 diff --git a/gce/requirements.txt b/gce/requirements.txt index ec5299a7..e45610a5 100644 --- a/gce/requirements.txt +++ b/gce/requirements.txt @@ -1,3 +1,3 @@ -flask==2.0.3 +flask==2.1.1 honcho==1.1.0 gunicorn==20.1.0 diff --git a/sessions/requirements.txt b/sessions/requirements.txt index 968ae7e4..63f6ef09 100644 --- a/sessions/requirements.txt +++ b/sessions/requirements.txt @@ -1,2 +1,2 @@ google-cloud-firestore==2.4.0 -flask==2.0.3 +flask==2.1.1 From bba07139956c740a5315edea22713ce1f899601b Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Thu, 31 Mar 2022 01:29:27 +0200 Subject: [PATCH 304/395] chore(deps): update dependency flask to v2.1.1 (#448) --- authenticating-users/requirements.txt | 2 +- bookshelf/requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index b81097e1..858f4813 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -1,5 +1,5 @@ # [START getting_started_requirements] -Flask==2.1.0 +Flask==2.1.1 cryptography==36.0.2 python-jose[cryptography]==3.3.0 requests==2.27.1 diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index de166b57..9565a5ee 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,4 +1,4 @@ -Flask==2.1.0 +Flask==2.1.1 google-cloud-firestore==2.4.0 google-cloud-storage==2.2.1 google-cloud-logging==3.0.0 From a361d1921e6dff899f9dbbf376db528e1bdaa69e Mon Sep 17 00:00:00 2001 From: Bu Sun Kim <8822365+busunkim96@users.noreply.github.com> Date: Fri, 1 Apr 2022 11:34:41 -0600 Subject: [PATCH 305/395] chore: commit logging agent script to repo (#449) Fixes b/210461441. > An unvendored shell script (not in your repo) is being executed using `curl | bash`. All executed source should be commited to your source repository. This commits what appears to be the latest iteration of the logging agent to the repository. I visited https://cloud.google.com/logging/docs/agent/logging/installation first, which has `https://dl.google.com/cloudagents/add-logging-agent-repo.sh`. However, it has this note at the top, so I switched to the Ops Agent: > Key Point: While this agent is still supported, we recommend that you use the [Ops Agent](https://cloud.google.com/stackdriver/docs/solutions/agents/ops-agent) for new workloads and eventually transition your existing VMs to use the new agent. The Ops Agent is the eventual replacement for the existing Google Cloud's operations suite agents, and it offers a simple user experience with metrics and logging combined into a single agent. --- gce/add-google-cloud-ops-agent-repo.sh | 519 +++++++++++++++++++++++++ gce/startup-script.sh | 17 +- 2 files changed, 527 insertions(+), 9 deletions(-) create mode 100644 gce/add-google-cloud-ops-agent-repo.sh diff --git a/gce/add-google-cloud-ops-agent-repo.sh b/gce/add-google-cloud-ops-agent-repo.sh new file mode 100644 index 00000000..63cf2dbd --- /dev/null +++ b/gce/add-google-cloud-ops-agent-repo.sh @@ -0,0 +1,519 @@ +#!/bin/bash +# Copyright 2020 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# *NOTE*: The source of truth for this script is: +# https://dl.google.com/cloudagents/add-google-cloud-ops-agent-repo.sh +# See https://cloud.google.com/stackdriver/docs/solutions/agents/ops-agent/installation +# for installation instructions. +# It is committed to this repository to follow security best practices. +# +# +# Add repository for the Google ops agent. +# +# This script adds the required apt or yum repository and installs or uninstalls +# the agent based on the corresponding flags. +# +# Available flags: +# * `--verbose`: +# Turns on verbose logging during the script execution, which is helpful for +# debugging purposes. +# +# * `--also-install`: +# Installs the agent after adding the agent package repository. If this flag +# is absent, the script only adds the agent package repository. This flag +# can not be run with the `--uninstall` flag. +# +# * `--version `: +# Sets the agent version for the script to install. Allowed formats: +# * `latest`: +# Adds an agent package repository that contains all agent versions, and +# installs the latest version of the agent. +# * `MAJOR_VERSION.*.*`: +# Adds an agent package repository that contains all agent versions up to +# this major version (e.g. `1.*.*`), and installs the latest version of +# the agent within the range of that major version. +# * `MAJOR_VERSION.MINOR_VERSION.PATCH_VERSION`: +# Adds an agent package repository that contains all agent versions, and +# installs the specified version of the agent (e.g. `3.2.1`). +# +# * `--uninstall`: +# Uninstalls the agent. This flag can not be run with the `--also-install` +# flag. +# +# * `--remove-repo`: +# Removes the corresponding agent package repository after installing or +# uninstalling the agent. +# +# * `--dry-run`: +# Triggers only a dry run of the script execution and prints out the +# commands that it is supposed to execute. This is helpful to know what +# actions the script will take. +# +# * `--uninstall-standalone-logging-agent`: +# Uninstalls the standalone logging agent (`google-fluentd`). +# +# * `--uninstall-standalone-monitoring-agent`: +# Uninstalls the standalone monitoring agent (`stackdriver-agent`). +# +# Sample usage: +# * To add the repo that contains all agent versions, run: +# $ bash add-google-cloud-ops-agent-repo.sh +# +# * To add the repo and also install the agent, run: +# $ bash add-google-cloud-ops-agent-repo.sh --also-install --version= +# +# * To uninstall the agent run: +# $ bash add-google-cloud-ops-agent-repo.sh --uninstall +# +# * To uninstall the agent and remove the repo, run: +# $ bash add-google-cloud-ops-agent-repo.sh --uninstall --remove-repo +# +# * To run the script with verbose logging, run: +# $ bash add-google-cloud-ops-agent-repo.sh --also-install --verbose +# +# * To run the script in dry-run mode, run: +# $ bash add-google-cloud-ops-agent-repo.sh --also-install --dry-run +# +# * To replace standalone agents with the Ops agent, run: +# $ bash add-google-cloud-ops-agent-repo.sh --also-install --uninstall-standalone-logging-agent --uninstall-standalone-monitoring-agent +# +# Internal usage only: +# The environment variable `REPO_SUFFIX` can be set to alter which repository is +# used. A dash (-) will be inserted prior to the supplied suffix. `REPO_SUFFIX` +# defaults to `all` which contains all agent versions across different major +# versions. The full repository name is: +# "google-cloud-ops-agent-[-]-". + +# Ignore the return code of command substitution in variables. +# shellcheck disable=SC2155 +# +# Initialize var used to notify config management tools of when a change is made. +CHANGED=0 + +fail() { + echo >&2 "[$(date +'%Y-%m-%dT%H:%M:%S%z')] $*" + exit 1 +} + +# Parsing flag value. +declare -a ACTIONS=() +DRY_RUN='' +VERBOSE='false' +while getopts -- '-:' OPTCHAR; do + case "${OPTCHAR}" in + -) + case "${OPTARG}" in + # Note: Do not remove entries from this list when deprecating flags. + # That would break user scripts that specify those flags. Instead, + # leave the flag in place but make it a noop. + also-install) ACTIONS+=('also-install') ;; + version=*) AGENT_VERSION="${OPTARG#*=}" ;; + uninstall) ACTIONS+=('uninstall') ;; + remove-repo) ACTIONS+=('remove-repo') ;; + uninstall-standalone-logging-agent) ACTIONS+=('uninstall-standalone-logging-agent') ;; + uninstall-standalone-monitoring-agent) ACTIONS+=('uninstall-standalone-monitoring-agent') ;; + dry-run) echo 'Starting dry run'; DRY_RUN='dryrun' ;; + verbose) VERBOSE='true' ;; + *) fail "Unknown option '${OPTARG}'." ;; + esac + esac +done +[[ " ${ACTIONS[*]} " == *\ uninstall\ * || ( " ${ACTIONS[*]} " == *\ remove-repo\ * && " ${ACTIONS[*]} " != *\ also-install\ * )]] || \ + ACTIONS+=('add-repo') +# Sort the actions array for easier parsing. +readarray -t ACTIONS < <(printf '%s\n' "${ACTIONS[@]}" | sort) +readonly ACTIONS DRY_RUN VERBOSE + +if [[ " ${ACTIONS[*]} " == *\ also-install*uninstall\ * ]]; then + fail "Received conflicting flags 'also-install' and 'uninstall'." +fi + +if [[ "${VERBOSE}" == 'true' ]]; then + echo 'Enable verbose logging.' + set -x +fi + +# Host that serves the repositories. +REPO_HOST='packages.cloud.google.com' + +# URL for the ops agent documentation. +AGENT_DOCS_URL='https://cloud.google.com/stackdriver/docs/solutions/ops-agent' + +# URL documentation which lists supported platforms for running the ops agent. +AGENT_SUPPORTED_URL="${AGENT_DOCS_URL}/#supported_operating_systems" + +# Packages to install. +AGENT_PACKAGE='google-cloud-ops-agent' +declare -a ADDITIONAL_PACKAGES=() + +if [[ -f /etc/os-release ]]; then + . /etc/os-release +fi + +# If dry-run mode is enabled, echo VM state-changing commands instead of executing them. +dryrun() { + # Needed for commands that use pipes. + if [[ ! -t 0 ]]; then + cat + fi + printf -v cmd_str '%q ' "$@" + echo "DRY_RUN: Not executing '$cmd_str'" +} + +refresh_failed() { + local REPO_TYPE="$1" + local OS_FAMILY="$2" + fail "Could not refresh the google-cloud-ops-agent ${REPO_TYPE} repositories. +Please check your network connectivity and make sure you are running a supported +${OS_FAMILY} distribution. See ${AGENT_SUPPORTED_URL} +for a list of supported platforms." +} + +resolve_version() { + if [[ "${AGENT_VERSION:-latest}" == 'latest' ]]; then + AGENT_VERSION='' + elif grep -qE '^[0-9]+\.\*\.\*$' <<<"${AGENT_VERSION}"; then + REPO_SUFFIX="${REPO_SUFFIX:-"${AGENT_VERSION%%.*}"}" + elif ! grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$' <<<"${AGENT_VERSION}"; then + fail "The agent version [${AGENT_VERSION}] is not allowed. Expected values: [latest], +or anything in the format of [MAJOR_VERSION.MINOR_VERSION.PATCH_VERSION] or [MAJOR_VERSION.*.*]." + fi +} + +handle_debian() { + declare -a EXTRA_OPTS=() + [[ "${VERBOSE}" == 'true' ]] && EXTRA_OPTS+=(-oDebug::pkgAcquire::Worker=1) + + add_repo() { + [[ -n "${REPO_CODENAME:-}" ]] || lsb_release -v >/dev/null 2>&1 || { \ + apt-get update; apt-get -y install lsb-release; CHANGED=1; + } + [[ "$(dpkg -l apt-transport-https 2>&1 | grep -o '^[a-z][a-z]')" == 'ii' ]] || { \ + ${DRY_RUN} apt-get update; ${DRY_RUN} apt-get -y install apt-transport-https; CHANGED=1; + } + [[ "$(dpkg -l ca-certificates 2>&1 | grep -o '^[a-z][a-z]')" == 'ii' ]] || { \ + ${DRY_RUN} apt-get update; ${DRY_RUN} apt-get -y install ca-certificates; CHANGED=1; + } + local CODENAME="${REPO_CODENAME:-"$(lsb_release -sc)"}" + local REPO_NAME="google-cloud-ops-agent-${CODENAME}-${REPO_SUFFIX:-all}" + local REPO_DATA="deb https://${REPO_HOST}/apt ${REPO_NAME} main" + if ! cmp -s <<<"${REPO_DATA}" - /etc/apt/sources.list.d/google-cloud-ops-agent.list; then + echo "Adding agent repository for ${ID}." + ${DRY_RUN} tee <<<"${REPO_DATA}" /etc/apt/sources.list.d/google-cloud-ops-agent.list + ${DRY_RUN} curl --connect-timeout 5 -s -f "https://${REPO_HOST}/apt/doc/apt-key.gpg" \ + | ${DRY_RUN} apt-key add - + CHANGED=1 + fi + } + + remove_repo() { + if [[ -f /etc/apt/sources.list.d/google-cloud-ops-agent.list ]]; then + echo "Removing agent repository for ${ID}." + ${DRY_RUN} rm /etc/apt/sources.list.d/google-cloud-ops-agent.list + CHANGED=1 + fi + } + + expected_version_installed() { + [[ "$(dpkg -l "${AGENT_PACKAGE}" "${ADDITIONAL_PACKAGES[@]}" 2>&1 | grep -o '^[a-z][a-z]' | sort -u)" == 'ii' ]] || \ + return + if [[ -z "${AGENT_VERSION:-}" ]]; then + apt-get --dry-run install "${AGENT_PACKAGE}" "${ADDITIONAL_PACKAGES[@]}" \ + | grep -qo '^0 upgraded, 0 newly installed' + elif grep -qE '^[0-9]+\.\*\.\*$' <<<"${AGENT_VERSION}"; then + dpkg -l "${AGENT_PACKAGE}" | grep -qE "$AGENT_PACKAGE $AGENT_VERSION" && \ + apt-get --dry-run install "${AGENT_PACKAGE}" "${ADDITIONAL_PACKAGES[@]}" \ + | grep -qo '^0 upgraded, 0 newly installed' + else + dpkg -l "${AGENT_PACKAGE}" | grep -qE "$AGENT_PACKAGE $AGENT_VERSION" + fi + } + + install_agent() { + ${DRY_RUN} apt-get update || refresh_failed 'apt' "${ID}" + expected_version_installed || { \ + if [[ -n "${AGENT_VERSION:-}" ]]; then + # Differentiate `MAJOR_VERSION.MINOR_VERSION.PATCH_VERSION` from `MAJOR_VERSION.*.*`. + # apt package version format: e.g. 2.0.1~debian9.13. + if grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$' <<<"${AGENT_VERSION}"; then + AGENT_VERSION="=${AGENT_VERSION}~*" + else + AGENT_VERSION="=${AGENT_VERSION%.\*}" + fi + fi + ${DRY_RUN} apt-get -y --allow-downgrades "${EXTRA_OPTS[@]}" install "${AGENT_PACKAGE}${AGENT_VERSION}" \ + "${ADDITIONAL_PACKAGES[@]}" || fail "${AGENT_PACKAGE} ${ADDITIONAL_PACKAGES[*]} \ +installation failed." + echo "${AGENT_PACKAGE} ${ADDITIONAL_PACKAGES[*]} installation succeeded." + CHANGED=1 + } + } + + uninstall() { + local -a packages=("$@") + # Return early unless at least one package is installed. + dpkg -l "${packages[@]}" 2>&1 | grep -qo '^ii' || return + ${DRY_RUN} apt-get -y "${EXTRA_OPTS[@]}" remove "${packages[@]}" || \ + fail "${packages[*]} uninstallation failed." + echo "${packages[*]} uninstallation succeeded." + CHANGED=1 + } +} + +handle_rpm() { + declare -a EXTRA_OPTS=() + [[ "${VERBOSE}" == 'true' ]] && EXTRA_OPTS+=(-v) + + add_repo() { + local REPO_NAME="google-cloud-ops-agent-${CODENAME}-\$basearch-${REPO_SUFFIX:-all}" + local REPO_DATA="\ +[google-cloud-ops-agent] +name=Google Cloud Ops Agent Repository +baseurl=https://${REPO_HOST}/yum/repos/${REPO_NAME} +autorefresh=0 +enabled=1 +type=rpm-md +gpgcheck=1 +repo_gpgcheck=0 +gpgkey=https://${REPO_HOST}/yum/doc/yum-key.gpg + https://${REPO_HOST}/yum/doc/rpm-package-key.gpg" + if ! cmp -s <<<"${REPO_DATA}" - /etc/yum.repos.d/google-cloud-ops-agent.repo; then + echo "Adding agent repository for ${ID}." + ${DRY_RUN} tee <<<"${REPO_DATA}" /etc/yum.repos.d/google-cloud-ops-agent.repo + # After repo upgrades, CentOS7/RHEL7 won't pick up newly available packages + # until the cache is cleared. + ${DRY_RUN} rm -rf /var/cache/yum/*/*/google-cloud-ops-agent/ + CHANGED=1 + fi + } + + remove_repo() { + if [[ -f /etc/yum.repos.d/google-cloud-ops-agent.repo ]]; then + echo "Removing agent repository for ${ID}." + ${DRY_RUN} rm /etc/yum.repos.d/google-cloud-ops-agent.repo + CHANGED=1 + fi + } + + expected_version_installed() { + rpm -q "${AGENT_PACKAGE}" "${ADDITIONAL_PACKAGES[@]}" >/dev/null 2>&1 || return + if [[ -z "${AGENT_VERSION:-}" ]]; then + yum -y check-update "${AGENT_PACKAGE}" "${ADDITIONAL_PACKAGES[@]}" >/dev/null 2>&1 + elif grep -qE '^[0-9]+\.\*\.\*$' <<<"${AGENT_VERSION}"; then + CURRENT_VERSION="$(rpm -q --queryformat '%{VERSION}' "${AGENT_PACKAGE}")" + grep -qE "${AGENT_VERSION}" <<<"${CURRENT_VERSION}" && \ + yum -y check-update "${AGENT_PACKAGE}" "${ADDITIONAL_PACKAGES[@]}" >/dev/null 2>&1 + else + CURRENT_VERSION="$(rpm -q --queryformat '%{VERSION}' "${AGENT_PACKAGE}")" + [[ "${AGENT_VERSION}" == "${CURRENT_VERSION}" ]] + fi + } + + install_agent() { + expected_version_installed || { \ + ${DRY_RUN} yum -y list updates || refresh_failed 'yum' "${ID}" + local COMMAND='install' + if [[ -n "${AGENT_VERSION:-}" ]]; then + [[ -z "${CURRENT_VERSION:-}" ]] || \ + [[ "${AGENT_VERSION}" == "$(sort -rV <<<"${AGENT_VERSION}"$'\n'"${CURRENT_VERSION}" | head -1)" ]] || \ + COMMAND='downgrade' + # Differentiate `MAJOR_VERSION.MINOR_VERSION.PATCH_VERSION` from `MAJOR_VERSION.*.*`. + # yum package version format: e.g. 1.0.1-1.el8. + if grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$' <<<"${AGENT_VERSION}"; then + AGENT_VERSION="-${AGENT_VERSION}-1*" + else + AGENT_VERSION="-${AGENT_VERSION}" + fi + fi + ${DRY_RUN} yum -y "${EXTRA_OPTS[@]}" "${COMMAND}" "${AGENT_PACKAGE}${AGENT_VERSION}" \ + "${ADDITIONAL_PACKAGES[@]}" || fail "${AGENT_PACKAGE} ${ADDITIONAL_PACKAGES[*]} \ +installation failed." + echo "${AGENT_PACKAGE} ${ADDITIONAL_PACKAGES[*]} installation succeeded." + CHANGED=1 + } + } + + uninstall() { + local -a packages=("$@") + # Return early if none of the packages are installed. + rpm -q "${packages[@]}" | grep -qvE 'is not installed$' || return + ${DRY_RUN} yum -y "${EXTRA_OPTS[@]}" remove "${packages[@]}" || \ + fail "${packages[*]} uninstallation failed." + echo "${packages[*]} uninstallation succeeded." + CHANGED=1 + } +} + +handle_redhat() { + local MAJOR_VERSION="$(rpm --eval %{?rhel})" + CODENAME="el${MAJOR_VERSION}" + handle_rpm +} + +handle_suse() { + declare -a EXTRA_OPTS=() + [[ "${VERBOSE}" == 'true' ]] && EXTRA_OPTS+=(-vv) + + add_repo() { + local SUSE_VERSION=${VERSION_ID%%.*} + local CODENAME="sles${SUSE_VERSION}" + local REPO_NAME="google-cloud-ops-agent-${CODENAME}-\$basearch-${REPO_SUFFIX:-all}" + { + ${DRY_RUN} zypper --non-interactive refresh || { \ + echo >&2 'Could not refresh zypper repositories.'; \ + echo >&2 'This is not necessarily a fatal error; proceeding...'; \ + } + } | grep -qF 'Retrieving repository' || [[ -n "${DRY_RUN:-}" ]] && CHANGED=1 + local REPO_DATA="\ +[google-cloud-ops-agent] +name=Google Cloud Ops Agent Repository +baseurl=https://${REPO_HOST}/yum/repos/${REPO_NAME} +autorefresh=0 +enabled=1 +type=rpm-md +gpgkey=https://${REPO_HOST}/yum/doc/yum-key.gpg + https://${REPO_HOST}/yum/doc/rpm-package-key.gpg" + if ! cmp -s <<<"${REPO_DATA}" - /etc/zypp/repos.d/google-cloud-ops-agent.repo; then + echo "Adding agent repository for ${ID}." + ${DRY_RUN} tee <<<"${REPO_DATA}" /etc/zypp/repos.d/google-cloud-ops-agent.repo + CHANGED=1 + fi + local RPM_KEYS="$(rpm --query gpg-pubkey)" # Save the installed keys. + ${DRY_RUN} rpm --import "https://${REPO_HOST}/yum/doc/yum-key.gpg" "https://${REPO_HOST}/yum/doc/rpm-package-key.gpg" + if [[ -n "${DRY_RUN:-}" ]] || ! cmp --silent <<<"${RPM_KEYS}" - <(rpm --query gpg-pubkey); then + CHANGED=1 + fi + { + ${DRY_RUN} zypper --non-interactive --gpg-auto-import-keys refresh google-cloud-ops-agent || \ + refresh_failed 'zypper' "${ID}"; \ + } | grep -qF 'Retrieving repository' || [[ -n "${DRY_RUN:-}" ]] && CHANGED=1 + } + + remove_repo() { + if [[ -f /etc/zypp/repos.d/google-cloud-ops-agent.repo ]]; then + echo "Removing agent repository for ${ID}." + ${DRY_RUN} rm /etc/zypp/repos.d/google-cloud-ops-agent.repo + CHANGED=1 + fi + } + + expected_version_installed() { + rpm -q "${AGENT_PACKAGE}" "${ADDITIONAL_PACKAGES[@]}" >/dev/null 2>&1 || return + if [[ -z "${AGENT_VERSION:-}" ]]; then + zypper --non-interactive update --dry-run "${AGENT_PACKAGE}" "${ADDITIONAL_PACKAGES[@]}" \ + | grep -qE '^Nothing to do.' + elif grep -qE '^[0-9]+\.\*\.\*$' <<<"${AGENT_VERSION}"; then + rpm -q --queryformat '%{VERSION}' "${AGENT_PACKAGE}" | grep -qE "${AGENT_VERSION}" && \ + zypper --non-interactive update --dry-run "${AGENT_PACKAGE}" "${ADDITIONAL_PACKAGES[@]}" \ + | grep -qE '^Nothing to do.' + else + [[ "${AGENT_VERSION}" == "$(rpm -q --queryformat '%{VERSION}' "${AGENT_PACKAGE}")" ]] + fi + } + + install_agent() { + expected_version_installed || { \ + if [[ -n "${AGENT_VERSION:-}" ]]; then + # Differentiate `MAJOR_VERSION.MINOR_VERSION.PATCH_VERSION` from `MAJOR_VERSION.*.*`. + # zypper package version format: e.g. 1.0.6-1.sles15. + if grep -qE '^[0-9]+\.\*\.\*$' <<<"${AGENT_VERSION}"; then + AGENT_VERSION="<$(( ${AGENT_VERSION%%.*} + 1 ))" + else + AGENT_VERSION="=${AGENT_VERSION}" + fi + fi + ${DRY_RUN} zypper --non-interactive "${EXTRA_OPTS[@]}" install --oldpackage "${AGENT_PACKAGE}${AGENT_VERSION}" \ + "${ADDITIONAL_PACKAGES[@]}" || fail "${AGENT_PACKAGE} ${ADDITIONAL_PACKAGES[*]} \ +installation failed." + echo "${AGENT_PACKAGE} ${ADDITIONAL_PACKAGES[*]} installation succeeded." + CHANGED=1 + } + } + + uninstall() { + local -a packages=("$@") + # Return early if none of the packages are installed. + rpm -q "${packages[@]}" | grep -qvE 'is not installed$' || return + ${DRY_RUN} zypper --non-interactive "${EXTRA_OPTS[@]}" remove "${packages[@]}" || \ + fail "${packages[*]} uninstallation failed." + echo "${packages[*]} uninstallation succeeded." + CHANGED=1 + } +} + +save_configuration_files() { + local save_dir="/var/lib/google-cloud-ops-agent/saved_configs" + ${DRY_RUN} mkdir -p "${save_dir}" + ${DRY_RUN} cp -rp "$@" "${save_dir}" + echo "$* is now copied over to ${save_dir} folder." +} + +main() { + case "${ID:-}" in + debian|ubuntu) handle_debian ;; + rhel|centos) handle_redhat ;; + sles|opensuse-leap) handle_suse ;; + *) + # Fallback for systems lacking /etc/os-release. + if [[ -f /etc/debian_version ]]; then + ID='debian' + handle_debian + elif [[ -f /etc/redhat-release ]]; then + ID='rhel' + handle_redhat + elif [[ -f /etc/SuSE-release ]]; then + ID='sles' + handle_suse + else + fail "Unidentifiable or unsupported platform. See +${AGENT_SUPPORTED_URL} for a list of supported platforms." + fi + esac + + + if [[ " ${ACTIONS[*]} " == *\ uninstall-standalone-logging-agent\ * ]]; then + save_configuration_files "/etc/google-fluentd" + # This will also remove dependent packages, e.g. "google-fluentd-catch-all-config" or "google-fluentd-catch-all-config-structured". + uninstall "google-fluentd" + fi + if [[ " ${ACTIONS[*]} " == *\ uninstall-standalone-monitoring-agent\ * ]]; then + save_configuration_files "/etc/stackdriver" "/opt/stackdriver/collectd/etc" + uninstall "stackdriver-agent" + fi + if [[ " ${ACTIONS[*]} " == *\ add-repo\ * ]]; then + resolve_version + add_repo + fi + if [[ " ${ACTIONS[*]} " == *\ also-install\ * ]]; then + install_agent + elif [[ " ${ACTIONS[*]} " == *\ uninstall\ * ]]; then + save_configuration_files "/etc/google-cloud-ops-agent" + uninstall "${AGENT_PACKAGE}" "${ADDITIONAL_PACKAGES[@]}" + fi + if [[ " ${ACTIONS[*]} " == *\ remove-repo\ * ]]; then + remove_repo + fi + + if [[ "${CHANGED}" == 0 ]]; then + echo 'No changes made.' + fi + + if [[ -n "${DRY_RUN:-}" ]]; then + echo 'Finished dry run. This was only a simulation, remove the --dry-run flag +to perform an actual execution of the script.' + fi +} + +main "$@" diff --git a/gce/startup-script.sh b/gce/startup-script.sh index 2c1712bc..cc35d30f 100644 --- a/gce/startup-script.sh +++ b/gce/startup-script.sh @@ -12,26 +12,25 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Echo commands -set -v +# Echo commands and fail on error +set -ev # [START getting_started_gce_startup_script] -# Install Stackdriver logging agent -curl -sSO https://dl.google.com/cloudagents/install-logging-agent.sh -sudo bash install-logging-agent.sh - # Install or update needed software apt-get update apt-get install -yq git supervisor python python-pip python3-distutils pip install --upgrade pip virtualenv -# Account to own server process -useradd -m -d /home/pythonapp pythonapp - # Fetch source code export HOME=/root git clone https://github.com/GoogleCloudPlatform/getting-started-python.git /opt/app +# Install Cloud Ops Agent +sudo bash /opt/app/gce/add-google-cloud-ops-agent-repo.sh --also-install + +# Account to own server process +useradd -m -d /home/pythonapp pythonapp + # Python environment setup virtualenv -p python3 /opt/app/gce/env /bin/bash -c "source /opt/app/gce/env/bin/activate" From 879322dffcad49c0fb0b8d1a25e554b8f04480e9 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 8 Apr 2022 19:45:39 +0200 Subject: [PATCH 306/395] chore(deps): update dependency google-cloud-pubsub to v2.12.0 (#451) --- background/app/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 752a5556..dd720794 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ google-cloud-firestore==2.4.0 -google-cloud-pubsub==2.11.0 +google-cloud-pubsub==2.12.0 flask==2.1.1 From 3774a2f4c61954bc5022fe44beaba742bf529fb9 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 13 Apr 2022 21:19:37 +0200 Subject: [PATCH 307/395] chore(deps): update dependency google-cloud-storage to v2.3.0 (#452) --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 9565a5ee..36a85073 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,6 +1,6 @@ Flask==2.1.1 google-cloud-firestore==2.4.0 -google-cloud-storage==2.2.1 +google-cloud-storage==2.3.0 google-cloud-logging==3.0.0 google-cloud-error-reporting==1.5.1 gunicorn==20.1.0 From 2fc87efcecab7347a94cfcbcc28849d923cce679 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Thu, 28 Apr 2022 21:28:22 +0200 Subject: [PATCH 308/395] chore(deps): update dependency google-cloud-error-reporting to v1.5.2 (#453) --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 36a85073..904ce3db 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -2,6 +2,6 @@ Flask==2.1.1 google-cloud-firestore==2.4.0 google-cloud-storage==2.3.0 google-cloud-logging==3.0.0 -google-cloud-error-reporting==1.5.1 +google-cloud-error-reporting==1.5.2 gunicorn==20.1.0 six==1.16.0 From 2a41274e08fa25d42bac7e4dd04f83ffd547007b Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 29 Apr 2022 00:35:45 +0200 Subject: [PATCH 309/395] chore(deps): update dependency flask to v2.1.2 (#456) Co-authored-by: Anthonios Partheniou --- authenticating-users/requirements.txt | 2 +- background/app/requirements.txt | 2 +- bookshelf/requirements.txt | 2 +- gce/requirements.txt | 2 +- sessions/requirements.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index 858f4813..bee3173f 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -1,5 +1,5 @@ # [START getting_started_requirements] -Flask==2.1.1 +Flask==2.1.2 cryptography==36.0.2 python-jose[cryptography]==3.3.0 requests==2.27.1 diff --git a/background/app/requirements.txt b/background/app/requirements.txt index dd720794..a6137465 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ google-cloud-firestore==2.4.0 google-cloud-pubsub==2.12.0 -flask==2.1.1 +flask==2.1.2 diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 904ce3db..c149edce 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,4 +1,4 @@ -Flask==2.1.1 +Flask==2.1.2 google-cloud-firestore==2.4.0 google-cloud-storage==2.3.0 google-cloud-logging==3.0.0 diff --git a/gce/requirements.txt b/gce/requirements.txt index e45610a5..af36ca39 100644 --- a/gce/requirements.txt +++ b/gce/requirements.txt @@ -1,3 +1,3 @@ -flask==2.1.1 +flask==2.1.2 honcho==1.1.0 gunicorn==20.1.0 diff --git a/sessions/requirements.txt b/sessions/requirements.txt index 63f6ef09..697518f6 100644 --- a/sessions/requirements.txt +++ b/sessions/requirements.txt @@ -1,2 +1,2 @@ google-cloud-firestore==2.4.0 -flask==2.1.1 +flask==2.1.2 From 7cd2403fee6728c0d2bc580426c0126379775990 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 29 Apr 2022 02:09:06 +0200 Subject: [PATCH 310/395] chore(deps): update dependency pytest to v7.1.2 (#454) Co-authored-by: Anthonios Partheniou --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2f6c1476..57fd715d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ flake8==4.0.1 nox==2022.1.7 -pytest==7.1.1 +pytest==7.1.2 requests==2.27.1 From 997ec42ae1694c2348ab0c5661b1abb288af5fd5 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 29 Apr 2022 13:09:25 +0200 Subject: [PATCH 311/395] chore(deps): update dependency cryptography to v37 (#455) Co-authored-by: Anthonios Partheniou --- authenticating-users/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index bee3173f..6366b917 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -1,6 +1,6 @@ # [START getting_started_requirements] Flask==2.1.2 -cryptography==36.0.2 +cryptography==37.0.1 python-jose[cryptography]==3.3.0 requests==2.27.1 # [END getting_started_requirements] From ec80d421e4c6d4f856b34371482f75e284522ab4 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Thu, 19 May 2022 15:01:35 +0200 Subject: [PATCH 312/395] chore(deps): update dependency cryptography to v37.0.2 (#457) --- authenticating-users/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index 6366b917..8a78372b 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -1,6 +1,6 @@ # [START getting_started_requirements] Flask==2.1.2 -cryptography==37.0.1 +cryptography==37.0.2 python-jose[cryptography]==3.3.0 requests==2.27.1 # [END getting_started_requirements] From 1d87fac2de5c359deea3c97b4a8f07ddba0dd87c Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Thu, 19 May 2022 23:58:44 +0200 Subject: [PATCH 313/395] chore(deps): update dependency google-cloud-translate to v3.7.3 (#459) --- background/function/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/function/requirements.txt b/background/function/requirements.txt index f45b8bf1..ad44c2ec 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-translate==3.7.2 +google-cloud-translate==3.7.3 google-cloud-firestore==2.4.0 From e0148b540847fd708ca31b38b23ff68501d8d9a7 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 20 May 2022 00:33:27 +0200 Subject: [PATCH 314/395] chore(deps): update dependency google-cloud-pubsub to v2.12.1 (#458) Co-authored-by: Anthonios Partheniou --- background/app/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index a6137465..6579c4ea 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ google-cloud-firestore==2.4.0 -google-cloud-pubsub==2.12.0 +google-cloud-pubsub==2.12.1 flask==2.1.2 From f0a88a5819f75576b3aa3c8c6b8011c138fde32f Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 20 May 2022 00:38:11 +0200 Subject: [PATCH 315/395] chore(deps): update dependency google-cloud-firestore to v2.5.0 (#460) Co-authored-by: Dan Lee <71398022+dandhlee@users.noreply.github.com> --- background/app/requirements.txt | 2 +- background/function/requirements.txt | 2 +- bookshelf/requirements.txt | 2 +- sessions/requirements.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 6579c4ea..59fe3a5f 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ -google-cloud-firestore==2.4.0 +google-cloud-firestore==2.5.0 google-cloud-pubsub==2.12.1 flask==2.1.2 diff --git a/background/function/requirements.txt b/background/function/requirements.txt index ad44c2ec..6823a2ea 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ google-cloud-translate==3.7.3 -google-cloud-firestore==2.4.0 +google-cloud-firestore==2.5.0 diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index c149edce..5e6bcf61 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,5 +1,5 @@ Flask==2.1.2 -google-cloud-firestore==2.4.0 +google-cloud-firestore==2.5.0 google-cloud-storage==2.3.0 google-cloud-logging==3.0.0 google-cloud-error-reporting==1.5.2 diff --git a/sessions/requirements.txt b/sessions/requirements.txt index 697518f6..7d77b819 100644 --- a/sessions/requirements.txt +++ b/sessions/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-firestore==2.4.0 +google-cloud-firestore==2.5.0 flask==2.1.2 From 018761a6096b7f7291c43d0f445433490e3d8dff Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 25 May 2022 00:49:11 +0200 Subject: [PATCH 316/395] chore(deps): update dependency google-cloud-logging to v3.1.1 (#461) --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 5e6bcf61..56e6354d 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,7 +1,7 @@ Flask==2.1.2 google-cloud-firestore==2.5.0 google-cloud-storage==2.3.0 -google-cloud-logging==3.0.0 +google-cloud-logging==3.1.1 google-cloud-error-reporting==1.5.2 gunicorn==20.1.0 six==1.16.0 From 170a30e97be6f35bf8eb0b32252fb470225e2b9b Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 31 May 2022 07:38:01 +0200 Subject: [PATCH 317/395] chore(deps): update dependency google-cloud-firestore to v2.5.1 (#462) --- background/app/requirements.txt | 2 +- background/function/requirements.txt | 2 +- bookshelf/requirements.txt | 2 +- sessions/requirements.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 59fe3a5f..8c80151f 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ -google-cloud-firestore==2.5.0 +google-cloud-firestore==2.5.1 google-cloud-pubsub==2.12.1 flask==2.1.2 diff --git a/background/function/requirements.txt b/background/function/requirements.txt index 6823a2ea..1640d7bd 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ google-cloud-translate==3.7.3 -google-cloud-firestore==2.5.0 +google-cloud-firestore==2.5.1 diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 56e6354d..1d9f0215 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,5 +1,5 @@ Flask==2.1.2 -google-cloud-firestore==2.5.0 +google-cloud-firestore==2.5.1 google-cloud-storage==2.3.0 google-cloud-logging==3.1.1 google-cloud-error-reporting==1.5.2 diff --git a/sessions/requirements.txt b/sessions/requirements.txt index 7d77b819..d99e385b 100644 --- a/sessions/requirements.txt +++ b/sessions/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-firestore==2.5.0 +google-cloud-firestore==2.5.1 flask==2.1.2 From 1e40c48e57b694f08c711b285bbdbe9b1be87873 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 1 Jun 2022 21:12:38 +0200 Subject: [PATCH 318/395] chore(deps): update dependency google-cloud-firestore to v2.5.2 (#463) --- background/app/requirements.txt | 2 +- background/function/requirements.txt | 2 +- bookshelf/requirements.txt | 2 +- sessions/requirements.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 8c80151f..31e6b551 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ -google-cloud-firestore==2.5.1 +google-cloud-firestore==2.5.2 google-cloud-pubsub==2.12.1 flask==2.1.2 diff --git a/background/function/requirements.txt b/background/function/requirements.txt index 1640d7bd..074a8491 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ google-cloud-translate==3.7.3 -google-cloud-firestore==2.5.1 +google-cloud-firestore==2.5.2 diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 1d9f0215..3ff2e7bc 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,5 +1,5 @@ Flask==2.1.2 -google-cloud-firestore==2.5.1 +google-cloud-firestore==2.5.2 google-cloud-storage==2.3.0 google-cloud-logging==3.1.1 google-cloud-error-reporting==1.5.2 diff --git a/sessions/requirements.txt b/sessions/requirements.txt index d99e385b..5a16352a 100644 --- a/sessions/requirements.txt +++ b/sessions/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-firestore==2.5.1 +google-cloud-firestore==2.5.2 flask==2.1.2 From 192fe5f4520127c2a2f4faa9866a48d2203b43d7 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 7 Jun 2022 16:28:27 +0200 Subject: [PATCH 319/395] chore(deps): update dependency google-cloud-pubsub to v2.13.0 (#466) --- background/app/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 31e6b551..44445215 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ google-cloud-firestore==2.5.2 -google-cloud-pubsub==2.12.1 +google-cloud-pubsub==2.13.0 flask==2.1.2 From 02679d8659cff8bb7c95cd993da2edcaba0de855 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 7 Jun 2022 18:16:25 +0200 Subject: [PATCH 320/395] chore(deps): update dependency google-cloud-logging to v3.1.2 (#464) Co-authored-by: Dan Lee <71398022+dandhlee@users.noreply.github.com> --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 3ff2e7bc..24a96f15 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,7 +1,7 @@ Flask==2.1.2 google-cloud-firestore==2.5.2 google-cloud-storage==2.3.0 -google-cloud-logging==3.1.1 +google-cloud-logging==3.1.2 google-cloud-error-reporting==1.5.2 gunicorn==20.1.0 six==1.16.0 From cfa95de180204414c69b3fcc41c72edd16ac3424 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 7 Jun 2022 18:25:39 +0200 Subject: [PATCH 321/395] chore(deps): update dependency google-cloud-translate to v3.7.4 (#465) Co-authored-by: Dan Lee <71398022+dandhlee@users.noreply.github.com> --- background/function/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/function/requirements.txt b/background/function/requirements.txt index 074a8491..85815456 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-translate==3.7.3 +google-cloud-translate==3.7.4 google-cloud-firestore==2.5.2 From 91e66291299be0b247d14aacc4ba81cb449e5b2a Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 7 Jun 2022 19:00:05 +0200 Subject: [PATCH 322/395] chore(deps): update dependency google-cloud-error-reporting to v1.5.3 (#467) Co-authored-by: Dan Lee <71398022+dandhlee@users.noreply.github.com> --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 24a96f15..57f1ce55 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,7 +1,7 @@ Flask==2.1.2 google-cloud-firestore==2.5.2 google-cloud-storage==2.3.0 +google-cloud-error-reporting==1.5.3 google-cloud-logging==3.1.2 -google-cloud-error-reporting==1.5.2 gunicorn==20.1.0 six==1.16.0 From a98c1ac9cc2c8559f6a2eccf8d601874b3f2f001 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Thu, 9 Jun 2022 21:08:27 +0200 Subject: [PATCH 323/395] chore(deps): update dependency requests to v2.28.0 (#471) --- authenticating-users/requirements.txt | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index 8a78372b..e634f86c 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -2,5 +2,5 @@ Flask==2.1.2 cryptography==37.0.2 python-jose[cryptography]==3.3.0 -requests==2.27.1 +requests==2.28.0 # [END getting_started_requirements] diff --git a/requirements.txt b/requirements.txt index 57fd715d..bcaf9e05 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ flake8==4.0.1 nox==2022.1.7 pytest==7.1.2 -requests==2.27.1 +requests==2.28.0 From e4df71119236380ffbe0eb172c30dcb6d899423a Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 29 Jun 2022 19:37:01 +0200 Subject: [PATCH 324/395] chore(deps): update dependency requests to v2.28.1 (#474) --- authenticating-users/requirements.txt | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index e634f86c..45bf2446 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -2,5 +2,5 @@ Flask==2.1.2 cryptography==37.0.2 python-jose[cryptography]==3.3.0 -requests==2.28.0 +requests==2.28.1 # [END getting_started_requirements] diff --git a/requirements.txt b/requirements.txt index bcaf9e05..9197eb09 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ flake8==4.0.1 nox==2022.1.7 pytest==7.1.2 -requests==2.28.0 +requests==2.28.1 From 81db1b222c00afd8bfc322ade27007c28f387b58 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 8 Jul 2022 17:17:20 +0200 Subject: [PATCH 325/395] chore(deps): update dependency cryptography to v37.0.4 (#475) --- authenticating-users/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index 45bf2446..4ba6f3d9 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -1,6 +1,6 @@ # [START getting_started_requirements] Flask==2.1.2 -cryptography==37.0.2 +cryptography==37.0.4 python-jose[cryptography]==3.3.0 requests==2.28.1 # [END getting_started_requirements] From 16272d919fd8d2e58625dfe1879fc1e4b1f33796 Mon Sep 17 00:00:00 2001 From: Dan Lee <71398022+dandhlee@users.noreply.github.com> Date: Fri, 8 Jul 2022 11:37:59 -0400 Subject: [PATCH 326/395] chore: update owners (#469) * chore: update owners * update CODEOWNERS Co-authored-by: Anthonios Partheniou --- .github/sync-repo-settings.yaml | 2 +- CODEOWNERS | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/sync-repo-settings.yaml b/.github/sync-repo-settings.yaml index 4dfda801..7d8eca8e 100644 --- a/.github/sync-repo-settings.yaml +++ b/.github/sync-repo-settings.yaml @@ -36,5 +36,5 @@ permissionRules: - team: yoshi-admins # Access level required, one of push|pull|admin permission: admin - - team: python-samples-owners + - team: python-samples-reviewers permission: admin diff --git a/CODEOWNERS b/CODEOWNERS index 0480d02a..0dfefd93 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -7,4 +7,4 @@ # The python-samples-owners team is the default owner for anything not # explicitly taken by someone else. -* @GoogleCloudPlatform/python-samples-owners +* @GoogleCloudPlatform/python-samples-reviewers From 9eaafc6ce93066419504e21a39ff7816b618264a Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 8 Jul 2022 17:46:13 +0200 Subject: [PATCH 327/395] chore(deps): update dependency google-cloud-storage to v2.4.0 (#470) Co-authored-by: Leah E. Cole <6719667+leahecole@users.noreply.github.com> Co-authored-by: Anthonios Partheniou --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 57f1ce55..f11f1f27 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,6 +1,6 @@ Flask==2.1.2 google-cloud-firestore==2.5.2 -google-cloud-storage==2.3.0 +google-cloud-storage==2.4.0 google-cloud-error-reporting==1.5.3 google-cloud-logging==3.1.2 gunicorn==20.1.0 From 9a5888bd2a569c183d6056686946e0c4e5a7e0dd Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 8 Jul 2022 17:53:27 +0200 Subject: [PATCH 328/395] chore(deps): update dependency google-cloud-firestore to v2.5.3 (#472) Co-authored-by: Anthonios Partheniou --- background/app/requirements.txt | 2 +- background/function/requirements.txt | 2 +- bookshelf/requirements.txt | 2 +- sessions/requirements.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 44445215..188b62ac 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ -google-cloud-firestore==2.5.2 +google-cloud-firestore==2.5.3 google-cloud-pubsub==2.13.0 flask==2.1.2 diff --git a/background/function/requirements.txt b/background/function/requirements.txt index 85815456..47d9b356 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ google-cloud-translate==3.7.4 -google-cloud-firestore==2.5.2 +google-cloud-firestore==2.5.3 diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index f11f1f27..7d4da7be 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,5 +1,5 @@ Flask==2.1.2 -google-cloud-firestore==2.5.2 +google-cloud-firestore==2.5.3 google-cloud-storage==2.4.0 google-cloud-error-reporting==1.5.3 google-cloud-logging==3.1.2 diff --git a/sessions/requirements.txt b/sessions/requirements.txt index 5a16352a..48998818 100644 --- a/sessions/requirements.txt +++ b/sessions/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-firestore==2.5.2 +google-cloud-firestore==2.5.3 flask==2.1.2 From 30031956ad87c99371fc16f7ba0613cf6593121e Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 8 Jul 2022 17:59:41 +0200 Subject: [PATCH 329/395] chore(deps): update dependency google-cloud-pubsub to v2.13.1 (#476) --- background/app/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 188b62ac..10775c87 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ google-cloud-firestore==2.5.3 -google-cloud-pubsub==2.13.0 +google-cloud-pubsub==2.13.1 flask==2.1.2 From d041d14ca31522021da9296add27ceca617d056d Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 12 Jul 2022 19:29:08 +0200 Subject: [PATCH 330/395] chore(deps): update dependency google-cloud-logging to v3.2.0 (#478) --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 7d4da7be..fe8affaf 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -2,6 +2,6 @@ Flask==2.1.2 google-cloud-firestore==2.5.3 google-cloud-storage==2.4.0 google-cloud-error-reporting==1.5.3 -google-cloud-logging==3.1.2 +google-cloud-logging==3.2.0 gunicorn==20.1.0 six==1.16.0 From d1fc85eb9a108799e4f37ea07e47a3c4b2a747bd Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 15 Jul 2022 01:44:08 +0200 Subject: [PATCH 331/395] chore(deps): update dependency google-cloud-pubsub to v2.13.2 (#477) Co-authored-by: Anthonios Partheniou --- background/app/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 10775c87..28c37677 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ google-cloud-firestore==2.5.3 -google-cloud-pubsub==2.13.1 +google-cloud-pubsub==2.13.2 flask==2.1.2 From 2321dfb48ffbdb8f08be4ab1ef145892ecc6f4e1 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 3 Aug 2022 22:56:25 +0200 Subject: [PATCH 332/395] chore(deps): update dependency google-cloud-storage to v2.5.0 (#487) --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index fe8affaf..a1ef777d 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,6 +1,6 @@ Flask==2.1.2 google-cloud-firestore==2.5.3 -google-cloud-storage==2.4.0 +google-cloud-storage==2.5.0 google-cloud-error-reporting==1.5.3 google-cloud-logging==3.2.0 gunicorn==20.1.0 From 5c8f5e8a3c3e99ea37693045f667bd90d4a7ed6b Mon Sep 17 00:00:00 2001 From: Roger Martinez <31829545+rogerthatdev@users.noreply.github.com> Date: Wed, 3 Aug 2022 17:08:09 -0700 Subject: [PATCH 333/395] add Dockerfile to bookshelf (#486) * add Dockerfile to bookshelf * use gunicorn entrypoint * fix line length for kokoro tests Co-authored-by: Charles Engelke --- bookshelf/Dockerfile | 14 ++++++++++++++ bookshelf/main_test.py | 8 ++++---- 2 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 bookshelf/Dockerfile diff --git a/bookshelf/Dockerfile b/bookshelf/Dockerfile new file mode 100644 index 00000000..85a0632d --- /dev/null +++ b/bookshelf/Dockerfile @@ -0,0 +1,14 @@ +# Use the official Python image. +# https://hub.docker.com/_/python +FROM python:3.10-slim + +# Copy local code to the container image. +ENV APP_HOME /app +WORKDIR $APP_HOME +COPY . ./ + +# Install production dependencies. +RUN pip install --no-cache-dir -r requirements.txt + +# Run the web service on container startup. +ENTRYPOINT [ "gunicorn", "--bind", "0.0.0.0:8080", "main:app" ] \ No newline at end of file diff --git a/bookshelf/main_test.py b/bookshelf/main_test.py index 00c613fc..0206dfd5 100644 --- a/bookshelf/main_test.py +++ b/bookshelf/main_test.py @@ -42,11 +42,11 @@ def app(request): def firestore(): import firestore - """This fixture provides a modified version of the app's Firebase model that - tracks all created items and deletes them at the end of the test. + """This fixture provides a modified version of the app's Firebase model + that tracks all created items and deletes them at the end of the test. - Any tests that directly or indirectly interact with the database should use - this to ensure that resources are properly cleaned up. + Any tests that directly or indirectly interact with the database should + use this to ensure that resources are properly cleaned up. """ # Ensure no books exist before running the tests. This typically helps if From 269ce5d8f43ac4bf36fb12f4fc522d2c34785f10 Mon Sep 17 00:00:00 2001 From: Karl Weinmeister <11586922+kweinmeister@users.noreply.github.com> Date: Thu, 4 Aug 2022 17:21:31 -0500 Subject: [PATCH 334/395] chore: fix lint error in bookshelf/main_test.py (#490) * chore: fix lint error in bookshelf/main_test.py * linting --- bookshelf/main_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bookshelf/main_test.py b/bookshelf/main_test.py index 0206dfd5..249cba17 100644 --- a/bookshelf/main_test.py +++ b/bookshelf/main_test.py @@ -40,8 +40,6 @@ def app(request): @pytest.fixture def firestore(): - - import firestore """This fixture provides a modified version of the app's Firebase model that tracks all created items and deletes them at the end of the test. @@ -49,6 +47,8 @@ def firestore(): use this to ensure that resources are properly cleaned up. """ + import firestore + # Ensure no books exist before running the tests. This typically helps if # tests somehow left the database in a bad state. delete_all_books(firestore) From 13b2ce263d5055e4ed1246ffb27e2fa320f94947 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 5 Aug 2022 19:56:32 +0200 Subject: [PATCH 335/395] chore(deps): update dependency google-cloud-firestore to v2.6.0 (#485) --- background/app/requirements.txt | 2 +- background/function/requirements.txt | 2 +- bookshelf/requirements.txt | 2 +- sessions/requirements.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 28c37677..6c133018 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ -google-cloud-firestore==2.5.3 +google-cloud-firestore==2.6.0 google-cloud-pubsub==2.13.2 flask==2.1.2 diff --git a/background/function/requirements.txt b/background/function/requirements.txt index 47d9b356..d1eb2d6c 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ google-cloud-translate==3.7.4 -google-cloud-firestore==2.5.3 +google-cloud-firestore==2.6.0 diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index a1ef777d..d8a02838 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,5 +1,5 @@ Flask==2.1.2 -google-cloud-firestore==2.5.3 +google-cloud-firestore==2.6.0 google-cloud-storage==2.5.0 google-cloud-error-reporting==1.5.3 google-cloud-logging==3.2.0 diff --git a/sessions/requirements.txt b/sessions/requirements.txt index 48998818..266ca2bb 100644 --- a/sessions/requirements.txt +++ b/sessions/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-firestore==2.5.3 +google-cloud-firestore==2.6.0 flask==2.1.2 From c29cdf0a710f7aeb4d80ce7f9f920683c97349a0 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 5 Aug 2022 19:57:29 +0200 Subject: [PATCH 336/395] chore(deps): update dependency flake8 to v5 (#483) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 9197eb09..4f5520b0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -flake8==4.0.1 +flake8==5.0.4 nox==2022.1.7 pytest==7.1.2 requests==2.28.1 From e5b726a9ab542459c87a834c7c0b7a14b2855239 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 5 Aug 2022 19:58:09 +0200 Subject: [PATCH 337/395] chore(deps): update dependency google-cloud-error-reporting to v1.6.0 (#481) --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index d8a02838..64530b17 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,7 +1,7 @@ Flask==2.1.2 google-cloud-firestore==2.6.0 google-cloud-storage==2.5.0 -google-cloud-error-reporting==1.5.3 +google-cloud-error-reporting==1.6.0 google-cloud-logging==3.2.0 gunicorn==20.1.0 six==1.16.0 From 5af7a3f987260159955e9a3268855017092e4c8a Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 5 Aug 2022 20:06:53 +0200 Subject: [PATCH 338/395] chore(deps): update dependency google-cloud-translate to v3.8.0 (#482) --- background/function/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/function/requirements.txt b/background/function/requirements.txt index d1eb2d6c..50980182 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-translate==3.7.4 +google-cloud-translate==3.8.0 google-cloud-firestore==2.6.0 From ac75918d8f66f3ca807dd9aa4f7c885d7d5e7d2f Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 5 Aug 2022 20:07:04 +0200 Subject: [PATCH 339/395] chore(deps): update dependency google-cloud-logging to v3.2.1 (#480) --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 64530b17..7f0799da 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -2,6 +2,6 @@ Flask==2.1.2 google-cloud-firestore==2.6.0 google-cloud-storage==2.5.0 google-cloud-error-reporting==1.6.0 -google-cloud-logging==3.2.0 +google-cloud-logging==3.2.1 gunicorn==20.1.0 six==1.16.0 From 4ad8255e26abc15b2b5043e88c957d081fbd2184 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 5 Aug 2022 20:15:54 +0200 Subject: [PATCH 340/395] chore(deps): update dependency flask to v2.2.1 (#479) --- authenticating-users/requirements.txt | 2 +- background/app/requirements.txt | 2 +- bookshelf/requirements.txt | 2 +- gce/requirements.txt | 2 +- sessions/requirements.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index 4ba6f3d9..91a30ef8 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -1,5 +1,5 @@ # [START getting_started_requirements] -Flask==2.1.2 +Flask==2.2.1 cryptography==37.0.4 python-jose[cryptography]==3.3.0 requests==2.28.1 diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 6c133018..78ae15d3 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ google-cloud-firestore==2.6.0 google-cloud-pubsub==2.13.2 -flask==2.1.2 +flask==2.2.1 diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 7f0799da..1aac51f1 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,4 +1,4 @@ -Flask==2.1.2 +Flask==2.2.1 google-cloud-firestore==2.6.0 google-cloud-storage==2.5.0 google-cloud-error-reporting==1.6.0 diff --git a/gce/requirements.txt b/gce/requirements.txt index af36ca39..ec7db17f 100644 --- a/gce/requirements.txt +++ b/gce/requirements.txt @@ -1,3 +1,3 @@ -flask==2.1.2 +flask==2.2.1 honcho==1.1.0 gunicorn==20.1.0 diff --git a/sessions/requirements.txt b/sessions/requirements.txt index 266ca2bb..e62afc50 100644 --- a/sessions/requirements.txt +++ b/sessions/requirements.txt @@ -1,2 +1,2 @@ google-cloud-firestore==2.6.0 -flask==2.1.2 +flask==2.2.1 From 12201a405db76b82fa115dee676c77f99e3dec83 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 5 Aug 2022 20:22:22 +0200 Subject: [PATCH 341/395] chore(deps): update dependency google-cloud-pubsub to v2.13.4 (#484) --- background/app/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 78ae15d3..b46ec30b 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ google-cloud-firestore==2.6.0 -google-cloud-pubsub==2.13.2 +google-cloud-pubsub==2.13.4 flask==2.2.1 From 5eba607563ab5a93f4326e56e9ba46b83d3b0592 Mon Sep 17 00:00:00 2001 From: Charles Engelke Date: Mon, 8 Aug 2022 18:12:03 -0700 Subject: [PATCH 342/395] test: add tests for authenticating-users and gce samples (#492) * Feature: add test * Fix: more tests --- authenticating-users/main_test.py | 41 ++++++++++++++++++++++ authenticating-users/requirements-test.txt | 1 + gce/main_test.py | 25 +++++++++++++ 3 files changed, 67 insertions(+) create mode 100644 authenticating-users/main_test.py create mode 100644 authenticating-users/requirements-test.txt create mode 100644 gce/main_test.py diff --git a/authenticating-users/main_test.py b/authenticating-users/main_test.py new file mode 100644 index 00000000..38b934b6 --- /dev/null +++ b/authenticating-users/main_test.py @@ -0,0 +1,41 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import main + + +def fake_validate(assertion): + if assertion == "Valid": + return "nobody@example.com", "user0001" + else: + return None, None + + +main.validate_assertion = fake_validate + + +def test_home_page(): + client = main.app.test_client() + + # Good request check + r = client.get("/", headers={"X-Goog-IAP-JWT-Assertion": "Valid"}) + assert "nobody@example.com" in r.text + + # Missing header check + r = client.get("/") + assert "None" in r.text + + # Bad header check + r = client.get("/", headers={"X-Goog-IAP-JWT-Assertion": "Not Valid"}) + assert "None" in r.text diff --git a/authenticating-users/requirements-test.txt b/authenticating-users/requirements-test.txt new file mode 100644 index 00000000..6a3d7bca --- /dev/null +++ b/authenticating-users/requirements-test.txt @@ -0,0 +1 @@ +pytest==7.1.2 \ No newline at end of file diff --git a/gce/main_test.py b/gce/main_test.py new file mode 100644 index 00000000..6d1f887b --- /dev/null +++ b/gce/main_test.py @@ -0,0 +1,25 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import main + + +def test_hello(): + main.app.testing = True + client = main.app.test_client() + + r = client.get("/") + assert r.status_code == 200 + response_text = r.data.decode("utf-8") + assert "Hello, world!" in response_text From 98f635f42330223895c618280a2e2c86065650e1 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 9 Aug 2022 03:26:23 +0200 Subject: [PATCH 343/395] chore(deps): update dependency nox to v2022.8.7 (#491) Co-authored-by: Anthonios Partheniou --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4f5520b0..fdc61766 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ flake8==5.0.4 -nox==2022.1.7 +nox==2022.8.7 pytest==7.1.2 requests==2.28.1 From d32a7d394903402675cb827499371960504b92a6 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 12 Aug 2022 17:54:27 +0200 Subject: [PATCH 344/395] chore(deps): update dependency flask to v2.2.2 (#493) Co-authored-by: Anthonios Partheniou --- authenticating-users/requirements.txt | 2 +- background/app/requirements.txt | 2 +- bookshelf/requirements.txt | 2 +- gce/requirements.txt | 2 +- sessions/requirements.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index 91a30ef8..b4e76b98 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -1,5 +1,5 @@ # [START getting_started_requirements] -Flask==2.2.1 +Flask==2.2.2 cryptography==37.0.4 python-jose[cryptography]==3.3.0 requests==2.28.1 diff --git a/background/app/requirements.txt b/background/app/requirements.txt index b46ec30b..e86729e0 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ google-cloud-firestore==2.6.0 google-cloud-pubsub==2.13.4 -flask==2.2.1 +flask==2.2.2 diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 1aac51f1..10047d4c 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,4 +1,4 @@ -Flask==2.2.1 +Flask==2.2.2 google-cloud-firestore==2.6.0 google-cloud-storage==2.5.0 google-cloud-error-reporting==1.6.0 diff --git a/gce/requirements.txt b/gce/requirements.txt index ec7db17f..a39a519c 100644 --- a/gce/requirements.txt +++ b/gce/requirements.txt @@ -1,3 +1,3 @@ -flask==2.2.1 +flask==2.2.2 honcho==1.1.0 gunicorn==20.1.0 diff --git a/sessions/requirements.txt b/sessions/requirements.txt index e62afc50..e2c2eade 100644 --- a/sessions/requirements.txt +++ b/sessions/requirements.txt @@ -1,2 +1,2 @@ google-cloud-firestore==2.6.0 -flask==2.2.1 +flask==2.2.2 From 4e979cc3e2a611674603b0d87fd6560db5db17fa Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 12 Aug 2022 18:04:07 +0200 Subject: [PATCH 345/395] chore(deps): update dependency google-cloud-pubsub to v2.13.6 (#494) --- background/app/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index e86729e0..ed529643 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ google-cloud-firestore==2.6.0 -google-cloud-pubsub==2.13.4 +google-cloud-pubsub==2.13.6 flask==2.2.2 From bb90c15d8a1f47e08c6e021b0f303d7a8de5f9be Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 23 Aug 2022 17:07:20 +0200 Subject: [PATCH 346/395] chore(deps): update dependency google-cloud-error-reporting to v1.6.1 (#498) --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 10047d4c..e6c9690a 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,7 +1,7 @@ Flask==2.2.2 google-cloud-firestore==2.6.0 google-cloud-storage==2.5.0 -google-cloud-error-reporting==1.6.0 +google-cloud-error-reporting==1.6.1 google-cloud-logging==3.2.1 gunicorn==20.1.0 six==1.16.0 From cff8182879131597cb216045037e188d86cd3ca2 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 23 Aug 2022 17:18:40 +0200 Subject: [PATCH 347/395] chore(deps): update dependency google-cloud-logging to v3.2.2 (#497) Co-authored-by: Anthonios Partheniou --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index e6c9690a..46bdb3d9 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -2,6 +2,6 @@ Flask==2.2.2 google-cloud-firestore==2.6.0 google-cloud-storage==2.5.0 google-cloud-error-reporting==1.6.1 -google-cloud-logging==3.2.1 +google-cloud-logging==3.2.2 gunicorn==20.1.0 six==1.16.0 From 9343648338928a3c790e22cf944b681d964b855d Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 23 Aug 2022 20:22:26 +0200 Subject: [PATCH 348/395] chore(deps): update dependency google-cloud-firestore to v2.6.1 (#496) Co-authored-by: Anthonios Partheniou --- background/app/requirements.txt | 2 +- background/function/requirements.txt | 2 +- bookshelf/requirements.txt | 2 +- sessions/requirements.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index ed529643..e30787b4 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ -google-cloud-firestore==2.6.0 +google-cloud-firestore==2.6.1 google-cloud-pubsub==2.13.6 flask==2.2.2 diff --git a/background/function/requirements.txt b/background/function/requirements.txt index 50980182..f33726d4 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ google-cloud-translate==3.8.0 -google-cloud-firestore==2.6.0 +google-cloud-firestore==2.6.1 diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 46bdb3d9..ec2eedaa 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,5 +1,5 @@ Flask==2.2.2 -google-cloud-firestore==2.6.0 +google-cloud-firestore==2.6.1 google-cloud-storage==2.5.0 google-cloud-error-reporting==1.6.1 google-cloud-logging==3.2.2 diff --git a/sessions/requirements.txt b/sessions/requirements.txt index e2c2eade..b15c3944 100644 --- a/sessions/requirements.txt +++ b/sessions/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-firestore==2.6.0 +google-cloud-firestore==2.6.1 flask==2.2.2 From 0bd820e58eb0afd8a284c8adc51a8a6e8862a333 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Fri, 26 Aug 2022 19:25:14 +0200 Subject: [PATCH 349/395] chore(deps): update dependency google-cloud-translate to v3.8.1 (#495) --- background/function/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/function/requirements.txt b/background/function/requirements.txt index f33726d4..c2b29d61 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-translate==3.8.0 +google-cloud-translate==3.8.1 google-cloud-firestore==2.6.1 From 495c5708cf6c5c9afc4c4546c0b4248ad43d97ac Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 6 Sep 2022 18:21:19 +0200 Subject: [PATCH 350/395] chore(deps): update dependency pytest to v7.1.3 (#499) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index fdc61766..472fe87f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ flake8==5.0.4 nox==2022.8.7 -pytest==7.1.2 +pytest==7.1.3 requests==2.28.1 From f084a293f75cbc3c865e9afba45e2f1b16ee2ff4 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 6 Sep 2022 23:42:27 +0200 Subject: [PATCH 351/395] chore(deps): update dependency google-cloud-translate to v3.8.2 (#500) --- background/function/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/function/requirements.txt b/background/function/requirements.txt index c2b29d61..b2b6c367 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-translate==3.8.1 +google-cloud-translate==3.8.2 google-cloud-firestore==2.6.1 From 9d9f841f30d9fce9af7185cf3154a9f6ff0ece44 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 7 Sep 2022 04:51:58 +0200 Subject: [PATCH 352/395] chore(deps): update dependency cryptography to v38 (#501) --- authenticating-users/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index b4e76b98..79960e82 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -1,6 +1,6 @@ # [START getting_started_requirements] Flask==2.2.2 -cryptography==37.0.4 +cryptography==38.0.0 python-jose[cryptography]==3.3.0 requests==2.28.1 # [END getting_started_requirements] From 9a25463990b4841e0ae2074bc3b33177213dab21 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 7 Sep 2022 17:31:27 +0200 Subject: [PATCH 353/395] chore(deps): update dependency cryptography to v38.0.1 (#502) --- authenticating-users/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index 79960e82..86671e1b 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -1,6 +1,6 @@ # [START getting_started_requirements] Flask==2.2.2 -cryptography==38.0.0 +cryptography==38.0.1 python-jose[cryptography]==3.3.0 requests==2.28.1 # [END getting_started_requirements] From fa318b7d57e4b34dd3949f08c4488d90ca232660 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 20 Sep 2022 17:27:14 +0200 Subject: [PATCH 354/395] chore(deps): update dependency google-cloud-firestore to v2.7.0 (#503) --- background/app/requirements.txt | 2 +- background/function/requirements.txt | 2 +- bookshelf/requirements.txt | 2 +- sessions/requirements.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index e30787b4..5626c6e4 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ -google-cloud-firestore==2.6.1 +google-cloud-firestore==2.7.0 google-cloud-pubsub==2.13.6 flask==2.2.2 diff --git a/background/function/requirements.txt b/background/function/requirements.txt index b2b6c367..2e934313 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ google-cloud-translate==3.8.2 -google-cloud-firestore==2.6.1 +google-cloud-firestore==2.7.0 diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index ec2eedaa..97fd2826 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,5 +1,5 @@ Flask==2.2.2 -google-cloud-firestore==2.6.1 +google-cloud-firestore==2.7.0 google-cloud-storage==2.5.0 google-cloud-error-reporting==1.6.1 google-cloud-logging==3.2.2 diff --git a/sessions/requirements.txt b/sessions/requirements.txt index b15c3944..a7a29060 100644 --- a/sessions/requirements.txt +++ b/sessions/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-firestore==2.6.1 +google-cloud-firestore==2.7.0 flask==2.2.2 From 7315e7bcd1004a2e5085ee53cf0fddad9cd14605 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 4 Oct 2022 02:23:56 +0200 Subject: [PATCH 355/395] chore(deps): update dependency google-cloud-logging to v3.2.3 (#505) --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 97fd2826..99c9e4cc 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -2,6 +2,6 @@ Flask==2.2.2 google-cloud-firestore==2.7.0 google-cloud-storage==2.5.0 google-cloud-error-reporting==1.6.1 -google-cloud-logging==3.2.2 +google-cloud-logging==3.2.3 gunicorn==20.1.0 six==1.16.0 From 3858568bf84de06edf8a982b03f1dfa052900dc4 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 4 Oct 2022 03:30:18 +0200 Subject: [PATCH 356/395] chore(deps): update dependency google-cloud-pubsub to v2.13.7 (#504) Co-authored-by: Anthonios Partheniou --- background/app/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 5626c6e4..2b3c895f 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ google-cloud-firestore==2.7.0 -google-cloud-pubsub==2.13.6 +google-cloud-pubsub==2.13.7 flask==2.2.2 From 0fd610c9713df8e5df0e9b8fa5f2dfb1a9cefd73 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 4 Oct 2022 09:22:03 +0200 Subject: [PATCH 357/395] chore(deps): update dependency google-cloud-logging to v3.2.4 (#507) --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 99c9e4cc..b18c1921 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -2,6 +2,6 @@ Flask==2.2.2 google-cloud-firestore==2.7.0 google-cloud-storage==2.5.0 google-cloud-error-reporting==1.6.1 -google-cloud-logging==3.2.3 +google-cloud-logging==3.2.4 gunicorn==20.1.0 six==1.16.0 From 338267ca93d7f6118360a3843d79a33c4b71d504 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 4 Oct 2022 09:29:59 +0200 Subject: [PATCH 358/395] chore(deps): update dependency google-cloud-error-reporting to v1.6.2 (#508) --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index b18c1921..796c8136 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,7 +1,7 @@ Flask==2.2.2 google-cloud-firestore==2.7.0 google-cloud-storage==2.5.0 -google-cloud-error-reporting==1.6.1 +google-cloud-error-reporting==1.6.2 google-cloud-logging==3.2.4 gunicorn==20.1.0 six==1.16.0 From bad2030d79972a16d8aa582931a231b7170e8141 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 4 Oct 2022 09:49:30 +0200 Subject: [PATCH 359/395] chore(deps): update dependency google-cloud-translate to v3.8.3 (#509) Co-authored-by: Dan Lee <71398022+dandhlee@users.noreply.github.com> --- background/function/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/function/requirements.txt b/background/function/requirements.txt index 2e934313..fb6352b3 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-translate==3.8.2 +google-cloud-translate==3.8.3 google-cloud-firestore==2.7.0 From c886dfa4507437c35e29ae6350c6231c136cd176 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 4 Oct 2022 10:08:11 +0200 Subject: [PATCH 360/395] chore(deps): update dependency google-cloud-firestore to v2.7.1 (#506) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [google-cloud-firestore](https://togithub.com/googleapis/python-firestore) | `==2.7.0` -> `==2.7.1` | [![age](https://badges.renovateapi.com/packages/pypi/google-cloud-firestore/2.7.1/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/pypi/google-cloud-firestore/2.7.1/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/pypi/google-cloud-firestore/2.7.1/compatibility-slim/2.7.0)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/pypi/google-cloud-firestore/2.7.1/confidence-slim/2.7.0)](https://docs.renovatebot.com/merge-confidence/) | --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Renovate will not automatically rebase this PR, because other commits have been found. 🔕 **Ignore**: Close this PR and you won't be reminded about these updates again. --- - [ ] If you want to rebase/retry this PR, click this checkbox. ⚠ **Warning**: custom changes will be lost. --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://app.renovatebot.com/dashboard#github/GoogleCloudPlatform/getting-started-python). --- background/app/requirements.txt | 2 +- background/function/requirements.txt | 2 +- bookshelf/requirements.txt | 2 +- sessions/requirements.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 2b3c895f..9d9f6c33 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ -google-cloud-firestore==2.7.0 +google-cloud-firestore==2.7.1 google-cloud-pubsub==2.13.7 flask==2.2.2 diff --git a/background/function/requirements.txt b/background/function/requirements.txt index fb6352b3..22b8e209 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ google-cloud-translate==3.8.3 -google-cloud-firestore==2.7.0 +google-cloud-firestore==2.7.1 diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 796c8136..158377ba 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,5 +1,5 @@ Flask==2.2.2 -google-cloud-firestore==2.7.0 +google-cloud-firestore==2.7.1 google-cloud-storage==2.5.0 google-cloud-error-reporting==1.6.2 google-cloud-logging==3.2.4 diff --git a/sessions/requirements.txt b/sessions/requirements.txt index a7a29060..c4741e4f 100644 --- a/sessions/requirements.txt +++ b/sessions/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-firestore==2.7.0 +google-cloud-firestore==2.7.1 flask==2.2.2 From 33aa05167062c5c35d6254c1678506f145016692 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Wed, 26 Oct 2022 13:19:23 +0200 Subject: [PATCH 361/395] chore(deps): update python docker tag to v3.11 (#516) --- bookshelf/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/Dockerfile b/bookshelf/Dockerfile index 85a0632d..44c98b38 100644 --- a/bookshelf/Dockerfile +++ b/bookshelf/Dockerfile @@ -1,6 +1,6 @@ # Use the official Python image. # https://hub.docker.com/_/python -FROM python:3.10-slim +FROM python:3.11-slim # Copy local code to the container image. ENV APP_HOME /app From ceb653aee6d3deb3a1c0732dc6a2f37a3872099c Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Mon, 21 Nov 2022 17:50:26 +0100 Subject: [PATCH 362/395] chore(deps): update dependency google-cloud-firestore to v2.7.2 (#512) --- background/app/requirements.txt | 2 +- background/function/requirements.txt | 2 +- bookshelf/requirements.txt | 2 +- sessions/requirements.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 9d9f6c33..c8cef21d 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ -google-cloud-firestore==2.7.1 +google-cloud-firestore==2.7.2 google-cloud-pubsub==2.13.7 flask==2.2.2 diff --git a/background/function/requirements.txt b/background/function/requirements.txt index 22b8e209..f5ced284 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ google-cloud-translate==3.8.3 -google-cloud-firestore==2.7.1 +google-cloud-firestore==2.7.2 diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 158377ba..6ee0f912 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,5 +1,5 @@ Flask==2.2.2 -google-cloud-firestore==2.7.1 +google-cloud-firestore==2.7.2 google-cloud-storage==2.5.0 google-cloud-error-reporting==1.6.2 google-cloud-logging==3.2.4 diff --git a/sessions/requirements.txt b/sessions/requirements.txt index c4741e4f..05ccdd70 100644 --- a/sessions/requirements.txt +++ b/sessions/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-firestore==2.7.1 +google-cloud-firestore==2.7.2 flask==2.2.2 From 7caf839b41cdc73ad33989e4d3bfb31950f44c68 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Mon, 21 Nov 2022 19:10:47 +0100 Subject: [PATCH 363/395] chore(deps): update dependency nox to v2022.11.21 (#521) Co-authored-by: Dan Lee <71398022+dandhlee@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 472fe87f..8fce9f7b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ flake8==5.0.4 -nox==2022.8.7 +nox==2022.11.21 pytest==7.1.3 requests==2.28.1 From 1f8df71f9c0f60067320b2a1e69e169518b116e0 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Mon, 21 Nov 2022 19:30:20 +0100 Subject: [PATCH 364/395] chore(deps): update dependency google-cloud-storage to v2.6.0 (#520) Co-authored-by: Dan Lee <71398022+dandhlee@users.noreply.github.com> --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 6ee0f912..73c0329a 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,6 +1,6 @@ Flask==2.2.2 google-cloud-firestore==2.7.2 -google-cloud-storage==2.5.0 +google-cloud-storage==2.6.0 google-cloud-error-reporting==1.6.2 google-cloud-logging==3.2.4 gunicorn==20.1.0 From 65a13b0911ec4a8599ed53244c3e7af9b27420d9 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Mon, 21 Nov 2022 20:21:05 +0100 Subject: [PATCH 365/395] chore(deps): update dependency cryptography to v38.0.3 [security] (#519) Co-authored-by: Dan Lee <71398022+dandhlee@users.noreply.github.com> --- authenticating-users/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index 86671e1b..178952d6 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -1,6 +1,6 @@ # [START getting_started_requirements] Flask==2.2.2 -cryptography==38.0.1 +cryptography==38.0.3 python-jose[cryptography]==3.3.0 requests==2.28.1 # [END getting_started_requirements] From 012db56ec7334b72ade6dcf9706ca51ea8abec23 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Mon, 21 Nov 2022 20:35:03 +0100 Subject: [PATCH 366/395] chore(deps): update dependency google-cloud-error-reporting to v1.6.3 (#510) Co-authored-by: Dan Lee <71398022+dandhlee@users.noreply.github.com> --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 73c0329a..cf1f23ee 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,7 +1,7 @@ Flask==2.2.2 google-cloud-firestore==2.7.2 google-cloud-storage==2.6.0 -google-cloud-error-reporting==1.6.2 +google-cloud-error-reporting==1.6.3 google-cloud-logging==3.2.4 gunicorn==20.1.0 six==1.16.0 From e0066cb6278a278c189efbbdd208f3574aa315e0 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Mon, 21 Nov 2022 21:14:14 +0100 Subject: [PATCH 367/395] chore(deps): update dependency google-cloud-translate to v3.8.4 (#511) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [google-cloud-translate](https://togithub.com/googleapis/python-translate) | `==3.8.3` -> `==3.8.4` | [![age](https://badges.renovateapi.com/packages/pypi/google-cloud-translate/3.8.4/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/pypi/google-cloud-translate/3.8.4/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/pypi/google-cloud-translate/3.8.4/compatibility-slim/3.8.3)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/pypi/google-cloud-translate/3.8.4/confidence-slim/3.8.3)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
googleapis/python-translate ### [`v3.8.4`](https://togithub.com/googleapis/python-translate/blob/HEAD/CHANGELOG.md#​384-httpsgithubcomgoogleapispython-translatecomparev383v384-2022-10-07) [Compare Source](https://togithub.com/googleapis/python-translate/compare/v3.8.3...v3.8.4) ##### Bug Fixes - **deps:** Allow protobuf 3.19.5 ([#​457](https://togithub.com/googleapis/python-translate/issues/457)) ([e3ce689](https://togithub.com/googleapis/python-translate/commit/e3ce689f23cf1b081705ccb9c7fbd31d79cb44e5))
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://app.renovatebot.com/dashboard#github/GoogleCloudPlatform/getting-started-python). --- background/function/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/function/requirements.txt b/background/function/requirements.txt index f5ced284..82449ee6 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-translate==3.8.3 +google-cloud-translate==3.8.4 google-cloud-firestore==2.7.2 From 54649970446ef9c7e115e6ffdd3da02af08109e6 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Mon, 21 Nov 2022 21:26:13 +0100 Subject: [PATCH 368/395] chore(deps): update dependency google-cloud-logging to v3.2.5 (#513) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [google-cloud-logging](https://togithub.com/googleapis/python-logging) | `==3.2.4` -> `==3.2.5` | [![age](https://badges.renovateapi.com/packages/pypi/google-cloud-logging/3.2.5/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/pypi/google-cloud-logging/3.2.5/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/pypi/google-cloud-logging/3.2.5/compatibility-slim/3.2.4)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/pypi/google-cloud-logging/3.2.5/confidence-slim/3.2.4)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
googleapis/python-logging ### [`v3.2.5`](https://togithub.com/googleapis/python-logging/releases/tag/v3.2.5) [Compare Source](https://togithub.com/googleapis/python-logging/compare/v3.2.4...v3.2.5) ##### Bug Fixes - **deps:** Allow protobuf 3.19.5 ([#​644](https://togithub.com/googleapis/python-logging/issues/644)) ([12f3001](https://togithub.com/googleapis/python-logging/commit/12f3001d834e858dbbbcdaab5894ec25a14c6820))
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://app.renovatebot.com/dashboard#github/GoogleCloudPlatform/getting-started-python). --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index cf1f23ee..4f013fd2 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -2,6 +2,6 @@ Flask==2.2.2 google-cloud-firestore==2.7.2 google-cloud-storage==2.6.0 google-cloud-error-reporting==1.6.3 -google-cloud-logging==3.2.4 +google-cloud-logging==3.2.5 gunicorn==20.1.0 six==1.16.0 From 35a3a994c052e315fb3c0338beabcdca6076915d Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Mon, 21 Nov 2022 21:34:13 +0100 Subject: [PATCH 369/395] chore(deps): update dependency google-cloud-pubsub to v2.13.11 (#514) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [google-cloud-pubsub](https://togithub.com/googleapis/python-pubsub) | `==2.13.7` -> `==2.13.11` | [![age](https://badges.renovateapi.com/packages/pypi/google-cloud-pubsub/2.13.11/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/pypi/google-cloud-pubsub/2.13.11/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/pypi/google-cloud-pubsub/2.13.11/compatibility-slim/2.13.7)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/pypi/google-cloud-pubsub/2.13.11/confidence-slim/2.13.7)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
googleapis/python-pubsub ### [`v2.13.11`](https://togithub.com/googleapis/python-pubsub/blob/HEAD/CHANGELOG.md#​21311-httpsgithubcomgoogleapispython-pubsubcomparev21310v21311-2022-11-11) [Compare Source](https://togithub.com/googleapis/python-pubsub/compare/v2.13.10...v2.13.11) ##### Bug Fixes - Remove suboptimal logic in leasing behavior ([#​816](https://togithub.com/googleapis/python-pubsub/issues/816)) ([f067af3](https://togithub.com/googleapis/python-pubsub/commit/f067af348b8d3deb72981c58d942e887c0efb5ff)) ### [`v2.13.10`](https://togithub.com/googleapis/python-pubsub/blob/HEAD/CHANGELOG.md#​21310-httpsgithubcomgoogleapispython-pubsubcomparev2138v21310-2022-10-14) [Compare Source](https://togithub.com/googleapis/python-pubsub/compare/v2.13.9...v2.13.10) ##### Bug Fixes - Batch at most 1,000 ack ids per request ([#​802](https://togithub.com/googleapis/python-pubsub/issues/802)) ([4361e67](https://togithub.com/googleapis/python-pubsub/commit/4361e6735004a5600ee73979b99e6b9dd587c49b)) - **deps:** Allow protobuf 3.19.5 ([#​801](https://togithub.com/googleapis/python-pubsub/issues/801)) ([fa23503](https://togithub.com/googleapis/python-pubsub/commit/fa235033481783c2ec378b2a26b223bdff206461)) - Silence invalid_ack_id warnings for receipt modacks ([#​798](https://togithub.com/googleapis/python-pubsub/issues/798)) ([17feea5](https://togithub.com/googleapis/python-pubsub/commit/17feea5783f3a878b4dcfb3a8570585f7637378f)) ##### Miscellaneous Chores - release as 2.13.10 ([34f022b](https://togithub.com/googleapis/python-pubsub/commit/34f022b4ee62d53a193bc2babafad508e2f2540b)) ### [`v2.13.9`](https://togithub.com/googleapis/python-pubsub/releases/tag/v2.13.9) [Compare Source](https://togithub.com/googleapis/python-pubsub/compare/v2.13.8...v2.13.9) ##### Bug Fixes - **deps:** Allow protobuf 3.19.5 ([#​801](https://togithub.com/googleapis/python-pubsub/issues/801)) ([fa23503](https://togithub.com/googleapis/python-pubsub/commit/fa235033481783c2ec378b2a26b223bdff206461)) ### [`v2.13.8`](https://togithub.com/googleapis/python-pubsub/blob/HEAD/CHANGELOG.md#​2138-httpsgithubcomgoogleapispython-pubsubcomparev2137v2138-2022-10-03) [Compare Source](https://togithub.com/googleapis/python-pubsub/compare/v2.13.7...v2.13.8) ##### Bug Fixes - **deps:** Require protobuf >= 3.20.2 ([#​792](https://togithub.com/googleapis/python-pubsub/issues/792)) ([1a54f7c](https://togithub.com/googleapis/python-pubsub/commit/1a54f7cd3d997270e0a5d70f7caea32d8753be76))
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://app.renovatebot.com/dashboard#github/GoogleCloudPlatform/getting-started-python). --- background/app/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index c8cef21d..2f3c5204 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ google-cloud-firestore==2.7.2 -google-cloud-pubsub==2.13.7 +google-cloud-pubsub==2.13.11 flask==2.2.2 From f559d1ac8b6f7ad43dbbc5717bcd7b37963d646f Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Mon, 21 Nov 2022 22:18:30 +0100 Subject: [PATCH 370/395] chore(deps): update dependency pytest to v7.2.0 (#517) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [pytest](https://docs.pytest.org/en/latest/) ([source](https://togithub.com/pytest-dev/pytest), [changelog](https://docs.pytest.org/en/stable/changelog.html)) | `==7.1.3` -> `==7.2.0` | [![age](https://badges.renovateapi.com/packages/pypi/pytest/7.2.0/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/pypi/pytest/7.2.0/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/pypi/pytest/7.2.0/compatibility-slim/7.1.3)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/pypi/pytest/7.2.0/confidence-slim/7.1.3)](https://docs.renovatebot.com/merge-confidence/) | --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Renovate will not automatically rebase this PR, because other commits have been found. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://app.renovatebot.com/dashboard#github/GoogleCloudPlatform/getting-started-python). --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8fce9f7b..8ca59953 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ flake8==5.0.4 +pytest==7.2.0 nox==2022.11.21 -pytest==7.1.3 requests==2.28.1 From 456f95317805fe2e7b516eab519da62b41a34fd4 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Tue, 29 Nov 2022 23:18:00 +0100 Subject: [PATCH 371/395] chore(deps): update dependency google-cloud-logging to v3.3.0 (#524) --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 4f013fd2..338f84e5 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -2,6 +2,6 @@ Flask==2.2.2 google-cloud-firestore==2.7.2 google-cloud-storage==2.6.0 google-cloud-error-reporting==1.6.3 -google-cloud-logging==3.2.5 +google-cloud-logging==3.3.0 gunicorn==20.1.0 six==1.16.0 From 128e464c59e3a199519e62d4fff0d77c166d9b0a Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Tue, 29 Nov 2022 23:24:13 +0100 Subject: [PATCH 372/395] chore(deps): update dependency cryptography to v38.0.4 (#523) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [cryptography](https://togithub.com/pyca/cryptography) ([changelog](https://cryptography.io/en/latest/changelog/)) | `==38.0.3` -> `==38.0.4` | [![age](https://badges.renovateapi.com/packages/pypi/cryptography/38.0.4/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/pypi/cryptography/38.0.4/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/pypi/cryptography/38.0.4/compatibility-slim/38.0.3)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/pypi/cryptography/38.0.4/confidence-slim/38.0.3)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
pyca/cryptography ### [`v38.0.4`](https://togithub.com/pyca/cryptography/compare/38.0.3...38.0.4) [Compare Source](https://togithub.com/pyca/cryptography/compare/38.0.3...38.0.4)
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://app.renovatebot.com/dashboard#github/GoogleCloudPlatform/getting-started-python). --- authenticating-users/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index 178952d6..ffc99cac 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -1,6 +1,6 @@ # [START getting_started_requirements] Flask==2.2.2 -cryptography==38.0.3 +cryptography==38.0.4 python-jose[cryptography]==3.3.0 requests==2.28.1 # [END getting_started_requirements] From 053626ee9d2b4b0f0e8f83aeb7cd8c376cdef3fd Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Wed, 7 Dec 2022 03:52:08 +0100 Subject: [PATCH 373/395] chore(deps): update dependency google-cloud-storage to v2.7.0 (#526) --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 338f84e5..12ec886d 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,6 +1,6 @@ Flask==2.2.2 google-cloud-firestore==2.7.2 -google-cloud-storage==2.6.0 +google-cloud-storage==2.7.0 google-cloud-error-reporting==1.6.3 google-cloud-logging==3.3.0 gunicorn==20.1.0 From 6dc30ee7366c31623dae1680e9e71ead59265090 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Fri, 16 Dec 2022 04:00:57 +0100 Subject: [PATCH 374/395] chore(deps): update dependency google-cloud-translate to v3.9.0 (#529) --- background/function/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/function/requirements.txt b/background/function/requirements.txt index 82449ee6..1b45254a 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-translate==3.8.4 +google-cloud-translate==3.9.0 google-cloud-firestore==2.7.2 From a05296ddd735ff42c0924a464764879e9f712bb6 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Wed, 4 Jan 2023 19:54:36 +0100 Subject: [PATCH 375/395] chore(deps): update dependency google-cloud-logging to v3.4.0 (#525) --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 12ec886d..bfa392b8 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -2,6 +2,6 @@ Flask==2.2.2 google-cloud-firestore==2.7.2 google-cloud-storage==2.7.0 google-cloud-error-reporting==1.6.3 -google-cloud-logging==3.3.0 +google-cloud-logging==3.4.0 gunicorn==20.1.0 six==1.16.0 From dd7f6ea286c06b6aaef5828ead79c50d189f7226 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Wed, 4 Jan 2023 20:04:48 +0100 Subject: [PATCH 376/395] chore(deps): update dependency google-cloud-firestore to v2.7.3 (#527) Co-authored-by: Anthonios Partheniou --- background/app/requirements.txt | 2 +- background/function/requirements.txt | 2 +- bookshelf/requirements.txt | 2 +- sessions/requirements.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 2f3c5204..2d90b009 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ -google-cloud-firestore==2.7.2 +google-cloud-firestore==2.7.3 google-cloud-pubsub==2.13.11 flask==2.2.2 diff --git a/background/function/requirements.txt b/background/function/requirements.txt index 1b45254a..f51c60c3 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ google-cloud-translate==3.9.0 -google-cloud-firestore==2.7.2 +google-cloud-firestore==2.7.3 diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index bfa392b8..0faf3c47 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,5 +1,5 @@ Flask==2.2.2 -google-cloud-firestore==2.7.2 +google-cloud-firestore==2.7.3 google-cloud-storage==2.7.0 google-cloud-error-reporting==1.6.3 google-cloud-logging==3.4.0 diff --git a/sessions/requirements.txt b/sessions/requirements.txt index 05ccdd70..4ed276aa 100644 --- a/sessions/requirements.txt +++ b/sessions/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-firestore==2.7.2 +google-cloud-firestore==2.7.3 flask==2.2.2 From 027c1e376962c0726f6cce3024ed8c9df3e1339f Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Wed, 4 Jan 2023 20:14:08 +0100 Subject: [PATCH 377/395] chore(deps): update dependency google-cloud-error-reporting to v1.7.0 (#528) Co-authored-by: Anthonios Partheniou --- bookshelf/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 0faf3c47..d3ea0917 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,7 +1,7 @@ Flask==2.2.2 google-cloud-firestore==2.7.3 google-cloud-storage==2.7.0 -google-cloud-error-reporting==1.6.3 +google-cloud-error-reporting==1.7.0 google-cloud-logging==3.4.0 gunicorn==20.1.0 six==1.16.0 From 4f26b4795d9fe41a9eddffeaf09b93a1cdb5802d Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Wed, 4 Jan 2023 20:45:36 +0100 Subject: [PATCH 378/395] chore(deps): update dependency cryptography to v39 (#530) * chore(deps): update dependency cryptography to v39 * group renovate PRs to reduce toilsome tasks Co-authored-by: Anthonios Partheniou --- .github/renovate.json | 1 + authenticating-users/requirements.txt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/renovate.json b/.github/renovate.json index 091cca06..c0a95e72 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -1,6 +1,7 @@ { "extends": [ "config:base", + "group:all", ":preserveSemverRanges", ":disableDependencyDashboard" ], diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index ffc99cac..49e7345c 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -1,6 +1,6 @@ # [START getting_started_requirements] Flask==2.2.2 -cryptography==38.0.4 +cryptography==39.0.0 python-jose[cryptography]==3.3.0 requests==2.28.1 # [END getting_started_requirements] From bd79d4b86a8994e55fe4103d518153f301d18434 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Wed, 11 Jan 2023 15:14:32 +0000 Subject: [PATCH 379/395] chore(deps): update all dependencies (#532) * chore(deps): update all dependencies * only update flake8 for python 3.8+ Co-authored-by: Anthonios Partheniou --- background/app/requirements.txt | 4 ++-- background/function/requirements.txt | 4 ++-- bookshelf/requirements.txt | 4 ++-- requirements.txt | 3 ++- sessions/requirements.txt | 2 +- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 2d90b009..019bac9c 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ -google-cloud-firestore==2.7.3 -google-cloud-pubsub==2.13.11 +google-cloud-firestore==2.8.0 +google-cloud-pubsub==2.13.12 flask==2.2.2 diff --git a/background/function/requirements.txt b/background/function/requirements.txt index f51c60c3..9ca6426c 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-translate==3.9.0 -google-cloud-firestore==2.7.3 +google-cloud-translate==3.10.0 +google-cloud-firestore==2.8.0 diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index d3ea0917..af651181 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,7 +1,7 @@ Flask==2.2.2 -google-cloud-firestore==2.7.3 +google-cloud-firestore==2.8.0 google-cloud-storage==2.7.0 -google-cloud-error-reporting==1.7.0 +google-cloud-error-reporting==1.8.0 google-cloud-logging==3.4.0 gunicorn==20.1.0 six==1.16.0 diff --git a/requirements.txt b/requirements.txt index 8ca59953..b0462b4d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ -flake8==5.0.4 +flake8==5.0.4; python_version < '3.8' +flake8==6.0.0; python_version >= '3.8' pytest==7.2.0 nox==2022.11.21 requests==2.28.1 diff --git a/sessions/requirements.txt b/sessions/requirements.txt index 4ed276aa..57d94701 100644 --- a/sessions/requirements.txt +++ b/sessions/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-firestore==2.7.3 +google-cloud-firestore==2.8.0 flask==2.2.2 From 747e15c7c92bcbbef38f874af4a72661ee4e1327 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Fri, 13 Jan 2023 17:13:10 +0000 Subject: [PATCH 380/395] chore(deps): update all dependencies (#533) * chore(deps): update all dependencies * revert flake8 for python 3.7 Co-authored-by: Anthonios Partheniou --- authenticating-users/requirements.txt | 2 +- requirements.txt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index 49e7345c..9725073e 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -2,5 +2,5 @@ Flask==2.2.2 cryptography==39.0.0 python-jose[cryptography]==3.3.0 -requests==2.28.1 +requests==2.28.2 # [END getting_started_requirements] diff --git a/requirements.txt b/requirements.txt index b0462b4d..fe9ae17e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -flake8==5.0.4; python_version < '3.8' +flake8===5.0.4; python_version < '3.8' flake8==6.0.0; python_version >= '3.8' pytest==7.2.0 nox==2022.11.21 -requests==2.28.1 +requests==2.28.2 From e090316f2b683393d7aeab437545c6937701c585 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Fri, 13 Jan 2023 18:29:47 +0000 Subject: [PATCH 381/395] chore(deps): update dependency google-cloud-firestore to v2.9.0 (#534) --- background/app/requirements.txt | 2 +- background/function/requirements.txt | 2 +- bookshelf/requirements.txt | 2 +- sessions/requirements.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 019bac9c..a339d76b 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ -google-cloud-firestore==2.8.0 +google-cloud-firestore==2.9.0 google-cloud-pubsub==2.13.12 flask==2.2.2 diff --git a/background/function/requirements.txt b/background/function/requirements.txt index 9ca6426c..334b9414 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ google-cloud-translate==3.10.0 -google-cloud-firestore==2.8.0 +google-cloud-firestore==2.9.0 diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index af651181..f1cbf765 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,5 +1,5 @@ Flask==2.2.2 -google-cloud-firestore==2.8.0 +google-cloud-firestore==2.9.0 google-cloud-storage==2.7.0 google-cloud-error-reporting==1.8.0 google-cloud-logging==3.4.0 diff --git a/sessions/requirements.txt b/sessions/requirements.txt index 57d94701..09ff17a0 100644 --- a/sessions/requirements.txt +++ b/sessions/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-firestore==2.8.0 +google-cloud-firestore==2.9.0 flask==2.2.2 From f583706ac669bbb252dad6840ba44c9e99de19c5 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Wed, 18 Jan 2023 00:33:19 +0000 Subject: [PATCH 382/395] chore(deps): update dependency pytest to v7.2.1 (#535) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index fe9ae17e..930cfb5d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ flake8===5.0.4; python_version < '3.8' flake8==6.0.0; python_version >= '3.8' -pytest==7.2.0 +pytest==7.2.1 nox==2022.11.21 requests==2.28.2 From 83768bedc113144949a7abd4eae38762cb36e35c Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Mon, 23 Jan 2023 16:13:53 +0000 Subject: [PATCH 383/395] chore(deps): update dependency google-cloud-pubsub to v2.14.0 (#536) --- background/app/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index a339d76b..ebd90391 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ google-cloud-firestore==2.9.0 -google-cloud-pubsub==2.13.12 +google-cloud-pubsub==2.14.0 flask==2.2.2 From 70ad41f01459a66b186473f7b0845f2ace47dbe5 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Mon, 23 Jan 2023 16:36:48 +0000 Subject: [PATCH 384/395] chore(deps): update dependency google-cloud-translate to v3.10.1 (#537) --- background/function/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/function/requirements.txt b/background/function/requirements.txt index 334b9414..44dffc3c 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-translate==3.10.0 +google-cloud-translate==3.10.1 google-cloud-firestore==2.9.0 From 8f289a6abee184095b71718fb9144b767aab42ae Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Wed, 1 Feb 2023 14:03:26 +0000 Subject: [PATCH 385/395] chore(deps): update all dependencies (#538) --- background/app/requirements.txt | 2 +- background/function/requirements.txt | 2 +- bookshelf/requirements.txt | 6 +++--- sessions/requirements.txt | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index ebd90391..d869b7d5 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ -google-cloud-firestore==2.9.0 +google-cloud-firestore==2.9.1 google-cloud-pubsub==2.14.0 flask==2.2.2 diff --git a/background/function/requirements.txt b/background/function/requirements.txt index 44dffc3c..7bf21296 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ google-cloud-translate==3.10.1 -google-cloud-firestore==2.9.0 +google-cloud-firestore==2.9.1 diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index f1cbf765..45969811 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,7 +1,7 @@ Flask==2.2.2 -google-cloud-firestore==2.9.0 +google-cloud-firestore==2.9.1 google-cloud-storage==2.7.0 -google-cloud-error-reporting==1.8.0 -google-cloud-logging==3.4.0 +google-cloud-error-reporting==1.8.1 +google-cloud-logging==3.5.0 gunicorn==20.1.0 six==1.16.0 diff --git a/sessions/requirements.txt b/sessions/requirements.txt index 09ff17a0..69960348 100644 --- a/sessions/requirements.txt +++ b/sessions/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-firestore==2.9.0 +google-cloud-firestore==2.9.1 flask==2.2.2 From daca19a28d7cab97d833cd8974fd52e6d1688dcd Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Thu, 20 Apr 2023 01:56:27 +0200 Subject: [PATCH 386/395] chore(deps): update dependency cryptography to v39.0.1 [security] (#541) --- authenticating-users/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index 9725073e..fa01653a 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -1,6 +1,6 @@ # [START getting_started_requirements] Flask==2.2.2 -cryptography==39.0.0 +cryptography==39.0.1 python-jose[cryptography]==3.3.0 requests==2.28.2 # [END getting_started_requirements] From 787b743af675a6a7e05c0edddbe68fac93bbc97b Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Tue, 9 May 2023 08:17:48 +0200 Subject: [PATCH 387/395] chore(deps): update dependency flask to v2.2.5 [security] (#546) --- background/app/requirements.txt | 2 +- gce/requirements.txt | 2 +- sessions/requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/background/app/requirements.txt b/background/app/requirements.txt index d869b7d5..23419a2a 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ google-cloud-firestore==2.9.1 google-cloud-pubsub==2.14.0 -flask==2.2.2 +flask==2.2.5 diff --git a/gce/requirements.txt b/gce/requirements.txt index a39a519c..b655465f 100644 --- a/gce/requirements.txt +++ b/gce/requirements.txt @@ -1,3 +1,3 @@ -flask==2.2.2 +flask==2.2.5 honcho==1.1.0 gunicorn==20.1.0 diff --git a/sessions/requirements.txt b/sessions/requirements.txt index 69960348..6050da39 100644 --- a/sessions/requirements.txt +++ b/sessions/requirements.txt @@ -1,2 +1,2 @@ google-cloud-firestore==2.9.1 -flask==2.2.2 +flask==2.2.5 From b85542612dedca6e1d11b559863ed3fb9fb339a2 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Tue, 9 May 2023 08:32:02 +0200 Subject: [PATCH 388/395] chore(deps): update all dependencies (#540) * chore(deps): update all dependencies * Apply suggestions from code review --------- Co-authored-by: Dan Lee <71398022+dandhlee@users.noreply.github.com> --- authenticating-users/requirements.txt | 6 +++--- background/app/requirements.txt | 4 ++-- background/function/requirements.txt | 4 ++-- bookshelf/requirements.txt | 8 ++++---- requirements.txt | 6 +++--- sessions/requirements.txt | 2 +- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index fa01653a..39aa1b4b 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -1,6 +1,6 @@ # [START getting_started_requirements] -Flask==2.2.2 -cryptography==39.0.1 +Flask==2.2.5 +cryptography==40.0.2 python-jose[cryptography]==3.3.0 -requests==2.28.2 +requests==2.30.0 # [END getting_started_requirements] diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 23419a2a..7cfba806 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ -google-cloud-firestore==2.9.1 -google-cloud-pubsub==2.14.0 +google-cloud-firestore==2.11.1 +google-cloud-pubsub==2.16.1 flask==2.2.5 diff --git a/background/function/requirements.txt b/background/function/requirements.txt index 7bf21296..b8e6aaad 100644 --- a/background/function/requirements.txt +++ b/background/function/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-translate==3.10.1 -google-cloud-firestore==2.9.1 +google-cloud-translate==3.11.1 +google-cloud-firestore==2.11.1 diff --git a/bookshelf/requirements.txt b/bookshelf/requirements.txt index 45969811..27d3d4fe 100644 --- a/bookshelf/requirements.txt +++ b/bookshelf/requirements.txt @@ -1,7 +1,7 @@ -Flask==2.2.2 -google-cloud-firestore==2.9.1 -google-cloud-storage==2.7.0 -google-cloud-error-reporting==1.8.1 +Flask==2.2.5 +google-cloud-firestore==2.11.1 +google-cloud-storage==2.9.0 +google-cloud-error-reporting==1.9.1 google-cloud-logging==3.5.0 gunicorn==20.1.0 six==1.16.0 diff --git a/requirements.txt b/requirements.txt index 930cfb5d..c07ff6c2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ flake8===5.0.4; python_version < '3.8' flake8==6.0.0; python_version >= '3.8' -pytest==7.2.1 -nox==2022.11.21 -requests==2.28.2 +pytest==7.3.1 +nox==2023.4.22 +requests==2.30.0 diff --git a/sessions/requirements.txt b/sessions/requirements.txt index 6050da39..e0e267b5 100644 --- a/sessions/requirements.txt +++ b/sessions/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-firestore==2.9.1 +google-cloud-firestore==2.11.1 flask==2.2.5 From ce883090665bf309925f8eb0038f268675169b3c Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Mon, 5 Jun 2023 13:13:57 +0200 Subject: [PATCH 389/395] chore(deps): update dependency requests to v2.31.0 [security] (#548) --- authenticating-users/requirements.txt | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index 39aa1b4b..23a31c7d 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -2,5 +2,5 @@ Flask==2.2.5 cryptography==40.0.2 python-jose[cryptography]==3.3.0 -requests==2.30.0 +requests==2.31.0 # [END getting_started_requirements] diff --git a/requirements.txt b/requirements.txt index c07ff6c2..0fd66f54 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,4 @@ flake8===5.0.4; python_version < '3.8' flake8==6.0.0; python_version >= '3.8' pytest==7.3.1 nox==2023.4.22 -requests==2.30.0 +requests==2.31.0 From f23ce88ab840dcb80cc82c9619658ec936185bc6 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Mon, 5 Jun 2023 13:20:12 +0200 Subject: [PATCH 390/395] chore(deps): update dependency cryptography to v41 [security] (#549) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [cryptography](https://togithub.com/pyca/cryptography) ([changelog](https://cryptography.io/en/latest/changelog/)) | `==40.0.2` -> `==41.0.0` | [![age](https://badges.renovateapi.com/packages/pypi/cryptography/41.0.0/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/pypi/cryptography/41.0.0/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/pypi/cryptography/41.0.0/compatibility-slim/40.0.2)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/pypi/cryptography/41.0.0/confidence-slim/40.0.2)](https://docs.renovatebot.com/merge-confidence/) | ### GitHub Vulnerability Alerts #### [GHSA-5cpq-8wj7-hf2v](https://togithub.com/pyca/cryptography/security/advisories/GHSA-5cpq-8wj7-hf2v) pyca/cryptography's wheels include a statically linked copy of OpenSSL. The versions of OpenSSL included in cryptography 0.5-40.0.2 are vulnerable to a security issue. More details about the vulnerability itself can be found in https://www.openssl.org/news/secadv/20230530.txt. If you are building cryptography source ("sdist") then you are responsible for upgrading your copy of OpenSSL. Only users installing from wheels built by the cryptography project (i.e., those distributed on PyPI) need to update their cryptography versions. --- ### Release Notes
pyca/cryptography ### [`v41.0.0`](https://togithub.com/pyca/cryptography/compare/40.0.2...41.0.0) [Compare Source](https://togithub.com/pyca/cryptography/compare/40.0.2...41.0.0)
--- ### Configuration 📅 **Schedule**: Branch creation - "" (UTC), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://app.renovatebot.com/dashboard#github/GoogleCloudPlatform/getting-started-python). --- authenticating-users/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index 23a31c7d..aa163f59 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -1,6 +1,6 @@ # [START getting_started_requirements] Flask==2.2.5 -cryptography==40.0.2 +cryptography==41.0.0 python-jose[cryptography]==3.3.0 requests==2.31.0 # [END getting_started_requirements] From 8be59fc2addafd984309b68b71906c467ab1088e Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Mon, 17 Jul 2023 17:53:47 +0200 Subject: [PATCH 391/395] chore(deps): update dependency cryptography to v41.0.2 [security] (#550) --- authenticating-users/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authenticating-users/requirements.txt b/authenticating-users/requirements.txt index aa163f59..0d4a3c36 100644 --- a/authenticating-users/requirements.txt +++ b/authenticating-users/requirements.txt @@ -1,6 +1,6 @@ # [START getting_started_requirements] Flask==2.2.5 -cryptography==41.0.0 +cryptography==41.0.2 python-jose[cryptography]==3.3.0 requests==2.31.0 # [END getting_started_requirements] From 21d4d6bc33a457dc91af0199cffd6211f1fff5a2 Mon Sep 17 00:00:00 2001 From: Jennifer Davis Date: Thu, 22 Aug 2024 13:01:55 -0700 Subject: [PATCH 392/395] fix: update libraries and python for the base image (#570) * fix: update libraries and python for the base image * fix:update python to python3 * fix: removing the python installation and using what's already installed * fix: resolve error around the use of apt instead of apt-get --- .kokoro/docker/Dockerfile | 27 +++++++-------------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/.kokoro/docker/Dockerfile b/.kokoro/docker/Dockerfile index 6b6c19f1..accdd0bf 100644 --- a/.kokoro/docker/Dockerfile +++ b/.kokoro/docker/Dockerfile @@ -15,8 +15,8 @@ FROM gcr.io/cloud-devrel-kokoro-resources/python-base:latest # Install libraries needed by third-party python packages that we depend on. -RUN apt update \ - && apt install -y \ +RUN apt-get update \ + && apt-get install -y \ graphviz \ libcurl4-openssl-dev \ libffi-dev \ @@ -30,32 +30,19 @@ RUN apt update \ libxml2-dev \ libxslt1-dev \ openssl \ - portaudio19-dev \ - python-pyaudio \ zlib1g-dev \ - && apt clean + && apt-get clean -###################### Install python 3.7.12 - -# Download python 3.7.12 -RUN wget https://www.python.org/ftp/python/3.7.12/Python-3.7.12.tgz - -# Extract files -RUN tar -xvf Python-3.7.12.tgz - -# Install python 3.7.12 -RUN ./Python-3.7.12/configure --enable-optimizations -RUN make altinstall ###################### Check python version -RUN python --version -RUN which python +RUN python3 --version +RUN which python3 # Setup Cloud SDK -ENV CLOUD_SDK_VERSION 369.0.0 +ENV CLOUD_SDK_VERSION 489.0.0 # Use system python for cloud sdk. -ENV CLOUDSDK_PYTHON /usr/bin/python +ENV CLOUDSDK_PYTHON python3.12 RUN wget https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-sdk-$CLOUD_SDK_VERSION-linux-x86_64.tar.gz RUN tar xzf google-cloud-sdk-$CLOUD_SDK_VERSION-linux-x86_64.tar.gz RUN /google-cloud-sdk/install.sh From d9da8db203f7729f5da28b57be66b69084955bde Mon Sep 17 00:00:00 2001 From: Jennifer Davis Date: Thu, 22 Aug 2024 14:23:10 -0700 Subject: [PATCH 393/395] fix: rev version of python for use noxfile.py (#571) --- noxfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index 68fb5a37..8f5a921c 100644 --- a/noxfile.py +++ b/noxfile.py @@ -55,7 +55,7 @@ def run_test(session, dir): success_codes=[0, 5]) -@nox.session(python="3.7") +@nox.session(python="3.12") @nox.parametrize('dir', DIRS) def run_tests(session, dir=None): """Run all tests for all directories (slow!)""" From d6097aa561c71d67c953bdeb724b4da372bd2cec Mon Sep 17 00:00:00 2001 From: bhandarivijay-png Date: Mon, 5 Jan 2026 18:43:17 +0000 Subject: [PATCH 394/395] chore: Migrate gsutil usage to gcloud storage (#584) --- optional-kubernetes-engine/Makefile | 4 ++-- optional-kubernetes-engine/README.md | 4 ++-- optional-kubernetes-engine/config.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/optional-kubernetes-engine/Makefile b/optional-kubernetes-engine/Makefile index f275ddf4..4c1cbf3e 100644 --- a/optional-kubernetes-engine/Makefile +++ b/optional-kubernetes-engine/Makefile @@ -12,8 +12,8 @@ create-cluster: .PHONY: create-bucket create-bucket: - gsutil mb gs://$(GCLOUD_PROJECT) - gsutil defacl set public-read gs://$(GCLOUD_PROJECT) + gcloud storage buckets create gs://$(GCLOUD_PROJECT) + gcloud storage buckets update gs://$(GCLOUD_PROJECT) --predefined-default-object-acl=public-read .PHONY: build build: diff --git a/optional-kubernetes-engine/README.md b/optional-kubernetes-engine/README.md index e3c17236..9acb98b4 100644 --- a/optional-kubernetes-engine/README.md +++ b/optional-kubernetes-engine/README.md @@ -36,8 +36,8 @@ Alternatively, you can use make: The bookshelf application uses [Google Cloud Storage](https://cloud.google.com/storage) to store image files. Create a bucket for your project: - gsutil mb gs:// - gsutil defacl set public-read gs:// + gcloud storage buckets create gs:// + gcloud storage buckets update gs:// --predefined-default-object-acl=public-read Alternatively, you can use make: diff --git a/optional-kubernetes-engine/config.py b/optional-kubernetes-engine/config.py index 0fd601ed..7da9b4cc 100644 --- a/optional-kubernetes-engine/config.py +++ b/optional-kubernetes-engine/config.py @@ -78,12 +78,12 @@ # Typically, you'll name your bucket the same as your project. To create a # bucket: # -# $ gsutil mb gs:// +# $ gcloud storage buckets create gs:// # # You also need to make sure that the default ACL is set to public-read, # otherwise users will not be able to see their upload images: # -# $ gsutil defacl set public-read gs:// +# $ gcloud storage buckets update --predefined-default-object-acl=public-read gs:// # # You can adjust the max content length and allow extensions settings to allow # larger or more varied file types if desired. From 08418ec52153b88a7148abca245ea29eb352f8df Mon Sep 17 00:00:00 2001 From: Jennifer Davis Date: Thu, 15 Jan 2026 13:06:12 -0800 Subject: [PATCH 395/395] fix: modernize background app (#572) * fix: modernize background app * fix: attempt to fix linting issues * fix: resolve server-side cross-site scripting * fix: resolve lint issue with import order. * chore: add temporary use of storage to check if auth is working correctly. * fix: moving the bucket_name to the right level of indentation. * tmp: adding an assert to use the bucket variable that should be removed. * fix: based on https://github.com/pallets/flask/pull/5223/files moving to the use of markupsafe for escape * fix: import order * fix: really fix the library order --- background/app/app.yaml | 2 +- background/app/main.py | 18 ++++++++---------- background/app/main_test.py | 7 +++++-- background/app/requirements.txt | 6 +++--- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/background/app/app.yaml b/background/app/app.yaml index fb63451a..02c0651c 100644 --- a/background/app/app.yaml +++ b/background/app/app.yaml @@ -13,5 +13,5 @@ # limitations under the License. # [START getting_started_background_config] -runtime: python37 +runtime: python312 # [END getting_started_background_config] diff --git a/background/app/main.py b/background/app/main.py index 5474d54c..fad93d46 100644 --- a/background/app/main.py +++ b/background/app/main.py @@ -21,8 +21,8 @@ import os from flask import Flask, redirect, render_template, request -from google.cloud import firestore -from google.cloud import pubsub +from google.cloud import firestore, pubsub +from markupsafe import escape app = Flask(__name__) @@ -61,15 +61,13 @@ def translate(): language (form field 'lang'), by sending a PubSub message to a topic. """ source_string = request.form.get("v", "") - to_language = request.form.get("lang", "") + to_language = escape(request.form.get("lang", "")) if source_string == "": - error_message = "Empty value" - return error_message, 400 + return "Invalid request, you must provide a value.", 400 if to_language not in ACCEPTABLE_LANGUAGES: - error_message = "Unsupported language: {}".format(to_language) - return error_message, 400 + return f"Unsupported language: {to_language}", 400 message = { "Original": source_string, @@ -78,11 +76,11 @@ def translate(): "OriginalLanguage": "", } - topic_name = "projects/{}/topics/{}".format( - os.getenv("GOOGLE_CLOUD_PROJECT"), "translate" + topic_name = ( + f"projects/{os.getenv('GOOGLE_CLOUD_PROJECT')}/topics/translate" ) publisher.publish( - topic=topic_name, data=json.dumps(message).encode("utf8") + topic=topic_name, data=json.dumps(message).encode("utf-8") ) return redirect("/") diff --git a/background/app/main_test.py b/background/app/main_test.py index f3bf28c2..41b529d3 100644 --- a/background/app/main_test.py +++ b/background/app/main_test.py @@ -16,8 +16,7 @@ import uuid import google.auth -from google.cloud import firestore -from google.cloud import pubsub +from google.cloud import firestore, pubsub, storage import main import pytest @@ -39,7 +38,10 @@ def clear_collection(collection): for doc in collection.stream(): doc.reference.delete() + bucket_name = 'system-test-bucket' client = firestore.Client() + storage_client = storage.Client() + bucket = storage_client.bucket(bucket_name) translations = client.collection("translations") clear_collection(translations) translations.add( @@ -51,6 +53,7 @@ def clear_collection(collection): }, document_id="test translation", ) + assert bucket in locals() yield client diff --git a/background/app/requirements.txt b/background/app/requirements.txt index 7cfba806..f70d16b7 100644 --- a/background/app/requirements.txt +++ b/background/app/requirements.txt @@ -1,3 +1,3 @@ -google-cloud-firestore==2.11.1 -google-cloud-pubsub==2.16.1 -flask==2.2.5 +google-cloud-firestore==2.18.0 +google-cloud-pubsub==2.23.0 +flask==3.0.3