├── .gitignore ├── Manifest.in ├── README.md ├── cloudfn ├── __init__.py ├── cli.py ├── django_handler.py ├── docker │ ├── DockerfilePython2-7 │ ├── DockerfilePython3-5 │ └── DockerfilePython3-5-alt ├── flask_handler.py ├── google_account.py ├── hooks │ ├── hook-django.contrib.py │ ├── hook-google.cloud.bigquery.py │ ├── hook-google.cloud.bigtable.py │ ├── hook-google.cloud.core.py │ ├── hook-google.cloud.datastore.py │ ├── hook-google.cloud.dns.py │ ├── hook-google.cloud.error_reporting.py │ ├── hook-google.cloud.firestore.py │ ├── hook-google.cloud.language.py │ ├── hook-google.cloud.logging.py │ ├── hook-google.cloud.monitoring.py │ ├── hook-google.cloud.pubsub.py │ ├── hook-google.cloud.resource_manager.py │ ├── hook-google.cloud.runtimeconfig.py │ ├── hook-google.cloud.spanner.py │ ├── hook-google.cloud.speech.py │ ├── hook-google.cloud.storage.py │ ├── hook-google.cloud.translate.py │ ├── hook-google.cloud.vision.py │ ├── hook-google.gax.py │ ├── hook-grpc.py │ ├── hook-rest_framework.py │ ├── hooks-django.middleware.py │ └── unbuffered.py ├── http.py ├── pubsub.py ├── storage.py ├── template │ └── index.js └── wsgi_util.py ├── examples ├── django │ ├── deploy.sh │ ├── function.py │ ├── manage.py │ ├── mysite │ │ ├── __init__.py │ │ ├── myapp │ │ │ ├── __init__.py │ │ │ ├── serializers.py │ │ │ ├── urls.py │ │ │ └── views.py │ │ ├── settings.py │ │ ├── urls.py │ │ └── wsgi.py │ └── requirements.txt ├── flask │ ├── deploy.sh │ ├── function.py │ └── requirements.txt ├── http │ ├── deploy.sh │ ├── function.py │ └── requirements.txt ├── pubsub │ ├── deploy.sh │ ├── function.py │ └── requirements.txt └── storage │ ├── deploy.sh │ ├── function.py │ └── requirements.txt ├── lint.sh ├── setup.py └── upload.sh /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build/ 3 | dist/ 4 | func.spec 5 | venv/ 6 | linux-venv/ 7 | *.pyc 8 | .DS_Store 9 | /*.egg-info 10 | examples/*/cloudfn/ 11 | __pycache__ 12 | -------------------------------------------------------------------------------- /Manifest.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include cloudfn/docker/* 3 | include cloudfn/hooks/* 4 | include cloudfn/template/* 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![PyPI version](https://badge.fury.io/py/pycloudfn.svg)](https://badge.fury.io/py/pycloudfn) 2 | 3 | [NOTE]: This is a highly experimental (and proof of concept) library so do not expect all python packages to work flawlessly. Also, cloud functions are now (Summer 2018) rolling out native support for python3 in EAP so that also might be an option, check out the #functions channel on googlecloud-community.slack.com where the product managers hang around and open to help you out! 4 | 5 | # cloud-functions-python 6 | `py-cloud-fn` is a CLI tool that allows you to write and deploy [Google cloud functions](https://cloud.google.com/functions/) in pure python, supporting python 2.7 and 3.5 7 | (thanks to @MitalAshok for helping on the code compatibility). 8 | No javascript allowed! 9 | The goal of this library is to be able to let developers write light weight functions 10 | in idiomatic python without needing to worry about node.js. It works OOTB with [pip](https://pypi.python.org/pypi), 11 | just include a file named `requirements.txt` that is structured like this: 12 | 13 | ``` 14 | pycloudfn==0.1.206 15 | jsonpickle==0.9.4 16 | ``` 17 | 18 | as you normally would when building any python application. When building (for production), the library 19 | will pick up this file and make sure to install the dependencies. It will do so while caching all dependencies 20 | in a [virtual environment](https://virtualenv.pypa.io/en/stable/), to speed up subsequent builds. 21 | 22 | TLDR, look at the [examples](https://github.com/MartinSahlen/cloud-functions-python/tree/master/examples) 23 | 24 | Run `pip install pycloudfn` to get it. 25 | You need to have [Google cloud SDK](https://cloud.google.com/sdk/downloads) installed, as well as 26 | the [Cloud functions emulator](https://github.com/GoogleCloudPlatform/cloud-functions-emulator/) and `npm` if you want to 27 | test your function locally. 28 | 29 | You also need **Docker** installed and running as well as the **gcloud** CLI. Docker is needed to build for the production environment, regardless of you local development environment. 30 | 31 | Currently, `http`, `pubsub` and `bucket` events are supported (no firebase). 32 | 33 | # Usage 34 | 35 | ## CLI 36 | 37 | ``` 38 | usage: py-cloud-fn [-h] [-p] [-f FILE_NAME] [--python_version {2.7,3.5,3.6}] 39 | function_name {http,pubsub,bucket} 40 | 41 | Build a GCP Cloud Function in python. 42 | 43 | positional arguments: 44 | function_name the name of your cloud function 45 | {http,pubsub,bucket} the trigger type of your cloud function 46 | 47 | optional arguments: 48 | -h, --help show this help message and exit 49 | -p, --production Build function for production environment 50 | -i, --production_image 51 | Docker image to use for building production environment 52 | -f FILE_NAME, --file_name FILE_NAME 53 | The file name of the file you wish to build 54 | --python_version {2.7,3.5} 55 | The python version you are targeting, only applies 56 | when building for production 57 | ``` 58 | 59 | Usage is meant to be pretty idiomatic: 60 | 61 | Run `py-cloud-fn ` to build your finished function. 62 | Run with `-h` to get some guidance on options. The library will assume that you have a file named `main.py` if not specified. 63 | 64 | The library will create a `cloudfn` folder wherever it is used, which can safely be put in `.gitignore`. It contains build files and cache for python packages. 65 | 66 | ``` 67 | $DJANGO_SETTINGS_MODULE=mysite.settings py-cloud-fn my-function http -f function.py --python_version 3.5 68 | 69 | _____ _ _ __ 70 | | __ \ | | | | / _| 71 | | |__) | _ ______ ___| | ___ _ _ __| |______| |_ _ __ 72 | | ___/ | | |______/ __| |/ _ \| | | |/ _` |______| _| '_ \ 73 | | | | |_| | | (__| | (_) | |_| | (_| | | | | | | | 74 | |_| \__, | \___|_|\___/ \__,_|\__,_| |_| |_| |_| 75 | __/ | 76 | |___/ 77 | 78 | Function: my-function 79 | File: function.py 80 | Trigger: http 81 | Python version: 3.5 82 | Production: False 83 | 84 | ⠴ Building, go grab a coffee... 85 | ⠋ Generating javascript... 86 | ⠼ Cleaning up... 87 | 88 | Elapsed time: 37.6s 89 | Output: ./cloudfn/target/index.js 90 | 91 | ``` 92 | 93 | ## Dependencies 94 | This library works with [pip](https://pypi.python.org/pypi) OOTB. Just add your `requirements.txt` file in the root 95 | of the repo and you are golden. It obviously needs `pycloudfn` to be present. 96 | 97 | ## Authentication 98 | Since this is not really supported by google, there is one thing that needs to be done to 99 | make this work smoothly: You can't use the default clients directly. It's solvable though, 100 | just do 101 | 102 | ```python 103 | from cloudfn.google_account import get_credentials 104 | 105 | biquery_client = bigquery.Client(credentials=get_credentials()) 106 | ``` 107 | 108 | And everything is taken care off for you!! no more actions need be done. 109 | 110 | ### Handling a http request 111 | 112 | Look at the [Request](https://github.com/MartinSahlen/cloud-functions-python/blob/master/cloudfn/http.py) 113 | object for the structure 114 | 115 | ```python 116 | from cloudfn.http import handle_http_event, Response 117 | 118 | 119 | def handle_http(req): 120 | return Response( 121 | status_code=200, 122 | body={'key': 2}, 123 | headers={'content-type': 'application/json'}, 124 | ) 125 | 126 | 127 | handle_http_event(handle_http) 128 | 129 | ``` 130 | 131 | If you don't return anything, or return something different than a `cloudfn.http.Response` object, the function will return a `200 OK` with an empty body. The body can be either a string, list or dictionary, other values will be forced to a string. 132 | 133 | ### Handling http with Flask 134 | 135 | [Flask](http://flask.pocoo.org/) is a great framework for building microservices. 136 | The library supports flask OOTB. If you need to have some routing / parsing and 137 | verification logic in place, flask might be a good fit! Have a look at the 138 | [example](https://github.com/MartinSahlen/cloud-functions-python/tree/master/examples/flask) 139 | to see how easy it is! 140 | 141 | ```python 142 | from cloudfn.flask_handler import handle_http_event 143 | from cloudfn.google_account import get_credentials 144 | from flask import Flask, request 145 | from flask.json import jsonify 146 | from google.cloud import bigquery 147 | 148 | app = Flask('the-function') 149 | biquery_client = bigquery.Client(credentials=get_credentials()) 150 | 151 | 152 | @app.route('/', methods=['POST', 'GET']) 153 | def hello(): 154 | print request.headers 155 | return jsonify(message='Hello world!', json=request.get_json()), 201 156 | 157 | 158 | @app.route('/lol') 159 | def helloLol(): 160 | return 'Hello lol!' 161 | 162 | 163 | @app.route('/bigquery-datasets', methods=['POST', 'GET']) 164 | def bigquery(): 165 | datasets = [] 166 | for dataset in biquery_client.list_datasets(): 167 | datasets.append(dataset.name) 168 | return jsonify(message='Hello world!', datasets={ 169 | 'datasets': datasets 170 | }), 201 171 | 172 | 173 | handle_http_event(app) 174 | ``` 175 | 176 | ### Handling http with Django 177 | 178 | [Django](https://www.djangoproject.com/) is a great framework for building microservices. 179 | The library supports django OOTB. Assuming you have setup your django application in a 180 | normal fashion, this should be what you need. You need to setup a pretty minimal django 181 | application (no database etc) to get it working. It might be a little overkill to squeeze 182 | django into a cloud function, but there are some pretty nice features for doing request 183 | verification and routing in django using for intance 184 | [django rest framework](http://www.django-rest-framework.org/). 185 | 186 | See the [example](https://github.com/MartinSahlen/cloud-functions-python/tree/master/examples/django) 187 | for how you can handle a http request using django. 188 | 189 | ```python 190 | from cloudfn.django_handler import handle_http_event 191 | from mysite.wsgi import application 192 | 193 | 194 | handle_http_event(application) 195 | ``` 196 | 197 | ### Handling a bucket event 198 | 199 | look at the [Object](https://github.com/MartinSahlen/cloud-functions-python/blob/master/cloudfn/storage.py) 200 | for the structure, it follows the convention in the [Storage API](https://cloud.google.com/storage/docs/json_api/v1/objects) 201 | 202 | ```python 203 | from cloudfn.storage import handle_bucket_event 204 | import jsonpickle 205 | 206 | 207 | def bucket_handler(obj): 208 | print jsonpickle.encode(obj) 209 | 210 | 211 | handle_bucket_event(bucket_handler) 212 | ``` 213 | 214 | ### Handling a pubsub message 215 | 216 | Look at the [Message](https://github.com/MartinSahlen/cloud-functions-python/blob/master/cloudfn/pubsub.py) 217 | for the structure, it follows the convention in the [Pubsub API](https://cloud.google.com/pubsub/docs/reference/rest/v1/PubsubMessage) 218 | 219 | ```python 220 | from cloudfn.pubsub import handle_pubsub_event 221 | import jsonpickle 222 | 223 | 224 | def pubsub_handler(message): 225 | print jsonpickle.encode(message) 226 | 227 | 228 | handle_pubsub_event(pubsub_handler) 229 | ``` 230 | 231 | ## Deploying a function 232 | I have previously built [go-cloud-fn](https://github.com/MartinSahlen/go-cloud-fn/), in which there is a complete CLI available for you to deploy a function. I did not want to go there now, but rather be concerned about `building` the function and be super light weight. Deploying a function can be done like this: 233 | 234 | (If you have the [emulator](https://github.com/GoogleCloudPlatform/cloud-functions-emulator) installed, 235 | just swap `gcloud beta functions` with `npm install && functions` and you are golden!). 236 | 237 | ### HTTP 238 | 239 | ```sh 240 | py-cloud-fn my-function http --production && \ 241 | cd cloudfn/target && gcloud beta functions deploy my-function \ 242 | --trigger-http --stage-bucket && cd ../.. 243 | ``` 244 | 245 | ### Storage 246 | 247 | ```sh 248 | py-cloud-fn my-bucket-function bucket -p && cd cloudfn/target && \ 249 | gcloud beta functions deploy my-bucket-function --trigger-bucket \ 250 | --stage-bucket && cd ../.. 251 | ``` 252 | 253 | ### Pubsub 254 | 255 | ```sh 256 | py-cloud-fn my-topic-function bucket -p && cd cloudfn/target && \ 257 | gcloud beta functions deploy my-topic-function --trigger-topic \ 258 | --stage-bucket && cd ../.. 259 | ``` 260 | 261 | ### Adding support for packages that do not work 262 | 263 | - Look at the build output for what might be wrong. 264 | - Look for what modules might be missing. 265 | - Add a line-delimited file for **hidden imports** and a folder called **cloudfn-hooks** 266 | in the root of your repo, see more at [Pyinstaller](https://pyinstaller.readthedocs.io/en/stable/hooks.html) for how it works. Check out [this](https://github.com/MartinSahlen/cloud-functions-python/blob/master/cloudfn/hooks) 267 | for how to add hooks. 268 | 269 | ### Troubleshooting 270 | 271 | When things blow up, the first thing to try is to delete the `cloudfn` cache 272 | folder. Things might go a bit haywire when builds are interrupted or other 273 | circumstances. It just might save the day! Please get in touch at twitter if 274 | you bump into anything: @MartinSahlen 275 | 276 | 277 | ## License 278 | 279 | Copyright © 2017 Martin Sahlen 280 | 281 | Distributed under the MIT License 282 | -------------------------------------------------------------------------------- /cloudfn/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MartinSahlen/cloud-functions-python/97f74f375485972e999d2ec6d8feabf61604a78e/cloudfn/__init__.py -------------------------------------------------------------------------------- /cloudfn/cli.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import subprocess 3 | import os 4 | import sys 5 | import argparse 6 | from jinja2 import Template 7 | from pyspin.spin import make_spin, Default 8 | import time 9 | 10 | 11 | def package_root(): 12 | return os.path.dirname(__file__) + '/' 13 | 14 | 15 | def hooks_path(python_version, production): 16 | if production: 17 | return cache_dir(python_version) + \ 18 | '/lib/python' + python_version + '/site-packages/cloudfn/hooks' 19 | return package_root() + 'hooks' 20 | 21 | 22 | def cache_dir(python_version): 23 | return 'pip-cache-' + str(python_version) 24 | 25 | 26 | def image_name(python_version): 27 | return 'pycloudfn-builder' + str(python_version) 28 | 29 | 30 | def docker_path(): 31 | return package_root() + 'docker/' 32 | 33 | 34 | def output_name(): 35 | return 'func' 36 | 37 | 38 | def get_django_settings(): 39 | m = os.environ.get('DJANGO_SETTINGS_MODULE', '') 40 | if m == '': 41 | return m 42 | return 'DJANGO_SETTINGS_MODULE='+m 43 | 44 | 45 | def dockerfile(python_version): 46 | return 'DockerfilePython' + python_version.replace('.', '-') 47 | 48 | 49 | def pip_prefix(python_version): 50 | return 'python' + python_version + ' -m ' 51 | 52 | 53 | def build_in_docker(file_name, python_version, production_image): 54 | cwd = os.getcwd() 55 | cmds = [] 56 | if not production_image: 57 | cmds = cmds + [ 58 | 'docker', 'build', '-f', docker_path() + dockerfile(python_version), 59 | '-t', image_name(python_version), docker_path(), '&&'] 60 | 61 | cmds = cmds + ['docker', 'run', '--rm', '-ti', '-v', cwd + ':/app'] 62 | 63 | if production_image: 64 | cmds.append(production_image) 65 | else: 66 | cmds.append(image_name(python_version)) 67 | 68 | cmds = cmds + [ 69 | '/bin/sh', '-c', 70 | '\'cd /app && test -d cloudfn || mkdir cloudfn && cd cloudfn ' 71 | '&& test -d ' + cache_dir(python_version) + ' || virtualenv ' + 72 | ' -p python' + python_version + ' ' + 73 | cache_dir(python_version) + ' ' + 74 | '&& . ' + cache_dir(python_version) + '/bin/activate && ' + 75 | 'test -f ../requirements.txt && ' + pip_prefix(python_version) + 76 | 'pip install -r ../requirements.txt ' + 77 | '|| echo no requirements.txt present && ' + 78 | get_django_settings() + ' ' + 79 | ' '.join(build(file_name, python_version, True)) + '\'', 80 | ] 81 | return cmds 82 | 83 | 84 | def build(file_name, python_version, production): 85 | base = [ 86 | 'pyinstaller', '../' + file_name, '-y', '-n', output_name(), 87 | '--clean', '--onedir', 88 | '--paths', '../', 89 | '--additional-hooks-dir', hooks_path(python_version, production), 90 | '--runtime-hook', 91 | hooks_path(python_version, production) + '/unbuffered.py', 92 | '--hidden-import', 'htmlentitydefs', 93 | '--hidden-import', 'HTMLParser', 94 | '--hidden-import', 'Cookie', 95 | '--exclude-module', 'jinja2.asyncsupport', 96 | '--exclude-module', 'jinja2.asyncfilters', 97 | ] 98 | prefix = '' 99 | if os.path.isdir(prefix + './cloudfn-hooks'): 100 | base.append('--additional-hooks-dir') 101 | base.append('../cloudfn-hooks') 102 | if os.path.isfile(prefix + './.hidden-imports'): 103 | with open(prefix + '.hidden-imports') as f: 104 | for line in f: 105 | if not f == '': 106 | base.append('--hidden-import') 107 | base.append(line.rstrip()) 108 | if not production: 109 | base.insert(0, 'test -d cloudfn || mkdir cloudfn && cd cloudfn && ') 110 | return base 111 | 112 | 113 | def build_cmd(file_name, python_version, production, production_image): 114 | if production: 115 | return build_in_docker(file_name, python_version, production_image) 116 | return build(file_name, python_version, production) 117 | 118 | 119 | def build_function( 120 | function_name, 121 | file_name, 122 | trigger_type, 123 | python_version, 124 | production, 125 | production_image, 126 | verbose): 127 | 128 | start = time.time() 129 | 130 | print(''' 131 | _____ _ _ __ 132 | | __ \ | | | | / _| 133 | | |__) | _ ______ ___| | ___ _ _ __| |______| |_ _ __ 134 | | ___/ | | |______/ __| |/ _ \| | | |/ _` |______| _| '_ \\ 135 | | | | |_| | | (__| | (_) | |_| | (_| | | | | | | | 136 | |_| \__, | \___|_|\___/ \__,_|\__,_| |_| |_| |_| 137 | __/ | 138 | |___/ 139 | ''') 140 | print('''Function: {function_name} 141 | File: {file_name} 142 | Trigger: {trigger_type} 143 | Python version: {python_version} 144 | Production: {production} 145 | Production Image: {production_image} 146 | '''.format( 147 | function_name=function_name, 148 | file_name=file_name, 149 | trigger_type=trigger_type, 150 | python_version=python_version, 151 | production=production, 152 | production_image=production_image, 153 | ) 154 | ) 155 | 156 | stdout = subprocess.PIPE 157 | stderr = subprocess.STDOUT 158 | if verbose: 159 | stdout = sys.stdout 160 | stderr = sys.stderr 161 | 162 | (p, output) = run_build_cmd( 163 | ' '.join(build_cmd(file_name, python_version, production, production_image)), 164 | stdout, 165 | stderr) 166 | if p.returncode == 0: 167 | build_javascript(function_name, trigger_type) 168 | else: 169 | print('\nBuild failed!' 170 | 'See the build output below for what might have went wrong:') 171 | print(output[0]) 172 | sys.exit(p.returncode) 173 | (c, co) = cleanup() 174 | if c.returncode == 0: 175 | end = time.time() 176 | print(''' 177 | Elapsed time: {elapsed}s 178 | Output: ./cloudfn/target/index.js 179 | '''.format(elapsed=round((end - start), 1))) 180 | else: 181 | print('\nSomething went wrong when cleaning up: ' + co[0]) 182 | sys.exit(c.returncode) 183 | 184 | 185 | @make_spin(Default, 'Building, go grab a coffee...') 186 | def run_build_cmd(cmd, stdout, stderr): 187 | p = subprocess.Popen( 188 | cmd, 189 | stdout=stdout, 190 | stderr=stderr, 191 | shell=True) 192 | output = p.communicate() 193 | return (p, output) 194 | 195 | 196 | @make_spin(Default, 'Generating javascript...') 197 | def build_javascript(function_name, trigger_type): 198 | js = open(package_root() + 'template/index.js').read() 199 | t = Template(js) 200 | rendered_js = t.render(config={ 201 | 'output_name': output_name(), 202 | 'function_name': function_name, 203 | 'trigger_http': trigger_type == 'http', 204 | } 205 | ) 206 | open('cloudfn/index.js', 'w').write(rendered_js) 207 | open('cloudfn/package.json', 'w').write('''{ 208 | "name": "target", 209 | "version": "1.0.0", 210 | "description": "", 211 | "main": "index.js", 212 | "author": "", 213 | "license": "ISC", 214 | "dependencies": { 215 | "google-auto-auth": "^0.7.0" 216 | } 217 | } 218 | ''') 219 | 220 | 221 | @make_spin(Default, 'Cleaning up...') 222 | def cleanup(): 223 | p = subprocess.Popen( 224 | 'cd cloudfn && rm -rf target && mkdir target && mv index.js target ' + 225 | '&& mv package.json target && mv dist target', 226 | stdout=subprocess.PIPE, 227 | stderr=subprocess.STDOUT, 228 | shell=True) 229 | output = p.communicate() 230 | return (p, output) 231 | 232 | 233 | def main(): 234 | parser = argparse.ArgumentParser( 235 | description='Build a GCP Cloud Function in python.' 236 | ) 237 | parser.add_argument('function_name', type=str, 238 | help='the name of your cloud function') 239 | parser.add_argument('trigger_type', type=str, 240 | help='the trigger type of your cloud function', 241 | choices=['http', 'pubsub', 'bucket']) 242 | parser.add_argument('-p', '--production', action='store_true', 243 | help='Build function for production environment') 244 | parser.add_argument('-i', '--production_image', type=str, 245 | help='Docker image to use for building production environment') 246 | parser.add_argument('-f', '--file_name', type=str, default='main.py', 247 | help='The file name of the file you wish to build') 248 | parser.add_argument('--python_version', type=str, default='2.7', 249 | help='The python version you are targeting, ' 250 | 'only applies when building for production', 251 | choices=['2.7', '3.5']) 252 | parser.add_argument('-v', '--verbose', action='store_true', 253 | help='Build in verbose mode ' 254 | 'showing full build output') 255 | 256 | args = parser.parse_args() 257 | build_function(args.function_name, 258 | args.file_name, 259 | args.trigger_type, 260 | args.python_version, 261 | args.production, 262 | args.production_image, 263 | args.verbose, 264 | ) 265 | -------------------------------------------------------------------------------- /cloudfn/django_handler.py: -------------------------------------------------------------------------------- 1 | import json 2 | import sys 3 | 4 | from django.core.handlers.wsgi import WSGIRequest 5 | 6 | from .wsgi_util import wsgi 7 | 8 | 9 | def handle_http_event(app): 10 | environ = wsgi(json.loads(sys.stdin.read())) 11 | app.load_middleware() 12 | resp = app.get_response(WSGIRequest(environ)) 13 | 14 | body = '' 15 | if resp.streaming: 16 | for content in resp.streaming_content: 17 | body += content 18 | else: 19 | body = resp.content.decode('utf-8') 20 | 21 | headers = {} 22 | for header in resp.items(): 23 | headers[header[0]] = header[1] 24 | resp.close() 25 | 26 | sys.stdout.write(json.dumps({ 27 | 'body': body, 28 | 'status_code': resp.status_code, 29 | 'headers': headers, 30 | })) 31 | -------------------------------------------------------------------------------- /cloudfn/docker/DockerfilePython2-7: -------------------------------------------------------------------------------- 1 | FROM ubuntu:17.10 2 | 3 | # Update 4 | RUN \ 5 | apt-get update && \ 6 | DEBIAN_FRONTEND=noninteractive \ 7 | apt-get install -y \ 8 | python2.7 \ 9 | python-pip \ 10 | && \ 11 | apt-get clean && \ 12 | rm -rf /var/lib/apt/lists/* 13 | 14 | # Install app dependencies 15 | RUN pip install pip==9.0.1 16 | RUN pip install virtualenv==15.1.0 17 | -------------------------------------------------------------------------------- /cloudfn/docker/DockerfilePython3-5: -------------------------------------------------------------------------------- 1 | FROM ubuntu:16.04 2 | 3 | # Update 4 | RUN \ 5 | apt-get update && \ 6 | DEBIAN_FRONTEND=noninteractive \ 7 | apt-get install -y \ 8 | python3.5 \ 9 | python3-pip \ 10 | && \ 11 | apt-get clean && \ 12 | rm -rf /var/lib/apt/lists/* 13 | 14 | # Install app dependencies 15 | RUN python3.5 -m pip install pip==9.0.1 16 | RUN python3.5 -m pip install virtualenv==15.1.0 17 | -------------------------------------------------------------------------------- /cloudfn/docker/DockerfilePython3-5-alt: -------------------------------------------------------------------------------- 1 | FROM python:3.5-jessie 2 | 3 | RUN pip install virtualenv 4 | -------------------------------------------------------------------------------- /cloudfn/flask_handler.py: -------------------------------------------------------------------------------- 1 | import json 2 | import sys 3 | from io import StringIO 4 | 5 | import six 6 | from six.moves.urllib_parse import urlparse 7 | from werkzeug.datastructures import Headers 8 | 9 | # from .wsgi_util import wsgi 10 | 11 | 12 | def handle_http_event(app): 13 | req_json = json.loads(sys.stdin.read()) 14 | c = urlparse(req_json['url']) 15 | path = c.path 16 | if path == '': 17 | path = '/' 18 | 19 | body = StringIO(req_json.get('body', u'')) 20 | 21 | req_headers = req_json.get('headers', None) 22 | h = Headers() 23 | if req_headers is not None: 24 | for key, value in six.iteritems(req_headers): 25 | h.add(key, value) 26 | 27 | with app.test_request_context( 28 | path=path, 29 | input_stream=body, 30 | method=req_json.get('method', 'GET'), 31 | headers=h, 32 | query_string=c.query): 33 | resp = app.full_dispatch_request() 34 | body = resp.get_data() 35 | try: 36 | body = json.loads(body) 37 | except: 38 | pass 39 | 40 | headers = {} 41 | for header in resp.headers: 42 | if header[0] in headers: 43 | headers[header[0]] += ', ' + header[1] 44 | else: 45 | headers[header[0]] = header[1] 46 | 47 | sys.stdout.write(json.dumps({ 48 | 'body': body, 49 | 'status_code': resp.status_code, 50 | 'headers': headers, 51 | })) 52 | -------------------------------------------------------------------------------- /cloudfn/google_account.py: -------------------------------------------------------------------------------- 1 | import os 2 | from google.oauth2 import credentials 3 | 4 | 5 | def get_credentials(): 6 | return credentials.Credentials(os.getenv('GOOGLE_OAUTH_TOKEN', '')) 7 | -------------------------------------------------------------------------------- /cloudfn/hooks/hook-django.contrib.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import collect_submodules 2 | 3 | hiddenimports = collect_submodules('django.contrib') 4 | -------------------------------------------------------------------------------- /cloudfn/hooks/hook-google.cloud.bigquery.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import copy_metadata 2 | 3 | datas = copy_metadata('google-cloud-bigquery') 4 | datas += copy_metadata('google-cloud-core') 5 | datas += copy_metadata('google-api-core') 6 | -------------------------------------------------------------------------------- /cloudfn/hooks/hook-google.cloud.bigtable.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import copy_metadata 2 | 3 | datas = copy_metadata('google-cloud-bigtable') 4 | datas += copy_metadata('google-cloud-core') 5 | datas += copy_metadata('google-api-core') -------------------------------------------------------------------------------- /cloudfn/hooks/hook-google.cloud.core.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import copy_metadata 2 | 3 | datas = copy_metadata('google-cloud-core') 4 | datas += copy_metadata('google-api-core') -------------------------------------------------------------------------------- /cloudfn/hooks/hook-google.cloud.datastore.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import copy_metadata 2 | 3 | datas = copy_metadata('google-cloud-datastore') 4 | datas += copy_metadata('google-cloud-core') 5 | datas += copy_metadata('google-api-core') 6 | -------------------------------------------------------------------------------- /cloudfn/hooks/hook-google.cloud.dns.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import copy_metadata 2 | 3 | datas = copy_metadata('google-cloud-dns') 4 | datas += copy_metadata('google-cloud-core') 5 | datas += copy_metadata('google-api-core') -------------------------------------------------------------------------------- /cloudfn/hooks/hook-google.cloud.error_reporting.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import copy_metadata 2 | 3 | datas = copy_metadata('google-cloud-error-reporting') 4 | datas += copy_metadata('google-cloud-core') 5 | datas += copy_metadata('google-api-core') -------------------------------------------------------------------------------- /cloudfn/hooks/hook-google.cloud.firestore.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import copy_metadata 2 | 3 | datas = copy_metadata('google-cloud-firestore') 4 | datas += copy_metadata('google-cloud-core') 5 | datas += copy_metadata('google-api-core') 6 | -------------------------------------------------------------------------------- /cloudfn/hooks/hook-google.cloud.language.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import copy_metadata 2 | 3 | datas = copy_metadata('google-cloud-language') 4 | datas += copy_metadata('google-cloud-core') 5 | datas += copy_metadata('google-api-core') -------------------------------------------------------------------------------- /cloudfn/hooks/hook-google.cloud.logging.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import copy_metadata 2 | 3 | datas = copy_metadata('google-cloud-logging') 4 | datas += copy_metadata('google-cloud-core') 5 | datas += copy_metadata('google-api-core') 6 | -------------------------------------------------------------------------------- /cloudfn/hooks/hook-google.cloud.monitoring.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import copy_metadata 2 | 3 | datas = copy_metadata('google-cloud-monitoring') 4 | datas += copy_metadata('google-cloud-core') 5 | datas += copy_metadata('google-api-core') 6 | -------------------------------------------------------------------------------- /cloudfn/hooks/hook-google.cloud.pubsub.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import copy_metadata 2 | 3 | datas = copy_metadata('google-cloud-pubsub') 4 | datas += copy_metadata('google-cloud-core') 5 | datas += copy_metadata('google-api-core') 6 | -------------------------------------------------------------------------------- /cloudfn/hooks/hook-google.cloud.resource_manager.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import copy_metadata 2 | 3 | datas = copy_metadata('google-cloud-resource-manager') 4 | datas += copy_metadata('google-cloud-core') 5 | datas += copy_metadata('google-api-core') 6 | -------------------------------------------------------------------------------- /cloudfn/hooks/hook-google.cloud.runtimeconfig.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import copy_metadata 2 | 3 | datas = copy_metadata('google-cloud-runtimeconfig') 4 | datas += copy_metadata('google-cloud-core') 5 | datas += copy_metadata('google-api-core') 6 | -------------------------------------------------------------------------------- /cloudfn/hooks/hook-google.cloud.spanner.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import copy_metadata 2 | 3 | datas = copy_metadata('google-cloud-spanner') 4 | datas += copy_metadata('google-cloud-core') 5 | datas += copy_metadata('google-api-core') 6 | -------------------------------------------------------------------------------- /cloudfn/hooks/hook-google.cloud.speech.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import copy_metadata 2 | 3 | datas = copy_metadata('google-cloud-speech') 4 | datas += copy_metadata('google-cloud-core') 5 | datas += copy_metadata('google-api-core') 6 | -------------------------------------------------------------------------------- /cloudfn/hooks/hook-google.cloud.storage.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import copy_metadata 2 | 3 | datas = copy_metadata('google-cloud-storage') 4 | datas += copy_metadata('google-cloud-core') 5 | datas += copy_metadata('google-api-core') 6 | -------------------------------------------------------------------------------- /cloudfn/hooks/hook-google.cloud.translate.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import copy_metadata 2 | 3 | datas = copy_metadata('google-cloud-translate') 4 | datas += copy_metadata('google-cloud-core') 5 | datas += copy_metadata('google-api-core') 6 | -------------------------------------------------------------------------------- /cloudfn/hooks/hook-google.cloud.vision.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import copy_metadata 2 | 3 | datas = copy_metadata('google-cloud-vision') 4 | datas += copy_metadata('google-cloud-core') 5 | datas += copy_metadata('google-api-core') 6 | -------------------------------------------------------------------------------- /cloudfn/hooks/hook-google.gax.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import copy_metadata 2 | 3 | datas = copy_metadata('google-gax') 4 | -------------------------------------------------------------------------------- /cloudfn/hooks/hook-grpc.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import copy_metadata 2 | 3 | datas = copy_metadata('grpcio') 4 | -------------------------------------------------------------------------------- /cloudfn/hooks/hook-rest_framework.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import collect_submodules 2 | 3 | hiddenimports = collect_submodules('rest_framework') 4 | -------------------------------------------------------------------------------- /cloudfn/hooks/hooks-django.middleware.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import collect_submodules 2 | 3 | hiddenimports = collect_submodules('django.middleware') 4 | -------------------------------------------------------------------------------- /cloudfn/hooks/unbuffered.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | 4 | class Unbuffered(object): 5 | def __init__(self, stream): 6 | self.stream = stream 7 | 8 | def write(self, data): 9 | self.stream.write(data) 10 | self.stream.flush() 11 | 12 | def writelines(self, datas): 13 | self.stream.writelines(datas) 14 | self.stream.flush() 15 | 16 | def __getattr__(self, attr): 17 | return getattr(self.stream, attr) 18 | 19 | 20 | sys.stdout = Unbuffered(sys.stdout) 21 | -------------------------------------------------------------------------------- /cloudfn/http.py: -------------------------------------------------------------------------------- 1 | import json 2 | import sys 3 | 4 | import six 5 | from six.moves.urllib_parse import urlparse 6 | 7 | 8 | class Request: 9 | def __init__(self, raw_json): 10 | self.headers = raw_json['headers'] 11 | self.method = raw_json['method'] 12 | self.body = raw_json['body'] 13 | self.url = raw_json['url'] 14 | self.ip = raw_json['remote_addr'] 15 | 16 | components = urlparse(self.url) 17 | self.path = components.path 18 | self.host = components.hostname 19 | self.scheme = components.scheme 20 | self.query = components.query 21 | self.port = components.port 22 | self.fragment = components.fragment 23 | self.params = components.params 24 | self.netloc = components.netloc 25 | 26 | 27 | class Response: 28 | def __init__(self, headers=None, body='', status_code=200): 29 | self.headers = {} if headers is None else headers 30 | if isinstance(body, (six.text_type, six.binary_type, dict, list)): 31 | self.body = body 32 | else: 33 | self.body = str(body) 34 | self.status_code = status_code 35 | 36 | def _json_string(self): 37 | return json.dumps({ 38 | 'body': self.body, 39 | 'status_code': self.status_code, 40 | 'headers': self.headers, 41 | }) 42 | 43 | 44 | def handle_http_event(handle_fn): 45 | req = Request(json.loads(sys.stdin.read())) 46 | res = handle_fn(req) 47 | if isinstance(res, Response): 48 | sys.stdout.write(res._json_string()) 49 | else: 50 | sys.stdout.write(Response()._json_string()) 51 | -------------------------------------------------------------------------------- /cloudfn/pubsub.py: -------------------------------------------------------------------------------- 1 | import json 2 | import sys 3 | 4 | from dateutil.parser import parse 5 | 6 | 7 | class Message: 8 | def __init__(self, raw_json): 9 | self.data = raw_json.get('data', None), 10 | self.message_id = raw_json.get('messageId', None) 11 | self.attributes = raw_json.get('attributes', None), 12 | self.publish_time = raw_json.get('publishTime', None) 13 | if self.publish_time is not None: 14 | self.publish_time = parse(self.publish_time) 15 | 16 | 17 | def handle_pubsub_event(handle_fn): 18 | handle_fn(Message(json.loads(sys.stdin.read()))) 19 | -------------------------------------------------------------------------------- /cloudfn/storage.py: -------------------------------------------------------------------------------- 1 | import json 2 | import sys 3 | 4 | from dateutil.parser import parse 5 | 6 | 7 | def _update_attributes(obj, d, keys, default=None): 8 | """Updates the attributes of `obj` with keys from `d` in camelCase. """ 9 | for key in keys: 10 | parts = key.split('_') 11 | camel_case = parts[0] + ''.join(map(str.title, parts[1:])) 12 | setattr(obj, key, d.get(camel_case, default)) 13 | 14 | 15 | class ACL: 16 | __ATTRIBUTES = ( 17 | 'kind', 'id', 'self_link', 'bucket', 'object', 'generation', 'entity', 18 | 'role', 'email', 'entity_id', 'domain', 'project_team', 'etag' 19 | ) 20 | 21 | def __init__(self, raw_json): 22 | _update_attributes(self, raw_json, self.__ATTRIBUTES) 23 | 24 | 25 | class Object: 26 | __ATTRIBUTES = ( 27 | 'kind', 'id', 'self_link', 'bucket', 'name', 'generation', 28 | 'metageneration', 'content_type', 'time_created', 'updated', 29 | 'time_deleted', 'storage_class', 'time_storage_class_updated', 'size', 30 | 'md5_hash', 'media_link', 'content_encoding', 'content_disposition', 31 | 'content_language', 'cache_control', 'metadata', 'owner', 'crc32c', 32 | 'component_count', 'customer_encryption' 33 | ) 34 | 35 | def __init__(self, raw_json): 36 | _update_attributes(self, raw_json, self.__ATTRIBUTES) 37 | 38 | if self.time_created is not None: 39 | self.time_created = parse(self.time_created) 40 | if self.updated is not None: 41 | self.updated = parse(self.updated) 42 | if self.time_deleted is not None: 43 | self.time_deleted = parse(self.time_deleted) 44 | if self.time_storage_class_updated is not None: 45 | self.time_storage_class_updated = \ 46 | parse(self.time_storage_class_updated) 47 | 48 | self.acl = list(map(ACL, raw_json.get('acl') or [])) 49 | 50 | 51 | def handle_bucket_event(handle_fn): 52 | handle_fn(Object(json.loads(sys.stdin.read()))) 53 | -------------------------------------------------------------------------------- /cloudfn/template/index.js: -------------------------------------------------------------------------------- 1 | var googleAuth = require('google-auto-auth')(); 2 | //Handle Background events according to spec 3 | function shimHandler(data) { 4 | return new Promise((resolve, reject) => { 5 | googleAuth.getToken(function (err, oauthToken) { 6 | if (err) { 7 | reject() 8 | } else { 9 | const p = require('child_process').execFile('./dist/{{config["output_name"]}}/{{config["output_name"]}}', { 10 | env: Object.assign(process.env, { 11 | 'GOOGLE_OAUTH_TOKEN': oauthToken, 12 | }) 13 | }); 14 | var lastMessage; 15 | p.stdin.setEncoding('utf-8'); 16 | //Log standard err messages to standard err 17 | p.stderr.on('data', (err) => { 18 | console.error(err.toString()); 19 | }) 20 | p.stdout.on('data', (out) => { 21 | console.log(out.toString()); 22 | lastMessage = out; 23 | }) 24 | p.on('close', (code) => { 25 | if (code !== 0) { 26 | //This means the shim failed / panicked. So we reject hard. 27 | reject(); 28 | } else { 29 | // Resolve the promise with the latest output from stdout 30 | // In case of shimming http, this is the response object. 31 | resolve(lastMessage); 32 | } 33 | }); 34 | //Write the object/message/request to the shim's stdin and signal 35 | //End of input. 36 | p.stdin.write(JSON.stringify(data)); 37 | p.stdin.end(); 38 | } 39 | }); 40 | }); 41 | } 42 | 43 | //Handle http request 44 | function handleHttp(req, res) { 45 | var requestBody; 46 | switch (req.get('content-type')) { 47 | case 'application/json': 48 | requestBody = JSON.stringify(req.body); 49 | break; 50 | case 'application/x-www-form-urlencoded': 51 | //The body parser for cloud functions does this, so just play along 52 | //with it, sorry man! Maybe we should construct some kind of proper 53 | //form request body? or not. let's keep it this way for now, as 54 | //This is how cloud functions behaves. 55 | req.set('content-type', 'application/json') 56 | requestBody = JSON.stringify(req.body); 57 | break; 58 | case 'application/octet-stream': 59 | requestBody = req.body; 60 | break; 61 | case 'text/plain': 62 | requestBody = req.body; 63 | break; 64 | } 65 | 66 | var fullUrl = req.protocol + '://' + req.get('host') + req.originalUrl; 67 | 68 | var httpRequest = { 69 | 'body': requestBody, 70 | 'headers': req.headers, 71 | 'method': req.method, 72 | 'remote_addr': req.ip, 73 | 'url': fullUrl 74 | }; 75 | 76 | shimHandler(httpRequest) 77 | .then((result) => { 78 | data = JSON.parse(result); 79 | res.status(data.status_code); 80 | res.set(data.headers) 81 | res.send(data.body); 82 | }) 83 | .catch(() => { 84 | res.status(500).end(); 85 | }) 86 | } 87 | 88 | //{% if config["trigger_http"] %} 89 | exports['{{config["function_name"]}}'] = function(req, res) { 90 | return handleHttp(req, res); 91 | }//{% else %} 92 | exports['{{config["function_name"]}}'] = function(event, callback) { 93 | return shimHandler(event.data).then(function() { 94 | callback(); 95 | }).catch(function() { 96 | callback(new Error("Function failed")); 97 | }); 98 | }//{% endif %} 99 | -------------------------------------------------------------------------------- /cloudfn/wsgi_util.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from io import BytesIO 3 | 4 | import six 5 | from six.moves.urllib_parse import urlparse 6 | 7 | 8 | def wsgi(raw_json): 9 | components = urlparse(raw_json['url']) 10 | path = components.path 11 | if path == '': 12 | path = '/' 13 | port = components.port 14 | if port is None: 15 | # We'll just leet it go 16 | port = 1337 17 | 18 | buf = bytearray(raw_json.get('body', u''), 'utf-8') 19 | 20 | environ = { 21 | 'PATH_INFO': path, 22 | 'QUERY_STRING': components.query, 23 | 'REQUEST_METHOD': raw_json['method'], 24 | 'SERVER_NAME': components.hostname, 25 | 'SERVER_PORT': port, 26 | 'SERVER_PROTOCOL': 'HTTP/1.1', 27 | 'SERVER_SOFTWARE': 'CloudFunctions/1.0', 28 | 'CONTENT_LENGTH': len(buf), 29 | 'wsgi.errors': sys.stderr, 30 | 'wsgi.input': BytesIO(buf), 31 | 'wsgi.multiprocess': False, 32 | 'wsgi.multithread': False, 33 | 'wsgi.run_once': False, 34 | 'wsgi.url_scheme': components.scheme, 35 | 'wsgi.version': (1, 0), 36 | } 37 | headers = raw_json.get('headers', None) 38 | if headers is not None: 39 | for key, value in six.iteritems(headers): 40 | environ['HTTP_' + key.replace('-', '_').upper()] = value 41 | return environ 42 | -------------------------------------------------------------------------------- /examples/django/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | DJANGO_SETTINGS_MODULE=mysite.settings \ 3 | py-cloud-fn $FUNC_NAME http --python_version ${PYTHON_VERSION:-2.7} -p -f function.py && \ 4 | cd cloudfn/target && gcloud beta functions deploy $FUNC_NAME \ 5 | --trigger-http --stage-bucket $STAGE_BUCKET --memory 2048MB && cd ../.. 6 | -------------------------------------------------------------------------------- /examples/django/function.py: -------------------------------------------------------------------------------- 1 | from cloudfn.django_handler import handle_http_event 2 | from mysite.wsgi import application 3 | 4 | 5 | handle_http_event(application) 6 | -------------------------------------------------------------------------------- /examples/django/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError: 10 | # The above import may fail for some other reason. Ensure that the 11 | # issue is really that Django is missing to avoid masking other 12 | # exceptions on Python 2. 13 | try: 14 | import django 15 | except ImportError: 16 | raise ImportError( 17 | "Couldn't import Django. Are you sure it's installed and " 18 | "available on your PYTHONPATH environment variable? Did you " 19 | "forget to activate a virtual environment?" 20 | ) 21 | raise 22 | execute_from_command_line(sys.argv) 23 | -------------------------------------------------------------------------------- /examples/django/mysite/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MartinSahlen/cloud-functions-python/97f74f375485972e999d2ec6d8feabf61604a78e/examples/django/mysite/__init__.py -------------------------------------------------------------------------------- /examples/django/mysite/myapp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MartinSahlen/cloud-functions-python/97f74f375485972e999d2ec6d8feabf61604a78e/examples/django/mysite/myapp/__init__.py -------------------------------------------------------------------------------- /examples/django/mysite/myapp/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | 4 | class DataSerializer(serializers.Serializer): 5 | objectId = serializers.CharField() 6 | email = serializers.EmailField() 7 | -------------------------------------------------------------------------------- /examples/django/mysite/myapp/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from .views import data_view 3 | 4 | urlpatterns = [ 5 | url(r'^data', data_view), 6 | ] 7 | -------------------------------------------------------------------------------- /examples/django/mysite/myapp/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import status 2 | from rest_framework.response import Response 3 | from rest_framework.decorators import api_view, permission_classes 4 | from rest_framework.permissions import AllowAny 5 | from .serializers import DataSerializer 6 | 7 | 8 | @api_view(http_method_names=['POST']) 9 | @permission_classes((AllowAny, ),) 10 | def data_view(request): 11 | serializer = DataSerializer(data=request.data) 12 | if serializer.is_valid(): 13 | return Response( 14 | status=status.HTTP_200_OK, 15 | data=serializer.validated_data) 16 | else: 17 | return Response( 18 | status=status.HTTP_400_BAD_REQUEST, data=serializer.errors) 19 | -------------------------------------------------------------------------------- /examples/django/mysite/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for mysite project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.11.1. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.11/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = '1+fkbb#o$j3&2xc=90k(cn^^41$^=5$w9#3-3frm2y4cz$%2qy' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = ['*'] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.auth', 35 | 'django.contrib.contenttypes', 36 | 'mysite.myapp', 37 | ] 38 | 39 | MIDDLEWARE = [] 40 | 41 | ROOT_URLCONF = 'mysite.urls' 42 | 43 | TEMPLATES = [ 44 | { 45 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 46 | 'DIRS': [], 47 | 'APP_DIRS': True, 48 | 'OPTIONS': { 49 | 'context_processors': [ 50 | 'django.template.context_processors.debug', 51 | 'django.template.context_processors.request', 52 | ], 53 | }, 54 | }, 55 | ] 56 | 57 | WSGI_APPLICATION = 'mysite.wsgi.application' 58 | 59 | 60 | # Database 61 | # https://docs.djangoproject.com/en/1.11/ref/settings/#databases 62 | 63 | DATABASES = {} 64 | 65 | # Password validation 66 | # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators 67 | 68 | AUTH_PASSWORD_VALIDATORS = [] 69 | 70 | 71 | # Internationalization 72 | # https://docs.djangoproject.com/en/1.11/topics/i18n/ 73 | 74 | LANGUAGE_CODE = 'en-us' 75 | 76 | TIME_ZONE = 'UTC' 77 | 78 | USE_I18N = True 79 | 80 | USE_L10N = True 81 | 82 | USE_TZ = True 83 | 84 | # Static files (CSS, JavaScript, Images) 85 | # https://docs.djangoproject.com/en/1.11/howto/static-files/ 86 | 87 | STATIC_URL = '/static/' 88 | -------------------------------------------------------------------------------- /examples/django/mysite/urls.py: -------------------------------------------------------------------------------- 1 | """mysite URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.11/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.conf.urls import url, include 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 15 | """ 16 | from django.conf.urls import url, include 17 | from .myapp.urls import urlpatterns 18 | 19 | urlpatterns = [ 20 | url(r'^', include(urlpatterns)), 21 | ] 22 | -------------------------------------------------------------------------------- /examples/django/mysite/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for mysite project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /examples/django/requirements.txt: -------------------------------------------------------------------------------- 1 | django==1.11 2 | pycloudfn==0.1.205 3 | djangorestframework==3.6.3 4 | -------------------------------------------------------------------------------- /examples/flask/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | py-cloud-fn $FUNC_NAME http --python_version ${PYTHON_VERSION:-2.7} -p -f function.py && \ 3 | cd cloudfn/target && gcloud beta functions deploy $FUNC_NAME \ 4 | --trigger-http --stage-bucket $STAGE_BUCKET --memory 2048MB && cd ../.. 5 | -------------------------------------------------------------------------------- /examples/flask/function.py: -------------------------------------------------------------------------------- 1 | from cloudfn.flask_handler import handle_http_event 2 | from cloudfn.google_account import get_credentials 3 | from flask import Flask, request 4 | from flask.json import jsonify 5 | from google.cloud import bigquery 6 | 7 | app = Flask('the-function') 8 | biquery_client = bigquery.Client(credentials=get_credentials()) 9 | 10 | 11 | @app.route('/', methods=['POST', 'GET']) 12 | def hello(): 13 | print request.headers 14 | return jsonify(message='Hello world!', json=request.get_json()), 201 15 | 16 | 17 | @app.route('/lol') 18 | def helloLol(): 19 | return 'Hello lol!' 20 | 21 | 22 | @app.route('/bigquery-datasets', methods=['POST', 'GET']) 23 | def bigquery(): 24 | datasets = [] 25 | for dataset in biquery_client.list_datasets(): 26 | datasets.append(dataset.name) 27 | return jsonify(message='Hello world!', datasets={ 28 | 'datasets': datasets 29 | }), 201 30 | 31 | 32 | handle_http_event(app) 33 | -------------------------------------------------------------------------------- /examples/flask/requirements.txt: -------------------------------------------------------------------------------- 1 | pycloudfn==0.1.205 2 | flask==0.12 3 | google-cloud-bigquery==0.24.0 4 | -------------------------------------------------------------------------------- /examples/http/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | py-cloud-fn $FUNC_NAME http --python_version ${PYTHON_VERSION:-2.7} -p -f function.py && \ 3 | cd cloudfn/target && gcloud beta functions deploy $FUNC_NAME \ 4 | --trigger-http --stage-bucket $STAGE_BUCKET --memory 2048MB && cd ../.. 5 | -------------------------------------------------------------------------------- /examples/http/function.py: -------------------------------------------------------------------------------- 1 | from cloudfn.http import handle_http_event, Response 2 | 3 | 4 | def handle_http(req): 5 | return Response( 6 | status_code=200, 7 | body={'key': 2}, 8 | headers={'content-type': 'application/json'}, 9 | ) 10 | 11 | 12 | handle_http_event(handle_http) 13 | -------------------------------------------------------------------------------- /examples/http/requirements.txt: -------------------------------------------------------------------------------- 1 | pycloudfn==0.1.205 2 | -------------------------------------------------------------------------------- /examples/pubsub/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | py-cloud-fn $FUNC_NAME http --python_version ${PYTHON_VERSION:-2.7} -p -f function.py && \ 3 | cd cloudfn/target && gcloud beta functions deploy $FUNC_NAME \ 4 | --trigger-topic $TRIGGER_TOPIC --stage-bucket $STAGE_BUCKET --memory 2048MB && cd ../.. 5 | -------------------------------------------------------------------------------- /examples/pubsub/function.py: -------------------------------------------------------------------------------- 1 | from cloudfn.pubsub import handle_pubsub_event 2 | import jsonpickle 3 | 4 | 5 | def pubsub_handler(message): 6 | print jsonpickle.encode(message) 7 | 8 | 9 | handle_pubsub_event(pubsub_handler) 10 | -------------------------------------------------------------------------------- /examples/pubsub/requirements.txt: -------------------------------------------------------------------------------- 1 | pycloudfn==0.1.205 2 | jsonpickle==0.9.4 3 | -------------------------------------------------------------------------------- /examples/storage/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | py-cloud-fn $FUNC_NAME bucket -p --python_version ${PYTHON_VERSION:-2.7} -f function.py && \ 3 | cd cloudfn/target && gcloud beta functions deploy $FUNC_NAME \ 4 | --trigger-bucket $TRIGGER_BUCKET --stage-bucket $STAGE_BUCKET --memory 2048MB && cd ../.. 5 | -------------------------------------------------------------------------------- /examples/storage/function.py: -------------------------------------------------------------------------------- 1 | from cloudfn.storage import handle_bucket_event 2 | import jsonpickle 3 | 4 | 5 | def bucket_handler(obj): 6 | print(jsonpickle.encode(obj)) 7 | 8 | 9 | handle_bucket_event(bucket_handler) 10 | -------------------------------------------------------------------------------- /examples/storage/requirements.txt: -------------------------------------------------------------------------------- 1 | pycloudfn==0.1.205 2 | jsonpickle==0.9.4 3 | -------------------------------------------------------------------------------- /lint.sh: -------------------------------------------------------------------------------- 1 | flake8 cloudfn 2 | flake8 examples --exclude cloudfn,manage.py 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='pycloudfn', 5 | version='0.1.209', 6 | description='GCP Cloud functions in python', 7 | url='https://github.com/MartinSahlen/cloud-functions-python', 8 | author='Martin Sahlen', 9 | author_email='martin8900@gmail.com', 10 | license='MIT', 11 | entry_points={ 12 | 'console_scripts': ['py-cloud-fn=cloudfn.cli:main'], 13 | }, 14 | classifiers=[ 15 | 'Development Status :: 3 - Alpha', 16 | 'Intended Audience :: Developers', 17 | 'License :: OSI Approved :: MIT License', 18 | 'Programming Language :: Python :: 2', 19 | 'Programming Language :: Python :: 2.7', 20 | 'Programming Language :: Python :: 3', 21 | 'Programming Language :: Python :: 3.5', 22 | ], 23 | install_requires=[ 24 | 'pyinstaller==3.3.1', 25 | 'python-dateutil==2.6.0', 26 | 'werkzeug==0.12', 27 | 'django==1.11.1', 28 | 'six==1.10.0', 29 | 'Jinja2==2.9.6', 30 | 'pyspin==1.1.1', 31 | 'google-auth==1.3.0', 32 | ], 33 | include_package_data=True, 34 | packages=['cloudfn'], 35 | zip_safe=False 36 | ) 37 | -------------------------------------------------------------------------------- /upload.sh: -------------------------------------------------------------------------------- 1 | rm -rf build 2 | rm -rf dist 3 | rm -rf pycloudfn.egg-info 4 | python setup.py sdist bdist_wheel 5 | twine upload dist/* 6 | --------------------------------------------------------------------------------