Skip to content

Commit 1c9dfd1

Browse files
authored
Python Getting Started "Bookshelf" app (GoogleCloudPlatform#210)
1 parent 5a053a0 commit 1c9dfd1

13 files changed

Lines changed: 689 additions & 2 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ htmlcov/
2020
.cache
2121
nosetests.xml
2222
coverage.xml
23+
*_log.xml
2324
*,cover
2425
sponge_log.xml
2526

bookshelf/app.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
runtime: python37
2+

bookshelf/firestore.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Copyright 2019 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# [START bookshelf_firestore_client_import]
16+
from google.cloud import firestore
17+
# [END bookshelf_firestore_client_import]
18+
19+
20+
def document_to_dict(doc):
21+
if not doc.exists:
22+
return None
23+
doc_dict = doc.to_dict()
24+
doc_dict['id'] = doc.id
25+
return doc_dict
26+
27+
28+
def next_page(limit=10, start_after=None):
29+
db = firestore.Client()
30+
31+
query = db.collection(u'Book').limit(limit).order_by(u'title')
32+
33+
if start_after:
34+
# Construct a new query starting at this document.
35+
query = query.start_after({u'title': start_after})
36+
37+
docs = query.stream()
38+
docs = list(map(document_to_dict, docs))
39+
40+
last_title = None
41+
if limit == len(docs):
42+
# Get the last document from the results and set as the last title.
43+
last_title = docs[-1][u'title']
44+
return docs, last_title
45+
46+
47+
def read(book_id):
48+
# [START bookshelf_firestore_client]
49+
db = firestore.Client()
50+
book_ref = db.collection(u'Book').document(book_id)
51+
snapshot = book_ref.get()
52+
# [END bookshelf_firestore_client]
53+
return document_to_dict(snapshot)
54+
55+
56+
def update(data, book_id=None):
57+
db = firestore.Client()
58+
book_ref = db.collection(u'Book').document(book_id)
59+
book_ref.set(data)
60+
return document_to_dict(book_ref.get())
61+
62+
63+
create = update
64+
65+
66+
def delete(id):
67+
db = firestore.Client()
68+
book_ref = db.collection(u'Book').document(id)
69+
book_ref.delete()

bookshelf/images/moby-dick.png

328 KB
Loading

bookshelf/main.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
# Copyright 2019 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import logging
16+
17+
import firestore
18+
from flask import current_app, flash, Flask, Markup, redirect, render_template
19+
from flask import request, url_for
20+
from google.cloud import error_reporting
21+
import google.cloud.logging
22+
import storage
23+
24+
25+
# [START upload_image_file]
26+
def upload_image_file(img):
27+
"""
28+
Upload the user-uploaded file to Google Cloud Storage and retrieve its
29+
publicly-accessible URL.
30+
"""
31+
if not img:
32+
return None
33+
34+
public_url = storage.upload_file(
35+
img.read(),
36+
img.filename,
37+
img.content_type
38+
)
39+
40+
current_app.logger.info(
41+
'Uploaded file %s as %s.', img.filename, public_url)
42+
43+
return public_url
44+
# [END upload_image_file]
45+
46+
47+
app = Flask(__name__)
48+
app.config.update(
49+
SECRET_KEY='secret',
50+
MAX_CONTENT_LENGTH=8 * 1024 * 1024,
51+
ALLOWED_EXTENSIONS=set(['png', 'jpg', 'jpeg', 'gif'])
52+
)
53+
54+
app.debug = False
55+
app.testing = False
56+
57+
# Configure logging
58+
if not app.testing:
59+
logging.basicConfig(level=logging.INFO)
60+
client = google.cloud.logging.Client()
61+
# Attaches a Google Stackdriver logging handler to the root logger
62+
client.setup_logging(logging.INFO)
63+
64+
65+
@app.route('/')
66+
def list():
67+
start_after = request.args.get('start_after', None)
68+
books, last_title = firestore.next_page(start_after=start_after)
69+
70+
return render_template('list.html', books=books, last_title=last_title)
71+
72+
73+
@app.route('/books/<book_id>')
74+
def view(book_id):
75+
book = firestore.read(book_id)
76+
return render_template('view.html', book=book)
77+
78+
79+
@app.route('/books/add', methods=['GET', 'POST'])
80+
def add():
81+
if request.method == 'POST':
82+
data = request.form.to_dict(flat=True)
83+
84+
# If an image was uploaded, update the data to point to the new image.
85+
image_url = upload_image_file(request.files.get('image'))
86+
87+
if image_url:
88+
data['imageUrl'] = image_url
89+
90+
book = firestore.create(data)
91+
92+
return redirect(url_for('.view', book_id=book['id']))
93+
94+
return render_template('form.html', action='Add', book={})
95+
96+
97+
@app.route('/books/<book_id>/edit', methods=['GET', 'POST'])
98+
def edit(book_id):
99+
book = firestore.read(book_id)
100+
101+
if request.method == 'POST':
102+
data = request.form.to_dict(flat=True)
103+
104+
# If an image was uploaded, update the data to point to the new image.
105+
image_url = upload_image_file(request.files.get('image'))
106+
107+
if image_url:
108+
data['imageUrl'] = image_url
109+
110+
book = firestore.update(data, book_id)
111+
112+
return redirect(url_for('.view', book_id=book['id']))
113+
114+
return render_template('form.html', action='Edit', book=book)
115+
116+
117+
@app.route('/books/<book_id>/delete')
118+
def delete(book_id):
119+
firestore.delete(book_id)
120+
return redirect(url_for('.list'))
121+
122+
123+
@app.route('/logs')
124+
def logs():
125+
logging.info('Hey, you triggered a custom log entry. Good job!')
126+
flash(Markup('''You triggered a custom log entry. You can view it in the
127+
<a href="https://console.cloud.google.com/logs">Cloud Console</a>'''))
128+
return redirect(url_for('.list'))
129+
130+
131+
@app.route('/errors')
132+
def errors():
133+
raise Exception('This is an intentional exception.')
134+
135+
136+
# Add an error handler that reports exceptions to Stackdriver Error
137+
# Reporting. Note that this error handler is only used when debug
138+
# is False
139+
@app.errorhandler(500)
140+
def server_error(e):
141+
client = error_reporting.Client()
142+
client.report_exception(
143+
http_context=error_reporting.build_flask_context(request))
144+
return """
145+
An internal error occurred: <pre>{}</pre>
146+
See logs for full stacktrace.
147+
""".format(e), 500
148+
149+
150+
# This is only used when running locally. When running live, gunicorn runs
151+
# the application.
152+
if __name__ == '__main__':
153+
app.run(host='127.0.0.1', port=8080, debug=True)

0 commit comments

Comments
 (0)