Skip to content

Commit c43b768

Browse files
committed
Add connectivity sample for SQLAlchemy.
1 parent d295f9f commit c43b768

File tree

5 files changed

+369
-0
lines changed

5 files changed

+369
-0
lines changed

cloud-sql/mysql/sqlalchemy/README.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Connecting to Cloud SQL - MySQL
2+
3+
## Before you begin
4+
5+
1. If you haven't already, set up a Python Development Environment by following the [python setup guide](https://cloud.google.com/python/setup) and
6+
[create a project](https://cloud.google.com/resource-manager/docs/creating-managing-projects#creating_a_project).
7+
8+
1. Create a 2nd Gen Cloud SQL Instance by following these
9+
[instructions](https://cloud.google.com/sql/docs/mysql/create-instance). Note the connection string,
10+
database user, and database password that you create.
11+
12+
1. Create a database for your application by following these
13+
[instructions](https://cloud.google.com/sql/docs/mysql/create-manage-databases). Note the database
14+
name.
15+
16+
1. Create a service account with the 'Cloud SQL Client' permissions by following these
17+
[instructions](https://cloud.google.com/sql/docs/mysql/connect-external-app#4_if_required_by_your_authentication_method_create_a_service_account).
18+
Download a JSON key to use to authenticate your connection.
19+
20+
1. Use the information noted in the previous steps:
21+
```bash
22+
export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service/account/key.json
23+
export CLOUD_SQL_CONNECTION_NAME='<MY-PROJECT>:<INSTANCE-REGION>:<MY-DATABASE>'
24+
export DB_USER='my-db-user'
25+
export DB_PASS='my-db-pass'
26+
export DB_NAME='my_db'
27+
```
28+
Note: Saving credentials in environment variables is convenient, but not secure - consider a more
29+
secure solution such as [Cloud KMS](https://cloud.google.com/kms/) to help keep secrets safe.
30+
31+
## Running locally
32+
33+
To run this application locally, download and install the cloud_sql_proxy by
34+
following the instructions [here](https://cloud.google.com/sql/docs/mysql/sql-proxy#install).
35+
36+
Once the proxy is ready, use the following command to start the proxy in the
37+
background:
38+
```bash
39+
./cloud_sql_proxy -dir=/cloudsql --instances=$CLOUD_SQL_CONNECTION_NAME --credential_file=$GOOGLE_APPLICATION_CREDENTIALS
40+
```
41+
Note: Make sure to run the command under a user with write access in the
42+
`/cloudsql` directory. This proxy will use this folder to create a unix socket
43+
the application will use to connect to Cloud SQL.
44+
45+
Next, setup install the requirements into a virtual enviroment:
46+
```bash
47+
virtualenv --python python3 env
48+
source env/bin/activate
49+
pip install -r requirements.txt
50+
```
51+
52+
Finally, start the application:
53+
```bash
54+
python main.py
55+
```
56+
57+
Navigate towards `http://127.0.0.1:8080` to verify your application is running correctly.
58+
59+
## Google App Engine Standard
60+
61+
To run on GAE-Standard, create an AppEngine project by following the setup for these
62+
[instructions](https://cloud.google.com/appengine/docs/standard/python3/quickstart#before-you-begin).
63+
64+
First, update `app.yaml` with the correct values to pass the environment
65+
variables into the runtime.
66+
67+
Next, the following command will deploy the application to your Google Cloud project:
68+
```bash
69+
gcloud app deploy
70+
```

cloud-sql/mysql/sqlalchemy/app.yaml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Copyright 2018 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+
runtime: python37
16+
17+
env_variables:
18+
CLOUD_SQL_INSTANCE_NAME: <MY-PROJECT>:<INSTANCE-REGION>:<MY-DATABASE>
19+
DB_USER: my-db-user
20+
DB_PASS: my-db-pass
21+
DB_NAME: my_db

cloud-sql/mysql/sqlalchemy/main.py

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
# Copyright 2018 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 datetime
16+
import logging
17+
import os
18+
19+
from flask import Flask, render_template, request, Response
20+
import sqlalchemy
21+
22+
23+
# Saving credentials in environment variables is convent, but not secure
24+
# - consider a more secure solution such as https://cloud.google.com/kms/ to
25+
# help keep secrets safe.
26+
db_user = os.environ.get("DB_USER")
27+
db_pass = os.environ.get("DB_PASS")
28+
db_name = os.environ.get("DB_NAME")
29+
cloud_sql_instance_name = os.environ.get("CLOUD_SQL_INSTANCE_NAME")
30+
31+
app = Flask(__name__)
32+
33+
logger = logging.getLogger()
34+
35+
# [START cloud_sql_mysql_connection_pool]
36+
# The SQLAlchemy engine will help manage interactions, including automatically
37+
# managing a pool of connections to your database
38+
db = sqlalchemy.create_engine(
39+
# Equivalent URL:
40+
# mysql+pymysql://<db_user>:<db_pass>@/<db_name>?unix_socket=/cloudsql/<cloud_sql_instance_name>
41+
sqlalchemy.engine.url.URL(
42+
drivername='mysql+pymysql',
43+
username=db_user,
44+
password=db_pass,
45+
database=db_name,
46+
query={
47+
'unix_socket': '/cloudsql/{}'.format(cloud_sql_instance_name)
48+
}
49+
),
50+
# ... Specify additional properties here.
51+
# [START_EXCLUDE]
52+
53+
# [START cloud_sql_mysql_limit_connections]
54+
# Pool size is the maximum number of permanent connections to keep.
55+
pool_size=5,
56+
# Temporarily exceeds the set pool_size if no connections are available.
57+
max_overflow=2,
58+
# The total number of concurrent connections for your application will be
59+
# a total of pool_size and max_overflow.
60+
# [END cloud_sql_mysql_limit_connections]
61+
62+
# [START cloud_sql_mysql_connection_backoff]
63+
# SQLAlchemy automatically uses delays between failed connection attempts,
64+
# but provides no arguments for configuration.
65+
# [END cloud_sql_mysql_connection_backoff]
66+
67+
# [START cloud_sql_mysql_connection_timeout]
68+
# 'pool_timeout' is the maximum number of seconds to wait when retrieving a
69+
# new connection from the pool. After the specified amount of time, an
70+
# exception will be thrown.
71+
pool_timeout=30, # 30 seconds
72+
# [END cloud_sql_mysql_connection_timeout]
73+
74+
# [START cloud_sql_mysql_connection_lifetime]
75+
# 'pool_recycle' is the maximum number of seconds a connection can persist.
76+
# Connections that live longer than the specified amount of time will be
77+
# reestablished
78+
pool_recycle=1800, # 30 minutes
79+
# [END cloud_sql_mysql_connection_lifetime]
80+
81+
# [END_EXCLUDE]
82+
)
83+
# [END cloud_sql_mysql_connection_pool]
84+
85+
86+
@app.before_first_request
87+
def create_tables():
88+
# Create tables (if they don't already exist)
89+
with db.connect() as conn:
90+
conn.execute(
91+
"CREATE TABLE IF NOT EXISTS votes "
92+
"( vote_id SERIAL NOT NULL, time_cast timestamp NOT NULL, "
93+
"candidate CHAR(6) NOT NULL, PRIMARY KEY (vote_id) );"
94+
)
95+
96+
97+
@app.route('/', methods=['GET'])
98+
def index():
99+
votes = []
100+
with db.connect() as conn:
101+
# Execute the query and fetch all results
102+
recent_votes = conn.execute(
103+
"SELECT candidate, time_cast FROM votes "
104+
"ORDER BY time_cast DESC LIMIT 5"
105+
).fetchall()
106+
# Convert the results into a list of dicts representing votes
107+
for row in recent_votes:
108+
votes.append({
109+
'candidate': row[0],
110+
'time_cast': row[1]
111+
})
112+
113+
stmt = sqlalchemy.text(
114+
"SELECT COUNT(vote_id) FROM votes WHERE candidate=:candidate")
115+
# Count number of votes for tabs
116+
tab_result = conn.execute(stmt, candidate="TABS").fetchone()
117+
tab_count = tab_result[0]
118+
# Count number of votes for spaces
119+
space_result = conn.execute(stmt, candidate="SPACES").fetchone()
120+
space_count = space_result[0]
121+
122+
return render_template(
123+
'index.html',
124+
recent_votes=votes,
125+
tab_count=tab_count,
126+
space_count=space_count
127+
)
128+
129+
130+
@app.route('/', methods=['POST'])
131+
def save_vote():
132+
# Get the team and time the vote was cast.
133+
team = request.form['team']
134+
time_cast = datetime.datetime.utcnow()
135+
# Verify that the team is one of the allowed options
136+
if team != "TABS" and team != "SPACES":
137+
logger.warning(team)
138+
return Response(
139+
response="Invalid team specified.",
140+
status=400
141+
)
142+
143+
# [START cloud_sql_mysql_example_statement]
144+
# Preparing a statement before hand can help protect against injections.
145+
stmt = sqlalchemy.text(
146+
"INSERT INTO votes (time_cast, candidate)"
147+
" VALUES (:time_cast, :candidate)"
148+
)
149+
try:
150+
# Using a with statement ensures that the connection is always released
151+
# back into the pool at the end of statement (even if an error occurs)
152+
with db.connect() as conn:
153+
conn.execute(stmt, time_cast=time_cast, candidate=team)
154+
except Exception as e:
155+
# If something goes wrong, handle the error in this section. This might
156+
# involve retrying or adjusting parameters depending on the situation.
157+
# [START_EXCLUDE]
158+
logger.exception(e)
159+
return Response(
160+
status=500,
161+
response="Unable to successfully cast vote! Please check the "
162+
"application logs for more details."
163+
)
164+
# [END_EXCLUDE]
165+
# [END cloud_sql_mysql_example_statement]
166+
167+
return Response(
168+
status=200,
169+
response="Vote successfully cast for '{}' at time {}!".format(
170+
team, time_cast)
171+
)
172+
173+
174+
if __name__ == '__main__':
175+
app.run(host='127.0.0.1', port=8080, debug=True)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Flask==1.0.2
2+
SQLAlchemy==1.2.13
3+
PyMySQL==0.9.2
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<!--
2+
Copyright 2018 Google LLC
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
-->
16+
<html lang="en">
17+
<head>
18+
<title>Tabs VS Spaces</title>
19+
<link rel="stylesheet"
20+
href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
21+
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
22+
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
23+
</head>
24+
<body>
25+
<nav class="red lighten-1">
26+
<div class="nav-wrapper">
27+
<a href="#" class="brand-logo center">Tabs VS Spaces</a>
28+
</div>
29+
</nav>
30+
<div class="section">
31+
<div class="center">
32+
<h4>
33+
{% if tab_count == space_count %}
34+
TABS and SPACES are evenly matched!
35+
{% elif tab_count > space_count %}
36+
TABS are winning by {{tab_count - space_count}}
37+
{{'votes' if tab_count - space_count > 1 else 'vote'}}!
38+
{% elif space_count > tab_count %}
39+
SPACES are winning by {{space_count - tab_count}}
40+
{{'votes' if space_count - tab_count > 1 else 'vote'}}!
41+
{% endif %}
42+
</h4>
43+
</div>
44+
<div class="row center">
45+
<div class="col s6 m5 offset-m1">
46+
<div class="card-panel {{'green lighten-3' if tab_count > space_count}}">
47+
<i class="material-icons large">keyboard_tab</i>
48+
<h3>{{tab_count}} votes</h3>
49+
<button id="voteTabs" class="btn green">Vote for TABS</button>
50+
</div>
51+
</div>
52+
<div class="col s6 m5">
53+
<div class="card-panel {{'blue lighten-3' if tab_count < space_count}}">
54+
<i class="material-icons large">space_bar</i>
55+
<h3>{{space_count}} votes</h3>
56+
<button id="voteSpaces" class="btn blue">Vote for SPACES</button>
57+
</div>
58+
</div>
59+
</div>
60+
<h4 class="header center">Recent Votes</h4>
61+
<ul class="container collection center">
62+
{% for vote in recent_votes %}
63+
<li class="collection-item avatar">
64+
{% if vote.candidate == "TABS" %}
65+
<i class="material-icons circle green">keyboard_tab</i>
66+
{% elif vote.candidate == "SPACES" %}
67+
<i class="material-icons circle blue">space_bar</i>
68+
{% endif %}
69+
<span class="title">
70+
A vote for <b>{{vote.candidate}}</b>
71+
</span>
72+
<p>was cast at {{vote.time_cast}}</p>
73+
</li>
74+
{% endfor %}
75+
</ul>
76+
</div>
77+
<script>
78+
function vote(team) {
79+
var xhr = new XMLHttpRequest();
80+
xhr.onreadystatechange = function () {
81+
if (this.readyState == 4) {
82+
if (!window.alert(this.responseText)) {
83+
window.location.reload();
84+
}
85+
}
86+
};
87+
xhr.open("POST", "/", true);
88+
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
89+
xhr.send("team=" + team);
90+
}
91+
92+
document.getElementById("voteTabs").addEventListener("click", function () {
93+
vote("TABS");
94+
});
95+
document.getElementById("voteSpaces").addEventListener("click", function () {
96+
vote("SPACES");
97+
});
98+
</script>
99+
</body>
100+
</html>

0 commit comments

Comments
 (0)