├── .gitignore ├── imgResource ├── demo.jpeg ├── merge.jpg ├── search.png └── segment.jpg ├── Python SDK demo 使用文档.pdf ├── README.md ├── PythonSDK ├── structures.py ├── compat.py ├── ImagePro.py └── facepp.py └── call.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | -------------------------------------------------------------------------------- /imgResource/demo.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FacePlusPlus/facepp-python-sdk/HEAD/imgResource/demo.jpeg -------------------------------------------------------------------------------- /imgResource/merge.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FacePlusPlus/facepp-python-sdk/HEAD/imgResource/merge.jpg -------------------------------------------------------------------------------- /imgResource/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FacePlusPlus/facepp-python-sdk/HEAD/imgResource/search.png -------------------------------------------------------------------------------- /imgResource/segment.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FacePlusPlus/facepp-python-sdk/HEAD/imgResource/segment.jpg -------------------------------------------------------------------------------- /Python SDK demo 使用文档.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FacePlusPlus/facepp-python-sdk/HEAD/Python SDK demo 使用文档.pdf -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FacePlusPlus Python SDK 2 | 3 | * 集成运行文档请查看目录下的文件`Python SDK demo 使用文档.pdf` 4 | 5 | * 如果集成中有问题,请[联系我们](https://www.faceplusplus.com.cn/contact-us/) 6 | 7 | 8 | -------------------------------------------------------------------------------- /PythonSDK/structures.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class ObjectDict(dict): 4 | """Dictionary object for json decode""" 5 | 6 | def __getattr__(self, name): 7 | if name in self: 8 | return self[name] 9 | else: 10 | raise AttributeError("No such attribute: " + name) 11 | 12 | def __setattr__(self, name, value): 13 | self[name] = value 14 | 15 | def __delattr__(self, name): 16 | if name in self: 17 | del self[name] 18 | else: 19 | raise AttributeError("No such attribute: " + name) -------------------------------------------------------------------------------- /PythonSDK/compat.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | This module handles import compatibility issues between Python 2 and 5 | Python 3. 6 | """ 7 | 8 | import sys 9 | import random 10 | import string 11 | 12 | # ------- 13 | # Pythons 14 | # ------- 15 | 16 | # Syntax sugar. 17 | _ver = sys.version_info 18 | 19 | #: Python 2.x? 20 | is_py2 = (_ver[0] == 2) 21 | 22 | #: Python 3.x? 23 | is_py3 = (_ver[0] == 3) 24 | 25 | try: 26 | import simplejson as json 27 | except ImportError: 28 | import json 29 | 30 | # --------- 31 | # Specifics 32 | # --------- 33 | 34 | if is_py2: 35 | from urllib2 import Request, urlopen, HTTPError, URLError 36 | builtin_str = str 37 | bytes = str 38 | str = unicode 39 | basestring = basestring 40 | numeric_types = (int, long, float) 41 | integer_types = (int, long) 42 | 43 | elif is_py3: 44 | from urllib.request import Request, urlopen 45 | from urllib.error import HTTPError, URLError 46 | builtin_str = str 47 | str = str 48 | bytes = bytes 49 | basestring = (str, bytes) 50 | numeric_types = (int, float) 51 | integer_types = (int,) 52 | 53 | 54 | def enc(x): 55 | if isinstance(x, str): 56 | return x.encode('utf-8') 57 | elif isinstance(x, numeric_types): 58 | return str(x).encode('utf-8') 59 | return x 60 | 61 | 62 | def choose_boundary(): 63 | rand_letters = ''.join(random.sample(string.ascii_letters+string.digits, 15)) 64 | return '{ch}{flag}{rand}'.format(ch='-'*6, flag='PylibFormBoundary', rand=rand_letters) 65 | -------------------------------------------------------------------------------- /call.py: -------------------------------------------------------------------------------- 1 | 2 | # 导入系统库并定义辅助函数 3 | from pprint import pformat 4 | 5 | # import PythonSDK 6 | from PythonSDK.facepp import API,File 7 | 8 | # 导入图片处理类 9 | import PythonSDK.ImagePro 10 | 11 | # 以下四项是dmeo中用到的图片资源,可根据需要替换 12 | detech_img_url = 'http://bj-mc-prod-asset.oss-cn-beijing.aliyuncs.com/mc-official/images/face/demo-pic11.jpg' 13 | faceSet_img = './imgResource/demo.jpeg' # 用于创建faceSet 14 | face_search_img = './imgResource/search.png' # 用于人脸搜索 15 | segment_img = './imgResource/segment.jpg' # 用于人体抠像 16 | merge_img = './imgResource/merge.jpg' # 用于人脸融合 17 | 18 | 19 | # 此方法专用来打印api返回的信息 20 | def print_result(hit, result): 21 | print(hit) 22 | print('\n'.join(" " + i for i in pformat(result, width=75).split('\n'))) 23 | 24 | def printFuctionTitle(title): 25 | return "\n"+"-"*60+title+"-"*60; 26 | 27 | # 初始化对象,进行api的调用工作 28 | api = API() 29 | # -----------------------------------------------------------人脸识别部分------------------------------------------- 30 | 31 | # 人脸检测:https://console.faceplusplus.com.cn/documents/4888373 32 | res = api.detect(image_url=detech_img_url, return_attributes="gender,age,smiling,headpose,facequality," 33 | "blur,eyestatus,emotion,ethnicity,beauty," 34 | "mouthstatus,skinstatus") 35 | print_result(printFuctionTitle("人脸检测"), res) 36 | 37 | 38 | # 人脸比对:https://console.faceplusplus.com.cn/documents/4887586 39 | # compare_res = api.compare(image_file1=File(face_search_img), image_file2=File(face_search_img)) 40 | # print_result("compare", compare_res) 41 | 42 | # 人脸搜索:https://console.faceplusplus.com.cn/documents/4888381 43 | # 人脸搜索步骤 44 | # 1,创建faceSet:用于存储人脸信息(face_token) 45 | # 2,向faceSet中添加人脸信息(face_token) 46 | # 3,开始搜索 47 | 48 | # 删除无用的人脸库,这里删除了,如果在项目中请注意是否要删除 49 | # api.faceset.delete(outer_id='faceplusplus', check_empty=0) 50 | # # 1.创建一个faceSet 51 | # ret = api.faceset.create(outer_id='faceplusplus') 52 | # 53 | # # 2.向faceSet中添加人脸信息(face_token) 54 | # faceResStr="" 55 | # res = api.detect(image_file=File(faceSet_img)) 56 | # faceList = res["faces"] 57 | # for index in range(len(faceList)): 58 | # if(index==0): 59 | # faceResStr = faceResStr + faceList[index]["face_token"] 60 | # else: 61 | # faceResStr = faceResStr + ","+faceList[index]["face_token"] 62 | # 63 | # api.faceset.addface(outer_id='faceplusplus', face_tokens=faceResStr) 64 | # 65 | # # 3.开始搜索相似脸人脸信息 66 | # search_result = api.search(image_file=File(face_search_img), outer_id='faceplusplus') 67 | # print_result('search', search_result) 68 | 69 | # -----------------------------------------------------------人体识别部分------------------------------------------- 70 | 71 | # 人体抠像:https://console.faceplusplus.com.cn/documents/10071567 72 | # segment_res = api.segment(image_file=File(segment_img)) 73 | # f = open('./imgResource/demo-segment.b64', 'w') 74 | # f.write(segment_res["result"]) 75 | # f.close() 76 | # print_result("segment", segment_res) 77 | # # 开始抠像 78 | # PythonSDK.ImagePro.ImageProCls.getSegmentImg("./imgResource/demo-segment.b64") 79 | 80 | # -----------------------------------------------------------证件识别部分------------------------------------------- 81 | # 身份证识别:https://console.faceplusplus.com.cn/documents/5671702 82 | # ocrIDCard_res = api.ocridcard(image_url="https://gss0.bdstatic.com/94o3dSag_xI4khGkpoWK1HF6hhy/baike/" 83 | # "c0%3Dbaike80%2C5%2C5%2C80%2C26/sign=7a16a1be19178a82da3177f2976a18e8" 84 | # "/902397dda144ad34a1b2dcf5d7a20cf431ad85b7.jpg") 85 | # print_result('ocrIDCard', ocrIDCard_res) 86 | 87 | # 银行卡识别:https://console.faceplusplus.com.cn/documents/10069553 88 | # ocrBankCard_res = api.ocrbankcard(image_url="http://pic.5tu.cn/uploads/allimg/1107/191634534200.jpg") 89 | # print_result('ocrBankCard', ocrBankCard_res) 90 | 91 | # -----------------------------------------------------------图像识别部分------------------------------------------- 92 | # 人脸融合:https://console.faceplusplus.com.cn/documents/20813963 93 | # template_rectangle参数中的数据要通过人脸检测api来获取 94 | # mergeFace_res = api.mergeface(template_file=File(segment_img), merge_file=File(merge_img), 95 | # template_rectangle="130,180,172,172") 96 | # print_result("mergeFace", mergeFace_res) 97 | # 98 | # # 开始融合 99 | # PythonSDK.ImagePro.ImageProCls.getMergeImg(mergeFace_res["result"]) 100 | -------------------------------------------------------------------------------- /PythonSDK/ImagePro.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import io 3 | import os 4 | from PIL import Image, ImageColor 5 | 6 | 7 | class ImageProCls: 8 | base64FilePath=None 9 | 10 | @staticmethod 11 | def humanbody_blending_with_image(input_image, gray_image, bg_image): 12 | """ 13 | 14 | :param input_image: the PIL.Image instance, the source humanbody image 15 | :param gray_image: the PIL.Image instance, it is created from api base64 result, a gray image 16 | :param bg_image: the PIL.Image instance, the background image you want to replace 17 | :return: the PIL.Image instance is blending with humanbody 18 | 19 | notes: you should close the return object after you leave 20 | """ 21 | input_width, input_height = input_image.size 22 | bg_width, bg_height = bg_image.size 23 | 24 | input_aspect_ratio = input_width / float(input_height) 25 | bg_aspect_ratio = bg_width / float(bg_height) 26 | 27 | if bg_aspect_ratio > input_aspect_ratio: 28 | target_width, target_height = int(bg_height * input_aspect_ratio), bg_height 29 | else: 30 | target_width, target_height = bg_width, int(bg_width / input_aspect_ratio) 31 | 32 | crop_image = bg_image.crop((0, 0, 0+target_width, 0+target_height)) 33 | new_image = crop_image.resize((input_width, input_height)) 34 | crop_image.close() 35 | 36 | for x in range(0, input_width): 37 | for y in range(0, input_height): 38 | coord = (x, y) 39 | gray_pixel_value = gray_image.getpixel(coord) 40 | input_rgb_color = input_image.getpixel(coord) 41 | bg_rgb_color = new_image.getpixel(coord) 42 | 43 | confidence = gray_pixel_value / 255.0 44 | alpha = confidence 45 | 46 | R = input_rgb_color[0] * alpha + bg_rgb_color[0] * (1 - alpha) 47 | G = input_rgb_color[1] * alpha + bg_rgb_color[1] * (1 - alpha) 48 | B = input_rgb_color[2] * alpha + bg_rgb_color[2] * (1 - alpha) 49 | 50 | R = max(0, min(int(R), 255)) 51 | G = max(0, min(int(G), 255)) 52 | B = max(0, min(int(B), 255)) 53 | 54 | new_image.putpixel(coord, (R, G, B)) 55 | 56 | return new_image 57 | 58 | @staticmethod 59 | def humanbody_blending_with_color(input_image, gray_image, bg_color): 60 | """ 61 | :param input_image: the PIL.Image instance 62 | :param gray_image: the PIL.Image instance, it is created from api base64 result, it is a gray image 63 | :param bg_color: a color string value, such as '#FFFFFF' 64 | :return: PIL.Image instance 65 | 66 | notes: you should close the return object after you leave 67 | """ 68 | input_width, input_height = input_image.size 69 | bg_rgb_color = ImageColor.getrgb(bg_color) 70 | 71 | new_image = Image.new("RGB", input_image.size, bg_color) 72 | 73 | for x in range(0, input_width): 74 | for y in range(0, input_height): 75 | coord = (x, y) 76 | gray_pixel_value = gray_image.getpixel(coord) 77 | input_rgb_color = input_image.getpixel(coord) 78 | 79 | confidence = gray_pixel_value / 255.0 80 | alpha = confidence 81 | 82 | R = input_rgb_color[0] * alpha + bg_rgb_color[0] * (1 - alpha) 83 | G = input_rgb_color[1] * alpha + bg_rgb_color[1] * (1 - alpha) 84 | B = input_rgb_color[2] * alpha + bg_rgb_color[2] * (1 - alpha) 85 | 86 | R = max(0, min(int(R), 255)) 87 | G = max(0, min(int(G), 255)) 88 | B = max(0, min(int(B), 255)) 89 | 90 | new_image.putpixel(coord, (R, G, B)) 91 | 92 | return new_image 93 | 94 | @staticmethod 95 | def getSegmentImg(filePath): 96 | input_file = '' 97 | with open(filePath, 'r') as f: 98 | input_file = f.read() 99 | input_file = base64.b64decode(input_file) 100 | 101 | gray_image = Image.open(io.BytesIO(input_file)) 102 | input_image = Image.open('./imgResource/segment.jpg', 'r') 103 | 104 | new_image = ImageProCls.humanbody_blending_with_color(input_image, gray_image, '#FFFFFF') 105 | new_image.save('./imgResource/resultImg.jpg') 106 | print('-' * 60) 107 | print('结果已经生成,生成文件名:resultImg.jpg,请在imgResource/目录下查看') 108 | if os.path.exists(filePath): 109 | os.remove(filePath) 110 | f.close() 111 | new_image.close() 112 | input_image.close() 113 | gray_image.close() 114 | 115 | @staticmethod 116 | def getMergeImg(base64Str): 117 | imgdata = base64.b64decode(base64Str) 118 | file = open('./imgResource/MergeResultImg.jpg', 'wb') 119 | file.write(imgdata) 120 | file.close() 121 | print('结果已经生成,生成文件名:MergeResultImg.jpg,请在imgResource/目录下查看') 122 | 123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /PythonSDK/facepp.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """a simple facepp sdk 4 | usage: 5 | api = API(key, secret) 6 | api.detect(img = File('/tmp/test.jpg')) 7 | """ 8 | 9 | import sys 10 | import socket 11 | import json 12 | import os.path 13 | import itertools 14 | import mimetypes 15 | import time 16 | from collections import Iterable 17 | from PythonSDK.structures import ObjectDict 18 | from PythonSDK.compat import (basestring, str, numeric_types, enc, choose_boundary, 19 | Request, urlopen, HTTPError, URLError) 20 | 21 | import ssl 22 | ssl._create_default_https_context = ssl._create_unverified_context 23 | 24 | __all__ = ['File', 'APIError', 'API'] 25 | 26 | DEBUG_LEVEL = 1 27 | 28 | # 添加API Key API Secret 29 | API_KEY = "XXX" 30 | API_SECRET = "XXX" 31 | 32 | 33 | class File(object): 34 | 35 | """an object representing a local file""" 36 | path = None 37 | content = None 38 | 39 | def __init__(self, path): 40 | self.path = path 41 | self._get_content() 42 | 43 | def _get_content(self): 44 | """read image content""" 45 | 46 | if os.path.getsize(self.path) > 2 * 1024 * 1024: 47 | raise APIError(-1, None, 'image file size too large') 48 | else: 49 | with open(self.path, 'rb') as f: 50 | self.content = f.read() 51 | 52 | def get_filename(self): 53 | return os.path.basename(self.path) 54 | 55 | 56 | class APIError(Exception): 57 | code = None 58 | """HTTP status code""" 59 | 60 | url = None 61 | """request URL""" 62 | 63 | body = None 64 | """server response body; or detailed error information""" 65 | 66 | def __init__(self, code, url, body): 67 | self.code = code 68 | self.url = url 69 | self.body = body 70 | 71 | def __str__(self): 72 | return 'code={s.code}\nurl={s.url}\n{s.body}'.format(s=self) 73 | 74 | __repr__ = __str__ 75 | 76 | 77 | class API(object): 78 | key = None 79 | secret = None 80 | server = 'https://api-cn.faceplusplus.com' 81 | 82 | decode_result = True 83 | timeout = None 84 | max_retries = None 85 | retry_delay = None 86 | 87 | def __init__(self): 88 | """ 89 | :param srv: The API server address 90 | :param decode_result: whether to json_decode the result 91 | :param timeout: HTTP request timeout in seconds 92 | :param max_retries: maximal number of retries after catching URL error 93 | or socket error 94 | :param retry_delay: time to sleep before retrying 95 | """ 96 | if len(API_KEY)==0 or len(API_SECRET)==0: 97 | print('\n'+'请在'+os.path.realpath(__file__)+'文件中填写正确的API_KEY和API_SECRET'+'\n') 98 | exit(1) 99 | 100 | self.key = API_KEY 101 | self.secret = API_SECRET 102 | 103 | srv = None 104 | decode_result = True 105 | timeout = 30 106 | max_retries = 10 107 | retry_delay = 5 108 | if srv: 109 | self.server = srv 110 | self.decode_result = decode_result 111 | assert timeout >= 0 or timeout is None 112 | assert max_retries >= 0 113 | self.timeout = timeout 114 | self.max_retries = max_retries 115 | self.retry_delay = retry_delay 116 | 117 | _setup_apiobj(self, self, '', []) 118 | 119 | def update_request(self, request): 120 | """overwrite this function to update the request before sending it to 121 | server""" 122 | pass 123 | 124 | 125 | def _setup_apiobj(self, api, prefix, path): 126 | if self is not api: 127 | self._api = api 128 | self._urlbase = '{server}/{prefix}/{path}'.format(server=api.server, prefix=prefix, path='/'.join(path)) 129 | 130 | lvl = len(path) 131 | done = set() 132 | for prefix, paths in _APIS: 133 | for i in paths: 134 | if len(i) <= lvl: 135 | continue 136 | cur = i[lvl] 137 | if i[:lvl] == path and cur not in done: 138 | done.add(cur) 139 | setattr(self, cur, _APIProxy(api, prefix, i[:lvl + 1])) 140 | 141 | 142 | class _APIProxy(object): 143 | _api = None 144 | """underlying :class:`API` object""" 145 | 146 | _urlbase = None 147 | 148 | def __init__(self, api, prefix, path): 149 | _setup_apiobj(self, api, prefix, path) 150 | 151 | def __call__(self, *args, **kargs): 152 | if len(args): 153 | raise TypeError('Only keyword arguments are allowed') 154 | form = _MultiPartForm() 155 | for (k, v) in kargs.items(): 156 | if isinstance(v, File): 157 | form.add_file(k, v.get_filename(), v.content) 158 | 159 | url = self._urlbase 160 | for k, v in self._mkarg(kargs).items(): 161 | form.add_field(k, v) 162 | 163 | body = form.bytes 164 | request = Request(url, data=body) 165 | request.add_header('Content-type', form.get_content_type()) 166 | request.add_header('Content-length', str(len(body))) 167 | 168 | self._api.update_request(request) 169 | 170 | retry = self._api.max_retries 171 | while True: 172 | retry -= 1 173 | try: 174 | ret = urlopen(request, timeout=self._api.timeout).read() 175 | break 176 | except HTTPError as e: 177 | raise APIError(e.code, url, e.read()) 178 | except (socket.error, URLError) as e: 179 | if retry < 0: 180 | raise e 181 | _print_debug('caught error: {}; retrying'.format(e)) 182 | time.sleep(self._api.retry_delay) 183 | 184 | if self._api.decode_result: 185 | try: 186 | ret = json.loads(ret, object_hook=ObjectDict) 187 | except: 188 | raise APIError(-1, url, 'json decode error, value={0!r}'.format(ret)) 189 | return ret 190 | 191 | def _mkarg(self, kargs): 192 | """change the argument list (encode value, add api key/secret) 193 | :return: the new argument list""" 194 | 195 | kargs = kargs.copy() 196 | kargs['api_key'] = self._api.key 197 | kargs['api_secret'] = self._api.secret 198 | for k, v in list(kargs.items()): 199 | if isinstance(v, Iterable) and not isinstance(v, basestring): 200 | kargs[k] = ','.join(v) 201 | elif isinstance(v, File) or v is None: 202 | del kargs[k] 203 | elif isinstance(v, numeric_types): 204 | kargs[k] = str(v) 205 | else: 206 | kargs[k] = v 207 | 208 | return kargs 209 | 210 | 211 | class _MultiPartForm(object): 212 | 213 | """Accumulate the data to be used when posting a form.""" 214 | 215 | def __init__(self): 216 | self.form_fields = [] 217 | self.files = [] 218 | self.boundary = choose_boundary() 219 | 220 | def get_content_type(self): 221 | return 'multipart/form-data; boundary={}'.format(self.boundary) 222 | 223 | def add_field(self, name, value): 224 | """Add a simple field to the form data.""" 225 | self.form_fields.append((name, value)) 226 | 227 | def add_file(self, fieldname, filename, content, mimetype=None): 228 | """Add a file to be uploaded.""" 229 | if mimetype is None: 230 | mimetype = mimetypes.guess_type(filename)[0] or 'application/octet-stream' 231 | self.files.append((fieldname, filename, mimetype, content)) 232 | 233 | @property 234 | def bytes(self): 235 | """Return a string(2.x) or bytes(3.x) representing the form data, including attached files.""" 236 | # Build a list of lists, each containing "lines" of the 237 | # request. Each part is separated by a boundary string. 238 | # Once the list is built, return a string where each 239 | # line is separated by '\r\n'. 240 | parts = [] 241 | part_boundary = "--" + self.boundary 242 | 243 | # Add the form fields 244 | parts.extend( 245 | [part_boundary, 246 | 'Content-Disposition: form-data; name="{}"'.format(name), '', value] 247 | for name, value in self.form_fields 248 | ) 249 | 250 | # Add the files to upload 251 | parts.extend( 252 | [part_boundary, 253 | 'Content-Disposition: form-data; name="{}"; filename="{}"'.format(field_name, filename), 254 | 'Content-Type: {}'.format(content_type), 255 | '', 256 | body, 257 | ] 258 | for field_name, filename, content_type, body in self.files 259 | ) 260 | 261 | # Flatten the list and add closing boundary marker, 262 | # then return CR+LF separated data 263 | flattened = list(itertools.chain(*parts)) 264 | flattened.append(part_boundary + '--') 265 | flattened.append('') 266 | return b'\r\n'.join(enc(x) for x in flattened) 267 | 268 | 269 | def _print_debug(msg): 270 | if DEBUG_LEVEL: 271 | sys.stderr.write(str(msg) + '\n') 272 | 273 | 274 | _APIS = [ 275 | { 276 | 'prefix': 'facepp/v3', 277 | 'paths': [ 278 | '/detect', 279 | '/compare', 280 | '/search', 281 | '/faceset/create', 282 | '/faceset/addface', 283 | '/faceset/removeface', 284 | '/faceset/update', 285 | '/faceset/getdetail', 286 | '/faceset/delete', 287 | '/faceset/getfacesets', 288 | '/face/analyze', 289 | '/face/getdetail', 290 | '/face/setuserid', 291 | ], 292 | }, 293 | { 294 | 'prefix': 'humanbodypp/v1', 295 | 'paths': [ 296 | '/detect', 297 | '/segment', 298 | ] 299 | }, 300 | { 301 | 'prefix': 'cardpp/v1', 302 | 'paths': [ 303 | '/ocridcard', 304 | '/ocrdriverlicense', 305 | '/ocrvehiclelicense', 306 | '/ocrbankcard', 307 | ] 308 | }, 309 | { 310 | 'prefix': 'imagepp/v1', 311 | 'paths': [ 312 | '/licenseplate', 313 | '/recognizetext', 314 | '/mergeface' 315 | ] 316 | } 317 | ] 318 | 319 | _APIS = [(i['prefix'], [p.split('/')[1:] for p in i['paths']]) for i in _APIS] 320 | --------------------------------------------------------------------------------