diff --git a/functions/README.rst b/functions/README.rst new file mode 100644 index 00000000000..f81a7a6611a --- /dev/null +++ b/functions/README.rst @@ -0,0 +1,84 @@ +.. This file is automatically generated. Do not edit this file directly. + +Google Cloud Functions Python Samples +=============================================================================== + +.. image:: https://gstatic.com/cloudssh/images/open-btn.png + :target: https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/GoogleCloudPlatform/python-docs-samples&page=editor&open_in_editor=/README.rst + + +This directory contains samples for Google Cloud Functions. `Cloud Functions`_ is a lightweight, event-based, asynchronous compute solution that allows you to create small, single-purpose functions that respond to Cloud events without the need to manage a server or a runtime environment. + + + + +.. _Cloud Functions: https://cloud.google.com/functions/docs/ + +Setup +------------------------------------------------------------------------------- + + +Authentication +++++++++++++++ + +This sample requires you to have authentication setup. Refer to the +`Authentication Getting Started Guide`_ for instructions on setting up +credentials for applications. + +.. _Authentication Getting Started Guide: + https://cloud.google.com/docs/authentication/getting-started + +Install Dependencies +++++++++++++++++++++ + +#. Install `pip`_ and `virtualenv`_ if you do not already have them. You may want to refer to the `Python Development Environment Setup Guide`_ for Google Cloud Platform for instructions. + + .. _Python Development Environment Setup Guide: + https://cloud.google.com/python/setup + +#. Create a virtualenv. Samples are compatible with Python 2.7 and 3.4+. + + .. code-block:: bash + + $ virtualenv env + $ source env/bin/activate + +#. Install the dependencies needed to run the samples. + + .. code-block:: bash + + $ pip install -r requirements.txt + +.. _pip: https://pip.pypa.io/ +.. _virtualenv: https://virtualenv.pypa.io/ + +Samples +------------------------------------------------------------------------------- + +- `Hello World`_ +- Concepts_ +- `Logging & Monitoring`_ +- Tips_ + + +.. _Hello World: helloworld/ +.. _Concepts: concepts/ +.. _Logging & Monitoring: log/ +.. _Tips: tips/ + +The client library +------------------------------------------------------------------------------- + +This sample uses the `Google Cloud Client Library for Python`_. +You can read the documentation for more details on API usage and use GitHub +to `browse the source`_ and `report issues`_. + +.. _Google Cloud Client Library for Python: + https://googlecloudplatform.github.io/google-cloud-python/ +.. _browse the source: + https://github.com/GoogleCloudPlatform/google-cloud-python +.. _report issues: + https://github.com/GoogleCloudPlatform/google-cloud-python/issues + + +.. _Google Cloud SDK: https://cloud.google.com/sdk/ diff --git a/functions/README.rst.in b/functions/README.rst.in new file mode 100644 index 00000000000..f86d5bd823b --- /dev/null +++ b/functions/README.rst.in @@ -0,0 +1,18 @@ +# This file is used to generate README.rst + +product: + name: Google Cloud Functions + short_name: GCF + url: https://cloud.google.com/functions/docs/ + description: > + Cloud Functions is a lightweight, event-based, asynchronous compute solution that allows you to create small, single-purpose functions that respond to Cloud events without the need to manage a server or a runtime environment. + +setup: +- auth +- install_deps + +samples: +- name: Hello World + file: helloworld/main.py + +cloud_client_library: true diff --git a/functions/concepts/README.md b/functions/concepts/README.md new file mode 100644 index 00000000000..a889048b17a --- /dev/null +++ b/functions/concepts/README.md @@ -0,0 +1,11 @@ +Google Cloud Platform logo + +# Google Cloud Functions - Concepts sample + +See: + +* [Cloud Functions Concepts tutorial][tutorial] +* [Cloud Functions Concepts sample source code][code] + +[tutorial]: https://cloud.google.com/functions/docs/concepts/exec +[code]: main.py diff --git a/functions/concepts/main.py b/functions/concepts/main.py new file mode 100644 index 00000000000..43bfd3aabcf --- /dev/null +++ b/functions/concepts/main.py @@ -0,0 +1,117 @@ +# 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. + +import time + + +# [START functions_concepts_stateless] +# Global variable, modified within the function by using the global keyword. +count = 0 + + +def statelessness(request): + """ + HTTP Cloud Function that counts how many times it is executed + within a specific instance. + Args: + request (flask.Request): The request object. + Returns: + The response text, or any set of values that can be turned into a + Response object using `make_response` + . + """ + global count + count += 1 + + # Note: the total function invocation count across + # all instances may not be equal to this value! + return 'Instance execution count: {}'.format(count) +# [END functions_concepts_stateless] + + +def heavy_computation(): + return time.time() + + +def light_computation(): + return time.time() + + +# [START functions_tips_scopes] +# Global (instance-wide) scope +# This computation runs at instance cold-start +instance_var = heavy_computation() + + +def scope_demo(request): + """ + HTTP Cloud Function that declares a variable. + Args: + request (flask.Request): The request object. + Returns: + The response text, or any set of values that can be turned into a + Response object using `make_response` + . + """ + + # Per-function scope + # This computation runs every time this function is called + function_var = light_computation() + return 'Instance: {}; function: {}'.format(instance_var, function_var) +# [END functions_tips_scopes] + + +# [START functions_concepts_requests] +def make_request(request): + """ + HTTP Cloud Function that makes another HTTP request. + Args: + request (flask.Request): The request object. + Returns: + The response text, or any set of values that can be turned into a + Response object using `make_response` + . + """ + import requests + + # The URL to send the request to + url = 'http://example.com' + + # Process the request + response = requests.get(url) + response.raise_for_status() + return 'Success!' +# [END functions_concepts_requests] + + +# [START functions_concepts_after_timeout] +def timeout(request): + print('Function running...') + time.sleep(120) + + # May not execute if function's timeout is <2 minutes + return 'Done!' +# [END functions_concepts_after_timeout] + + +# [START functions_concepts_filesystem] +def list_files(request): + import os + from os import path + + root = path.dirname(path.abspath(__file__)) + children = os.listdir(root) + files = [c for c in children if path.isfile(path.join(root, c))] + return 'Files: {}'.format(files) +# [END functions_concepts_filesystem] diff --git a/functions/concepts/main_test.py b/functions/concepts/main_test.py new file mode 100644 index 00000000000..71e07b50587 --- /dev/null +++ b/functions/concepts/main_test.py @@ -0,0 +1,63 @@ +# 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. + + +import flask +import pytest +import requests +import responses + +import main + + +# Create a fake "app" for generating test request contexts. +@pytest.fixture(scope="module") +def app(): + return flask.Flask(__name__) + + +def test_statelessness(app): + with app.test_request_context(): + res = main.statelessness(flask.request) + assert res == 'Instance execution count: 1' + res = main.statelessness(flask.request) + assert res == 'Instance execution count: 2' + + +def test_scope_demo(app): + with app.test_request_context(): + main.scope_demo(flask.request) + + +@responses.activate +def test_make_request_200(app): + responses.add(responses.GET, 'http://example.com', + json={'status': 'OK'}, status=200) + with app.test_request_context(): + main.make_request(flask.request) + + +@responses.activate +def test_make_request_404(app): + responses.add(responses.GET, 'http://example.com', + json={'error': 'not found'}, status=404) + with app.test_request_context(): + with pytest.raises(requests.exceptions.HTTPError): + main.make_request(flask.request) + + +def test_list_files(app): + with app.test_request_context(): + res = main.list_files(flask.request) + assert 'main.py' in res diff --git a/functions/helloworld/README.md b/functions/helloworld/README.md new file mode 100644 index 00000000000..11d68f93063 --- /dev/null +++ b/functions/helloworld/README.md @@ -0,0 +1,11 @@ +Google Cloud Platform logo + +# Google Cloud Functions - Hello World sample + +See: + +* [Cloud Functions Hello World tutorial][tutorial] +* [Cloud Functions Hello World sample source code][code] + +[tutorial]: https://cloud.google.com/functions/docs/quickstart +[code]: main.py diff --git a/functions/helloworld/main.py b/functions/helloworld/main.py new file mode 100644 index 00000000000..45b779f16f3 --- /dev/null +++ b/functions/helloworld/main.py @@ -0,0 +1,171 @@ +# 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. + +# [START functions_helloworld_error] +import logging +# [END functions_helloworld_error] +import sys + + +# [START functions_tips_terminate] +# [START functions_helloworld_get] +def hello_get(request): + """HTTP Cloud Function. + Args: + request (flask.Request): The request object. + Returns: + The response text, or any set of values that can be turned into a + Response object using `make_response` + . + """ + return 'Hello, World!' +# [END functions_helloworld_get] + + +# [START functions_helloworld_background] +def hello_background(data, context): + """Background Cloud Function. + Args: + event (dict): The dictionary with data specific to the given event. + context (google.cloud.functions.Context): The Cloud Functions event + context. + """ + if data and 'name' in data: + name = data['name'] + else: + name = 'World' + return 'Hello, {}!'.format(name) +# [END functions_helloworld_background] +# [END functions_tips_terminate] + + +# [START functions_helloworld_http] +def hello_http(request): + """HTTP Cloud Function. + Args: + request (flask.Request): The request object. + Returns: + The response text, or any set of values that can be turned into a + Response object using `make_response` + . + """ + request_json = request.get_json() + if request_json and 'message' in request_json: + name = request_json['message'] + else: + name = 'World' + return 'Hello, {}!'.format(name) +# [END functions_helloworld_http] + + +# [START functions_helloworld_pubsub] +def hello_pubsub(data, context): + """Background Cloud Function to be triggered by Pub/Sub. + Args: + event (dict): The dictionary with data specific to this type of event. + context (google.cloud.functions.Context): The Cloud Functions event + context. + """ + import base64 + + if 'data' in data: + name = base64.b64decode(data['data']).decode('utf-8') + else: + name = 'World' + print('Hello, {}!'.format(name)) +# [END functions_helloworld_pubsub] + + +# [START functions_helloworld_storage] +def hello_gcs(event, context): + """Background Cloud Function to be triggered by Cloud Storage. + Args: + event (dict): The dictionary with data specific to this type of event. + context (google.cloud.functions.Context): The Cloud Functions + event context. + """ + print("File: {}.".format(event['objectId'])) +# [END functions_helloworld_storage] + + +# [START functions_http_content] +def hello_content(request): + """ Responds to an HTTP request using data from the request body parsed + according to the "content-type" header. + Args: + request (flask.Request): The request object. + Returns: + The response text, or any set of values that can be turned into a + Response object using `make_response` + . + """ + content_type = request.headers['content-type'] + if content_type == 'application/json': + name = request.json.get('name') + elif content_type == 'application/octet-stream': + name = request.data + elif content_type == 'text/plain': + name = request.data + elif content_type == 'application/x-www-form-urlencoded': + name = request.form.get('name') + else: + raise ValueError("Unknown content type: {}".format(content_type)) + return 'Hello, {}!'.format(name) +# [END functions_http_content] + + +# [START functions_http_methods] +def hello_method(request): + """ Responds to a GET request with "Hello world!". Forbids a PUT request. + Args: + request (flask.Request): The request object. + Returns: + The response text, or any set of values that can be turned into a + Response object using `make_response` + . + """ + from flask import abort + + if request.method == 'GET': + return 'Hello, World!' + elif request.method == 'PUT': + return abort(403) + else: + return abort(405) +# [END functions_http_methods] + + +def hello_error_1(request): + # [START functions_helloworld_error] + # This WILL be reported to Stackdriver Error Reporting, + # and WILL terminate the function + raise RuntimeError('I failed you') + + # [END functions_helloworld_error] + + +def hello_error_2(request): + # [START functions_helloworld_error] + # WILL NOT be reported to Stackdriver Error Reporting, but will show up + # in logs + print(RuntimeError('I failed you (print to stdout)')) + logging.warn(RuntimeError('I failed you (logging.warn)')) + logging.error(RuntimeError('I failed you (logging.error)')) + sys.stderr.write('I failed you (sys.stderr.write)\n') + + # WILL NOT be reported to Stackdriver Error Reporting, but will show up + # in request logs (as a 500 response) + from flask import abort + return abort(500) + # [END functions_helloworld_error] diff --git a/functions/helloworld/main_test.py b/functions/helloworld/main_test.py new file mode 100644 index 00000000000..fcd00eab99a --- /dev/null +++ b/functions/helloworld/main_test.py @@ -0,0 +1,62 @@ +# 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. + +import flask +import pytest + +import main + + +# Create a fake "app" for generating test request contexts. +@pytest.fixture(scope="module") +def app(): + return flask.Flask(__name__) + + +def test_hello_get(app): + with app.test_request_context(): + res = main.hello_get(flask.request) + assert 'Hello, World!' in res + + +def test_hello_http_no_args(app): + with app.test_request_context(): + res = main.hello_http(flask.request) + assert 'Hello, World!' in res + + +def test_hello_http_args(app): + with app.test_request_context(json={'message': 'test'}): + res = main.hello_http(flask.request) + assert 'Hello, test!' in res + + +def test_hello_content_json(app): + with app.test_request_context(json={'name': 'test'}): + res = main.hello_content(flask.request) + assert 'Hello, test!' in res + + +def test_hello_content_urlencoded(app): + with app.test_request_context( + data={'name': 'test'}, + content_type='application/x-www-form-urlencoded'): + res = main.hello_content(flask.request) + assert 'Hello, test!' in res + + +def test_hello_method(app): + with app.test_request_context(method='GET'): + res = main.hello_method(flask.request) + assert 'Hello, World!' in res diff --git a/functions/helloworld/sample_http_test.py b/functions/helloworld/sample_http_test.py new file mode 100644 index 00000000000..316422ca87f --- /dev/null +++ b/functions/helloworld/sample_http_test.py @@ -0,0 +1,34 @@ +# 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. + +# [START functions_http_unit_test] +from unittest.mock import Mock + +import main + + +def test_print_name(): + name = 'test' + req = Mock(get_json=Mock(return_value={'message': name})) + + # Call tested function + assert main.hello_http(req) == 'Hello, {}!'.format(name) + + +def test_print_hello_world(): + req = Mock(get_json=Mock(return_value={})) + + # Call tested function + assert main.hello_http(req) == 'Hello, World!' +# [END functions_http_unit_test] diff --git a/functions/helloworld/sample_http_test_system.py b/functions/helloworld/sample_http_test_system.py new file mode 100644 index 00000000000..2f629881c31 --- /dev/null +++ b/functions/helloworld/sample_http_test_system.py @@ -0,0 +1,40 @@ +# 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. + +# [START functions_http_system_test] +import os +import uuid + +import requests + + +def test_no_args(): + BASE_URL = os.getenv('BASE_URL') + assert BASE_URL is not None + + res = requests.get('{}/hello_http'.format(BASE_URL)) + assert res.text == 'Hello, World!' + + +def test_args(): + BASE_URL = os.getenv('BASE_URL') + assert BASE_URL is not None + + name = str(uuid.uuid4()) + res = requests.post( + '{}/hello_http'.format(BASE_URL), + json={'message': name} + ) + assert res.text == 'Hello, {}!'.format(name) +# [END functions_http_system_test] diff --git a/functions/helloworld/sample_pubsub_test.py b/functions/helloworld/sample_pubsub_test.py new file mode 100644 index 00000000000..26ef363feaf --- /dev/null +++ b/functions/helloworld/sample_pubsub_test.py @@ -0,0 +1,38 @@ +# 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. + +# [START functions_pubsub_unit_test] +import base64 + +import main + + +def test_print_hello_world(capsys): + data = {} + + # Call tested function + main.hello_pubsub(data, None) + out, err = capsys.readouterr() + assert out == 'Hello, World!\n' + + +def test_print_name(capsys): + name = 'test' + data = {'data': base64.b64encode(name.encode())} + + # Call tested function + main.hello_pubsub(data, None) + out, err = capsys.readouterr() + assert out == 'Hello, {}!\n'.format(name) +# [END functions_pubsub_unit_test] diff --git a/functions/helloworld/sample_storage_test.py b/functions/helloworld/sample_storage_test.py new file mode 100644 index 00000000000..aac1769b26b --- /dev/null +++ b/functions/helloworld/sample_storage_test.py @@ -0,0 +1,27 @@ +# 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. + +# [START functions_storage_unit_test] +import main + + +def test_print(capsys): + name = 'test' + data = {'objectId': name} + + # Call tested function + main.hello_gcs(data, None) + out, err = capsys.readouterr() + assert out == 'File: {}.\n'.format(name) +# [END functions_storage_unit_test] diff --git a/functions/log/README.md b/functions/log/README.md new file mode 100644 index 00000000000..21bdd9548ec --- /dev/null +++ b/functions/log/README.md @@ -0,0 +1,11 @@ +Google Cloud Platform logo + +# Google Cloud Functions - Logging and Monitoring sample + +* [Writing and Viewing Logs from Cloud Functions documentation][docs] +* [Viewing Cloud Functions monitored metrics documentation][docs2] +* [Background functions sample source code][code] + +[docs]: https://cloud.google.com/functions/docs/monitoring/logging +[docs2]: https://cloud.google.com/functions/docs/monitoring/metrics +[code]: main.py diff --git a/functions/log/main.py b/functions/log/main.py new file mode 100644 index 00000000000..4353ec0ede4 --- /dev/null +++ b/functions/log/main.py @@ -0,0 +1,28 @@ +# 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. + +# [START functions_log_helloworld] +import logging + + +def hello_world(event, context): + """Background Cloud Function. + Args: + event (dict): The dictionary with data specific to the given event. + context (google.cloud.functions.Context): The Cloud Functions event + context. + """ + print('Hello, stdout!') + logging.warn('Hello, logging handler!') +# [END functions_log_helloworld] diff --git a/functions/log/main_test.py b/functions/log/main_test.py new file mode 100644 index 00000000000..5450a75434d --- /dev/null +++ b/functions/log/main_test.py @@ -0,0 +1,23 @@ +# 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. + + +import main + + +def test_hello_world(capsys): + main.hello_world(None, None) + + out, _ = capsys.readouterr() + assert "Hello, stdout!" in out diff --git a/functions/tips/README.md b/functions/tips/README.md new file mode 100644 index 00000000000..c6ff906dab9 --- /dev/null +++ b/functions/tips/README.md @@ -0,0 +1,11 @@ +Google Cloud Platform logo + +# Google Cloud Functions - Tips sample + +See: + +* [Cloud Functions Tips tutorial][tutorial] +* [Cloud Functions Tips sample source code][code] + +[tutorial]: https://cloud.google.com/functions/docs/bestpractices/tips +[code]: main.py diff --git a/functions/tips/main.py b/functions/tips/main.py new file mode 100644 index 00000000000..2ee083f42a9 --- /dev/null +++ b/functions/tips/main.py @@ -0,0 +1,126 @@ +# 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. + +# [START functions_tips_infinite_retries] +from datetime import datetime + +# The 'python-dateutil' package must be included in requirements.txt. +from dateutil import parser + +# [END functions_tips_infinite_retries] +# [START functions_tips_connection_pooling] +import requests + +# [END functions_tips_connection_pooling] + + +def file_wide_computation(): + return sum(range(10)) + + +def function_specific_computation(): + return sum(range(10)) + + +# [START functions_tips_lazy_globals] +# Always initialized (at cold-start) +non_lazy_global = file_wide_computation() + +# Declared at cold-start, but only initialized if/when the function executes +lazy_global = None + + +def lazy_globals(request): + """ + HTTP Cloud Function that uses lazily-initialized globals. + Args: + request (flask.Request): The request object. + Returns: + The response text, or any set of values that can be turned into a + Response object using `make_response` + . + """ + global lazy_global, non_lazy_global + + # This value is initialized only if (and when) the function is called + if not lazy_global: + lazy_global = function_specific_computation() + + return 'Lazy: {}, non-lazy: {}.'.format(lazy_global, non_lazy_global) +# [END functions_tips_lazy_globals] + + +# [START functions_tips_connection_pooling] +# Create a global HTTP session (which provides connection pooling) +session = requests.Session() + + +def connection_pooling(request): + """ + HTTP Cloud Function that uses a connection pool to make HTTP requests. + Args: + request (flask.Request): The request object. + Returns: + The response text, or any set of values that can be turned into a + Response object using `make_response` + . + """ + + # The URL to send the request to + url = 'http://example.com' + + # Process the request + response = session.get(url) + response.raise_for_status() + return 'Success!' +# [END functions_tips_connection_pooling] + + +# [START functions_tips_infinite_retries] +def avoid_infinite_retries(event, context): + timestamp = event.timestamp + + event_time = parser.parse(timestamp) + event_age = (datetime.now() - event_time).total_seconds() * 1000 + + # Ignore events that are too old + if event_age > 10000: + print('Dropped {} (age {}ms)'.format(context.event_id, event_age)) + return 'Timeout' + + # Do what the function is supposed to do + print('Processed {} (age {}ms)'.format(context.event_id, event_age)) + return +# [END functions_tips_infinite_retries] + + +# [START functions_tips_retry] +def retry_or_not(event, context): + from google import cloud + error_client = cloud.error_reporting.Client() + + if event.data.get('retry'): + try_again = True + else: + try_again = False + + try: + raise Exception('I failed you') + except Exception as e: + error_client.report_exception() + if try_again: + raise e # Raise the exception and try again + else: + return # Swallow the exception and don't retry +# [END functions_tips_retry] diff --git a/functions/tips/main_test.py b/functions/tips/main_test.py new file mode 100644 index 00000000000..110fc6459e4 --- /dev/null +++ b/functions/tips/main_test.py @@ -0,0 +1,90 @@ +# 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. + +import datetime +from unittest.mock import MagicMock, Mock, patch + +import flask +import pytest +import requests +import responses + +import main + + +# Create a fake "app" for generating test request contexts. +@pytest.fixture(scope="module") +def app(): + return flask.Flask(__name__) + + +def test_lazy_globals(app): + with app.test_request_context(): + main.lazy_globals(flask.request) + + +@responses.activate +def test_connection_pooling_200(app): + responses.add(responses.GET, 'http://example.com', + json={'status': 'OK'}, status=200) + with app.test_request_context(): + main.connection_pooling(flask.request) + + +@responses.activate +def test_connection_pooling_404(app): + responses.add(responses.GET, 'http://example.com', + json={'error': 'not found'}, status=404) + with app.test_request_context(): + with pytest.raises(requests.exceptions.HTTPError): + main.connection_pooling(flask.request) + + +def test_avoid_infinite_retries(capsys): + now = datetime.datetime.now() + + with patch('main.datetime', wraps=datetime.datetime) as datetime_mock: + datetime_mock.now = Mock(return_value=now) + old_event = Mock( + timestamp=(now - datetime.timedelta(seconds=15)).isoformat()) + young_event = Mock( + timestamp=(now - datetime.timedelta(seconds=5)).isoformat()) + context = Mock(event_id='fake_event_id') + + main.avoid_infinite_retries(old_event, context) + out, _ = capsys.readouterr() + assert 'Dropped {} (age 15000.0ms)'.format(context.event_id) in out + + main.avoid_infinite_retries(young_event, context) + out, _ = capsys.readouterr() + assert 'Processed {} (age 5000.0ms)'.format(context.event_id) in out + + +def test_retry_or_not(): + with patch('google.cloud') as cloud_mock: + + error_client = MagicMock() + + cloud_mock.error_reporting = MagicMock( + Client=MagicMock(return_value=error_client)) + + event = Mock(data={}) + main.retry_or_not(event, None) + assert error_client.report_exception.call_count == 1 + + event.data = {'retry': True} + with pytest.raises(Exception): + main.retry_or_not(event, None) + + assert error_client.report_exception.call_count == 2 diff --git a/functions/tips/requirements.txt b/functions/tips/requirements.txt new file mode 100644 index 00000000000..3aeddb98016 --- /dev/null +++ b/functions/tips/requirements.txt @@ -0,0 +1,2 @@ +google-cloud-error-reporting==0.30.0 +python-dateutil==2.7.3 \ No newline at end of file