Skip to content

Commit a762bda

Browse files
authored
Merge pull request #478 from GoogleCloudPlatform/app-scaffold-benoit
App Scaffold
2 parents f66fbaf + c25ae4a commit a762bda

18 files changed

+463
-0
lines changed

Diff for: scaffolds/app_engine/.gcloudignore

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# This file specifies files that are *not* uploaded to Google Cloud
2+
# using gcloud. It follows the same syntax as .gitignore, with the addition of
3+
# "#!include" directives (which insert the entries of the given .gitignore-style
4+
# file at that point).
5+
#
6+
# For more information, run:
7+
# $ gcloud topic gcloudignore
8+
#
9+
.gcloudignore
10+
# If you would like to upload your .git directory, .gitignore file or files
11+
# from your .gitignore file, remove the corresponding line
12+
# below:
13+
.git
14+
.gitignore
15+
16+
# Python pycache:
17+
__pycache__/
18+
# Ignored by the build system
19+
/setup.cfg

Diff for: scaffolds/app_engine/Makefile

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
all: clean venv
2+
3+
.PHONY: clean
4+
clean:
5+
@find . -name '*.pyc' -delete
6+
@find . -name '__pycache__' -delete
7+
@find . -name '*egg-info' -type d -exec rm -r {} +
8+
@find . -name '.pytest_cache' -type d -exec rm -r {} +
9+
@rm -rf venv
10+
11+
.PHONY: venv
12+
venv:
13+
@python3 -m venv venv
14+
@. venv/bin/activate && pip install -U pip && pip install -e .
15+
16+
.PHONY: auth
17+
auth:
18+
gcloud auth application-default login
19+
20+
.PHONY: tests
21+
tests:
22+
./scripts/run_tests.sh
23+
24+
.PHONY: deploy
25+
deploy: clean
26+
./scripts/create_app.sh
27+
./scripts/deploy.sh
28+
29+
.PHONY: run
30+
run:
31+
./scripts/run_locally.sh

