Skip to content
This repository was archived by the owner on Oct 21, 2022. It is now read-only.

Commit 27b5212

Browse files
authored
Implement the badge server (Part I) (#54)
1 parent 8c74aa7 commit 27b5212

4 files changed

Lines changed: 411 additions & 0 deletions

File tree

badge_server/Dockerfile

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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+
# An image used to run a Python webserver that does compatibility checking
16+
# between pip-installable packages.
17+
18+
# [START docker]
19+
FROM gcr.io/google_appengine/python
20+
ADD requirements.txt /app/requirements.txt
21+
RUN pip3 install -r /app/requirements.txt
22+
ADD . /app
23+
ENTRYPOINT ["python3"]
24+
CMD ["badge_server.py"]
25+
# [END docker]

badge_server/badge_server.py

Lines changed: 348 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,348 @@
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+
"""
16+
URL for creating the badge:
17+
[TODO] Switch to use pybadges once figure out the module not found issue.
18+
'https://img.shields.io/badge/{name}-{status}-{color}.svg'
19+
20+
Commands for build the docker image and deploy to GKE:
21+
docker build -t gcr.io/python-compatibility-tools/badge_server:v1 .
22+
gcloud docker -- push gcr.io/python-compatibility-tools/badge_server:v1
23+
kubectl apply -f deployment/app-with-secret.yaml
24+
"""
25+
import ast
26+
import flask
27+
import logging
28+
import requests
29+
import socket
30+
import threading
31+
32+
from pymemcache.client.hash import HashClient
33+
34+
from compatibility_lib import compatibility_checker
35+
from compatibility_lib import compatibility_store
36+
from compatibility_lib import configs
37+
from compatibility_lib import package as package_module
38+
39+
app = flask.Flask(__name__)
40+
41+
# Cache storing the package name associated with its check results.
42+
# {
43+
# 'pkg1_dep_badge':{},
44+
# 'pkg1_self_comp_badge': {
45+
# 'py2':{
46+
# 'status': 'SUCCESS',
47+
# 'details': None,
48+
# },
49+
# 'py3':{
50+
# 'status': 'CHECK_WARNING',
51+
# 'details': '...',
52+
# }
53+
# },
54+
# 'pkg1_google_comp_badge': {
55+
# 'py2':{
56+
# 'status': 'SUCCESS',
57+
# 'details': None,
58+
# },
59+
# 'py3':{
60+
# 'status': 'CHECK_WARNING',
61+
# 'details': {
62+
# 'package1': '...',
63+
# },
64+
# }
65+
# },
66+
# 'pkg1_api_badge':{},
67+
# }
68+
_, _, ips = socket.gethostbyname_ex(
69+
'badge-cache-memcached.default.svc.cluster.local')
70+
servers = [(ip, 11211) for ip in ips]
71+
client = HashClient(servers, use_pooling=True)
72+
73+
checker = compatibility_checker.CompatibilityChecker()
74+
store = compatibility_store.CompatibilityStore()
75+
76+
URL_PREFIX = 'https://img.shields.io/badge/'
77+
78+
PY_VER_MAPPING = {
79+
2: 'py2',
80+
3: 'py3',
81+
}
82+
83+
STATUS_COLOR_MAPPING = {
84+
'SUCCESS': 'green',
85+
'UNKNOWN': 'black',
86+
'INSTALL_ERROR': 'orange',
87+
'CHECK_WARNING': 'red',
88+
'CALCULATING': 'blue',
89+
'CONVERSION_ERROR': 'orange',
90+
}
91+
92+
CONVERSION_ERROR_RES = {
93+
'py2': {
94+
'status': 'CONVERSION_ERROR',
95+
'details': None,
96+
},
97+
'py3': {
98+
'status': 'CONVERSION_ERROR',
99+
'details': None,
100+
}
101+
}
102+
103+
PKG_PY_VERSION_NOT_SUPPORTED = {
104+
2: ['tensorflow', ],
105+
3: ['google-cloud-dataflow', ],
106+
}
107+
108+
EMPTY_DETAILS = 'NO DETAILS'
109+
110+
DEP_BADGE = 'dep_badge'
111+
SELF_COMP_BADGE = 'self_comp_badge'
112+
GOOGLE_COMP_BADGE = 'google_comp_badge'
113+
API_BADGE = 'api_badge'
114+
115+
116+
def _get_pair_status_for_packages(pkg_sets):
117+
version_and_res = {
118+
'py2': {
119+
'status': 'SUCCESS',
120+
'details': {},
121+
},
122+
'py3': {
123+
'status': 'SUCCESS',
124+
'details': {},
125+
}
126+
}
127+
for pkg_set in pkg_sets:
128+
pkgs = [package_module.Package(pkg) for pkg in pkg_set]
129+
pair_res = store.get_pair_compatibility(pkgs)
130+
for res in pair_res:
131+
py_version = PY_VER_MAPPING[res.python_major_version]
132+
# Status showing one of the check failures
133+
if res.status.value != 'SUCCESS':
134+
version_and_res[py_version]['status'] = res.status.value
135+
version_and_res[py_version]['details'][pkg_set[1]] = \
136+
res.details if res.details is not None else EMPTY_DETAILS
137+
return version_and_res
138+
139+
140+
def _get_badge_url(version_and_res, package_name):
141+
# By default use the status of py3, if it is not SUCCESS, and it can be
142+
# installed with python2, then use the result of python2.
143+
package_name = package_name.replace('-', '.')
144+
status = version_and_res['py3']['status']
145+
if status != 'SUCCESS' and \
146+
package_name not in PKG_PY_VERSION_NOT_SUPPORTED.get(2):
147+
status = version_and_res['py2']['status']
148+
149+
color = STATUS_COLOR_MAPPING[status]
150+
url = URL_PREFIX + '{}-{}-{}.svg'.format(
151+
package_name, status, color)
152+
153+
return url
154+
155+
156+
@app.route('/')
157+
def greetings():
158+
return 'hello world'
159+
160+
161+
@app.route('/self_compatibility_badge/image')
162+
def self_compatibility_badge_image():
163+
"""Badge showing whether a package is compatible with itself."""
164+
package_name = flask.request.args.get('package')
165+
package = package_module.Package(package_name)
166+
compatibility_status = store.get_self_compatibility(package)
167+
168+
version_and_res = {
169+
'py2': {
170+
'status': 'CALCULATING',
171+
'details': None,
172+
},
173+
'py3': {
174+
'status': 'CALCULATING',
175+
'details': None,
176+
}
177+
}
178+
179+
def run_check():
180+
# First see if this package is already stored in BigQuery.
181+
if compatibility_status:
182+
for res in compatibility_status:
183+
py_version = PY_VER_MAPPING[res.python_major_version]
184+
version_and_res[py_version]['status'] = res.status.value
185+
version_and_res[py_version]['details'] = res.details \
186+
if res.details is not None else EMPTY_DETAILS
187+
188+
# If not pre stored in BigQuery, run the check for the package.
189+
else:
190+
py2_res = checker.check([package_name], '2')
191+
py3_res = checker.check([package_name], '3')
192+
193+
version_and_res['py2']['status'] = py2_res.get('result')
194+
py2_description = py2_res.get('description')
195+
py2_details = EMPTY_DETAILS if py2_description is None \
196+
else py2_description
197+
version_and_res['py2']['details'] = py2_details
198+
version_and_res['py3']['status'] = py3_res.get('result')
199+
py3_description = py3_res.get('description')
200+
py3_details = EMPTY_DETAILS if py3_description is None \
201+
else py3_description
202+
version_and_res['py3']['details'] = py3_details
203+
204+
url = _get_badge_url(version_and_res, package_name)
205+
206+
# Write the result to cache
207+
client.set('{}_self_comp_badge'.format(package_name), version_and_res)
208+
return requests.get(url).text
209+
210+
self_comp_res = client.get(
211+
'{}_self_comp_badge'.format(package_name))
212+
threading.Thread(target=run_check).start()
213+
214+
if self_comp_res is not None:
215+
try:
216+
details = ast.literal_eval(self_comp_res.decode('utf-8'))
217+
except SyntaxError:
218+
logging.error(
219+
'Error occurs while converting to dict, value is {}.'.format(
220+
self_comp_res))
221+
details = CONVERSION_ERROR_RES
222+
else:
223+
details = version_and_res
224+
225+
url = _get_badge_url(details, package_name)
226+
227+
return requests.get(url).text
228+
229+
230+
@app.route('/self_compatibility_badge/target')
231+
def self_compatibility_badge_target():
232+
"""Return the dict which contains the self compatibility status and details
233+
for py2 and py3.
234+
235+
e.g. {
236+
'py2':{
237+
'status': 'SUCCESS',
238+
'details': None,
239+
},
240+
'py3':{
241+
'status': 'CHECK_WARNING',
242+
'details': '...',
243+
}
244+
}
245+
"""
246+
package_name = flask.request.args.get('package')
247+
self_comp_res = client.get(
248+
'{}_self_comp_badge'.format(package_name))
249+
250+
return str(self_comp_res)
251+
252+
253+
@app.route('/google_compatibility_badge/image')
254+
def google_compatibility_badge_image():
255+
"""Badge showing whether a package is compatible with Google OSS Python
256+
packages. If all packages success, status is SUCCESS; else set status
257+
to one of the failure types, details can be found at the target link."""
258+
package_name = flask.request.args.get('package')
259+
260+
default_version_and_res = {
261+
'py2': {
262+
'status': 'CALCULATING',
263+
'details': {},
264+
},
265+
'py3': {
266+
'status': 'CALCULATING',
267+
'details': {},
268+
}
269+
}
270+
271+
def run_check():
272+
pkg_sets = [[package_name, pkg] for pkg in configs.PKG_LIST]
273+
if package_name in configs.PKG_LIST:
274+
result = _get_pair_status_for_packages(pkg_sets)
275+
else:
276+
for pkg_set in pkg_sets:
277+
for py_ver in [2, 3]:
278+
py_version = PY_VER_MAPPING[py_ver]
279+
res = checker.check(pkg_set, str(py_ver))
280+
status = res.get('result')
281+
if status != 'SUCCESS':
282+
# Ignore the package that not support for given py_ver
283+
if pkg_set[1] in PKG_PY_VERSION_NOT_SUPPORTED.get(
284+
py_ver):
285+
continue
286+
# Status showing one of the check failures
287+
default_version_and_res[
288+
py_version]['status'] = res.get('result')
289+
description = res.get('description')
290+
details = EMPTY_DETAILS if description is None \
291+
else description
292+
default_version_and_res[
293+
py_version]['details'][pkg_set[1]] = details
294+
result = default_version_and_res
295+
296+
# Write the result to cache
297+
client.set(
298+
'{}_google_comp_badge'.format(package_name), result)
299+
url = _get_badge_url(result, package_name)
300+
return requests.get(url).text
301+
302+
google_comp_res = client.get(
303+
'{}_google_comp_badge'.format(package_name))
304+
threading.Thread(target=run_check).start()
305+
306+
if google_comp_res is not None:
307+
try:
308+
details = ast.literal_eval(google_comp_res.decode('utf-8'))
309+
except SyntaxError:
310+
logging.error(
311+
'Error occurs while converting to dict, value is {}.'.format(
312+
google_comp_res))
313+
details = CONVERSION_ERROR_RES
314+
else:
315+
details = default_version_and_res
316+
317+
url = _get_badge_url(details, package_name)
318+
319+
return requests.get(url).text
320+
321+
322+
@app.route('/google_compatibility_badge/target')
323+
def google_compatibility_badge_target():
324+
"""Return the dict which contains the compatibility status with google
325+
packages and details for py2 and py3.
326+
327+
e.g. {
328+
'py2':{
329+
'status': 'SUCCESS',
330+
'details': None,
331+
},
332+
'py3':{
333+
'status': 'CHECK_WARNING',
334+
'details': {
335+
'package1': '...',
336+
},
337+
}
338+
}
339+
"""
340+
package_name = flask.request.args.get('package')
341+
google_comp_res = client.get(
342+
'{}_google_comp_badge'.format(package_name))
343+
344+
return str(google_comp_res)
345+
346+
347+
if __name__ == '__main__':
348+
app.run(host='0.0.0.0', port=8080)

0 commit comments

Comments
 (0)