Diff for: scaffolds/app_engine/README.md

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Development Workflow
2+
3+
## Local development
4+
5+
The workflow described here works from CloudShell or any node with the [gcloud CLI](https://cloud.google.com/sdk/docs/install) has been properly installed and authenticated.
6+
7+
This means that you can develop your application fully locally on your laptop for example, as long as you have run `make auth` after installing the [gcloud CLI](https://cloud.google.com/sdk/docs/install) on it.
8+
9+
The first step is to add your `PROJECT` and `BUCKET` names in the following files:
10+
* `./scripts/config.sh`
11+
* `app.yaml`
12+
13+
For local development, install then the gcloud CLI following [these instructions](https://cloud.google.com/sdk/docs/install).
14+
15+
Make sure to accept upgrading Python to 3.10 if prompted, then authenticate for local development by running:
16+
17+
```bash
18+
make auth
19+
```
20+
21+
The second step is to create and populate the virtual environment with
22+
23+
```bash
24+
make venv
25+
```
26+
After this step you should find a new folder called `venv` containing the virtual environment.
27+
28+
At this point you should already be able to run the tests by running
29+
```bash
30+
make tests
31+
```
32+
33+
To run the app locally, simply run
34+
```bash
35+
make run
36+
```
37+
38+
At last to deploy the application on AppEngine run
39+
```bash
40+
make deploy
41+
```
42+
43+
**Note:** `make clean` will remove all the built artifacts as long as the virtual environment created by `make venv`. This target is invoked by `make deploy` so that the built artifacts are not uploaded to AppEngine. The down-side is that the virtual environment will need to be recreated after each deployment.
44+
45+
## Development workflow
46+
47+
1. Edit the code
48+
1. Run the tests with `make tests`
49+
1. Test the app local with `make run`
50+
1. Deploy the app on AppEngine with `make deploy`

Diff for: scaffolds/app_engine/app.yaml

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
runtime: python310
2+
entrypoint: gunicorn -b :$PORT app.server:app
3+
4+
runtime_config:
5+
operating_system: "ubuntu22"
6+
7+
env_variables:
8+
BUCKET: "<YOUR_BUCKET>"
9+
LOCATION: "us-central1"
10+
PROJECT: "<YOUR_PROJECT>"

Diff for: scaffolds/app_engine/app/__init__.py

Whitespace-only changes.

Diff for: scaffolds/app_engine/app/server.py

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
""" Flask serving API and small demo UI.
2+
"""
3+
4+
import logging
5+
6+
from flask import Flask, jsonify, request, send_file
7+
8+
app = Flask(__name__)
9+
10+
11+
@app.route("/")
12+
def _index():
13+
"""Serve index.html in the static directory"""
14+
return send_file("static/index.html")
15+
16+
17+
@app.route("/myapp", methods=["GET"])
18+
def _answernaut():
19+
return jsonify({"answer": request.args["query"]})
20+
21+
22+
@app.errorhandler(500)
23+
def _server_error(e):
24+
"""Serves a formatted message on-error"""
25+
logging.exception("An error occurred during a request.")
26+
return (
27+
f"An internal error occurred: <pre>{e}</pre><br>",
28+
500,
29+
)
30+
31+
32+
if __name__ == "__main__":
33+
# This is used when running locally. Gunicorn is used to run the
34+
# application on Google App Engine. See entrypoint in app.yaml.
35+
app.run(host="127.0.0.1", port=8080, debug=True)

Diff for: scaffolds/app_engine/app/static/asl.png

76 KB
Loading

Diff for: scaffolds/app_engine/app/static/index.html

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<link rel="stylesheet" href="/static/main.css">
7+
<link rel="icon" type"image/x-icon" href="/static/asl.png">
8+
</head>
9+
<body>
10+
<div class="body">
11+
12+
<div class="logo">
13+
<img src="/static/asl.png" alt="ASL Logo" width="100" height="100">
14+
</div>
15+
16+
<form action="" id="query-form" class="form">
17+
<input type="text" id="query-input" class="input"></textarea>
18+
<button id="ask-button" type="submit" class="ask">Ask</button>
19+
</form>
20+
21+
<div id="query-answer" class="answer"></div>
22+
<div id="spinner" class="lds-spinner"><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div></div>
23+
24+
</div>
25+
26+
<script type="text/javascript" src="/static/main.js"></script>
27+
28+
29+
</body>
30+
</html>

Diff for: scaffolds/app_engine/app/static/main.css

+161
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
2+
*, ::after, ::before {
3+
box-sizing: border-box;
4+
border-width: 0;
5+
border-style: solid;
6+
border-color: #e5e7eb;
7+
font-size: 1rem;
8+
font-family: ui-sans-serif, system-ui, -apple-system, "system-ui", "Segoe UI";
9+
}
10+
11+
.body {
12+
padding: 4rem;
13+
justify-content: center;
14+
display: flex;
15+
flex-direction: column;
16+
align-items: center;
17+
width: 100vw;
18+
height: 100vh;
19+
}
20+
21+
.logo {
22+
margin-bottom: 2.5rem;
23+
}
24+
25+
.form {
26+
display: flex;
27+
flex-direction: column;
28+
align-items: center;
29+
width: 50%;
30+
margin-bottom: 3rem;
31+
}
32+
33+
.input {
34+
padding-left: 0.5rem;
35+
padding-right: 0.5rem;
36+
border-width: 1px;
37+
border-radius: 0.5rem;
38+
width: 100%;
39+
height: 2rem;
40+
}
41+
42+
.ask {
43+
padding-top: 0.25rem;
44+
padding-bottom: 0.25rem;
45+
padding-left: 0.5rem;
46+
padding-right: 0.5rem;
47+
border-width: 1px;
48+
border-color: rgb(191 219 254);
49+
border-radius: 0.5rem;
50+
margin-left: 0.75rem;
51+
margin-top: 2rem;
52+
width: 4rem;
53+
background-color: rgb(219 234 254);
54+
}
55+
56+
button.ask:hover {
57+
background-color: rgb(3 105 161);
58+
}
59+
60+
.answer {
61+
display: flex;
62+
padding: 1.25rem;
63+
background-color: rgb(219 234 254);
64+
border-color: rgb(191 219 254);
65+
border-radius: 0.5rem;
66+
width: 50%;
67+
line-height: 1.4rem;
68+
display: none;
69+
overflow-y: auto;
70+
}
71+
72+
.hide {
73+
display: none !important;
74+
}
75+
76+
.show {
77+
display: block !important;
78+
}
79+
80+
81+
/* Spinner */
82+
83+
.lds-spinner {
84+
color: official;
85+
display: inline-block;
86+
position: relative;
87+
width: 80px;
88+
height: 80px;
89+
display: none;
90+
}
91+
.lds-spinner div {
92+
transform-origin: 40px 40px;
93+
animation: lds-spinner 1.2s linear infinite;
94+
}
95+
.lds-spinner div:after {
96+
content: " ";
97+
display: block;
98+
position: absolute;
99+
top: 3px;
100+
left: 37px;
101+
width: 6px;
102+
height: 18px;
103+
border-radius: 20%;
104+
background: rgb(66,132,243);
105+
}
106+
.lds-spinner div:nth-child(1) {
107+
transform: rotate(0deg);
108+
animation-delay: -1.1s;
109+
}
110+
.lds-spinner div:nth-child(2) {
111+
transform: rotate(30deg);
112+
animation-delay: -1s;
113+
}
114+
.lds-spinner div:nth-child(3) {
115+
transform: rotate(60deg);
116+
animation-delay: -0.9s;
117+
}
118+
.lds-spinner div:nth-child(4) {
119+
transform: rotate(90deg);
120+
animation-delay: -0.8s;
121+
}
122+
.lds-spinner div:nth-child(5) {
123+
transform: rotate(120deg);
124+
animation-delay: -0.7s;
125+
}
126+
.lds-spinner div:nth-child(6) {
127+
transform: rotate(150deg);
128+
animation-delay: -0.6s;
129+
}
130+
.lds-spinner div:nth-child(7) {
131+
transform: rotate(180deg);
132+
animation-delay: -0.5s;
133+
}
134+
.lds-spinner div:nth-child(8) {
135+
transform: rotate(210deg);
136+
animation-delay: -0.4s;
137+
}
138+
.lds-spinner div:nth-child(9) {
139+
transform: rotate(240deg);
140+
animation-delay: -0.3s;
141+
}
142+
.lds-spinner div:nth-child(10) {
143+
transform: rotate(270deg);
144+
animation-delay: -0.2s;
145+
}
146+
.lds-spinner div:nth-child(11) {
147+
transform: rotate(300deg);
148+
animation-delay: -0.1s;
149+
}
150+
.lds-spinner div:nth-child(12) {
151+
transform: rotate(330deg);
152+
animation-delay: 0s;
153+
}
154+
@keyframes lds-spinner {
155+
0% {
156+
opacity: 1;
157+
}
158+
100% {
159+
opacity: 0;
160+
}
161+
}

Diff for: scaffolds/app_engine/app/static/main.js

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
async function queryApi(prompt) {
2+
const endpoint = `/myapp?query=${prompt}`
3+
const response = await fetch(endpoint)
4+
const answer = await response.json()
5+
return answer
6+
}
7+
8+
function displayAnswer(answer) {
9+
const queryAnswer = document.getElementById("query-answer")
10+
queryAnswer.innerHTML = `<md-block> ${answer} </md-block>`
11+
}
12+
13+
function getPrompt() {
14+
const queryInput = document.getElementById("query-input")
15+
return queryInput.value
16+
}
17+
18+
function getAnswer() {
19+
const spinner = document.getElementById("spinner")
20+
const queryAnswer = document.getElementById("query-answer")
21+
queryAnswer.classList.remove("show")
22+
spinner.classList.add("show")
23+
const prompt = getPrompt()
24+
queryApi(prompt).then(
25+
response => {
26+
spinner.classList.remove("show")
27+
queryAnswer.classList.add("show")
28+
displayAnswer(response.answer)
29+
}
30+
)
31+
32+
}
33+
34+
function init() {
35+
const queryForm = document.getElementById("query-form")
36+
queryForm.addEventListener("submit", e => {
37+
e.preventDefault()
38+
getAnswer()
39+
})
40+
41+
const queryInput = document.getElementById("query-input")
42+
queryInput.addEventListener("keyup", e => {
43+
e.preventDefault()
44+
if (e.KeyCode === 13){
45+
document.getElementById("ask-button").click()
46+
}
47+
})
48+
49+
}
50+
51+
init()

Diff for: scaffolds/app_engine/requirements.txt

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Flask==3.0.0
2+
gunicorn==20.1.0
3+
pre-commit==3.7.1
4+
pytest==7.0.1

0 commit comments

Comments
 (0)