├── .editorconfig ├── .gitignore ├── .travis.yml ├── AUTHORS.rst ├── HISTORY.rst ├── LICENSE ├── MANIFEST.in ├── README.md ├── requirements_dev.txt ├── setup.cfg ├── setup.py ├── stf_selector ├── __init__.py ├── query.py ├── selector.py └── stf.py ├── tests ├── __init__.py ├── conftest.py └── test_stf_selector.py └── travis_pypi_setup.py /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [*.bat] 14 | indent_style = tab 15 | end_of_line = crlf 16 | 17 | [LICENSE] 18 | insert_final_newline = false 19 | 20 | [Makefile] 21 | indent_style = tab 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | # pyenv python configuration file 62 | .python-version 63 | 64 | # pycharm ide 65 | .idea/ 66 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Config file for automatic testing at travis-ci.org 2 | # This file will be regenerated if you run travis_pypi_setup.py 3 | 4 | language: python 5 | python: 3.5 6 | 7 | env: 8 | - TOXENV=py35 9 | - TOXENV=py34 10 | - TOXENV=py33 11 | - TOXENV=py27 12 | - TOXENV=py26 13 | - TOXENV=pypy 14 | 15 | # command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors 16 | install: pip install -U tox 17 | 18 | # command to run tests, e.g. python setup.py test 19 | script: tox -e ${TOXENV} 20 | 21 | # After you create the Github repo and add it to Travis, run the 22 | # travis_pypi_setup.py script to finish PyPI deployment setup 23 | deploy: 24 | provider: pypi 25 | distributions: sdist bdist_wheel 26 | user: wliu_intern 27 | password: 28 | secure: PLEASE_REPLACE_ME 29 | on: 30 | tags: true 31 | repo: wliu_intern/stf_selector 32 | condition: $TOXENV == py27 33 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Credits 3 | ======= 4 | 5 | Development Lead 6 | ---------------- 7 | 8 | * Juan Liu 9 | 10 | Contributors 11 | ------------ 12 | 13 | None yet. Why not be the first? 14 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | History 3 | ======= 4 | 5 | 0.1.0 (2016-12-21) 6 | ------------------ 7 | 8 | * First release on PyPI. 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache Software License 2.0 3 | 4 | Copyright (c) 2016, Juan Liu 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | 18 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | 2 | include AUTHORS.rst 3 | 4 | include CONTRIBUTING.rst 5 | include HISTORY.rst 6 | include LICENSE 7 | include README.rst 8 | 9 | recursive-include tests * 10 | recursive-exclude * __pycache__ 11 | recursive-exclude * *.py[co] 12 | 13 | recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # stf-selector 2 | 3 | The stf-selector package is designed for supporting query available mobile devices through STF restful api with flexible conditions. 4 | 5 | ## Getting Started 6 | 7 | ### Installing stf-selector 8 | 9 | To install **stf-selector** from PyPI, run: 10 | 11 | $ pip install stf-selector 12 | 13 | You can also grab the latest development version from [GitHub][github]. After downloading and unpacking it, you can install it using: 14 | 15 | [github]: https://github.com/RedQA/stf-selector 16 | 17 | $ python setup.py install 18 | 19 | ## STF 20 | Because stf-selector is based on the [STF] '(Smartphone Test Farm)', you should install **STF** firstly and then read the [STF API]. If you already know the stf, I suggest you skip to this. 21 | 22 | [STF]: https://github.com/openstf/stf 23 | [STF API]: https://github.com/openstf/stf/blob/master/doc/API.md 24 | 25 | STF project and api document are avaliable from below: 26 | 27 | *STF github: 28 | 29 | *STF API: 30 | 31 | 32 | ## Usage 33 | According to STF API, you need get token firstly. If you don't know how to get token, please refer to [Authentication]. 34 | 35 | [Authentication]: https://github.com/openstf/stf/blob/master/doc/API.md#authentication 36 | ``` python 37 | #!/usr/bin/env python 38 | # -*- coding: utf-8 -*- 39 | from stf_selector.selector import Selector 40 | from stf_selector.query import where 41 | 42 | # Your STF's url, like: https://stf.example.org/api/v1/user 43 | url = "" 44 | # Token genereated from the STF, some like:"3e5dd447c..." 45 | token = "" 46 | ``` 47 | 48 | ### Example Code 49 | #### Basic usage 50 | Get all the devices on the STF 51 | 52 | ```python 53 | s = Selector(url=url, token=token) 54 | device_list = s.load().devices() 55 | ``` 56 | 57 | #### Query Language 58 | Search for a field value 59 | 60 | ```python 61 | s = Selector(url=url, token=token) 62 | s.load() 63 | device_list = s.find(where("manufacturer")=="SAMSUNG").devices() 64 | ``` 65 | 66 | **Note:** 67 | Because the devices on the STF can be used many people at the same time, which results in devices using or releasing at any time, you need **refresh()** at appropriate frequency. 68 | 69 | Search with combing two queries with logical '&' or 'and'. 70 | 71 | ```python 72 | s = Selector(url=url, token=token) 73 | s.load() 74 | conds = (where("manufacturer")=="SAMSUNG") & (where('version')=='5.0') 75 | device_list = s.find(conds).devices() 76 | ``` 77 | 78 | **Note:** 79 | When using & or |, make sure you wrap the conditions on both sides with parentheses or Python will mess up the comparison. 80 | 81 | Search with combing two queries with logical '|' or 'or'. 82 | 83 | ```python 84 | s = Selector(url=url, token=token) 85 | s.load() 86 | conds = (where("manufacturer")=="SAMSUNG") |(where("manufacturer")=="OPPO")) 87 | device_list = s.find(cond=conds).devices() 88 | # More possible comparisons: != < > <= >= 89 | ``` 90 | 91 | #### More about query 92 | You’ve learned about the basic comparisons (==, <, >, ...). In addition to these query supports the following queries: 93 | 94 | 1. Existence of a 'field': 95 | 96 | ``` python 97 | s = Selector(url=url, token=token) 98 | s.load() 99 | s = s.find(where('sdk').exists()) 100 | ``` 101 | 2. Regex: 102 | 103 | ``` python 104 | s = Selector(url=url, token=token) 105 | s.load() 106 | # return all model start with SM... 107 | s = s.find(where('model').matches('SM*')) 108 | ``` 109 | 110 | 3. Custom test: 111 | 112 | ``` python 113 | s = Selector(url=url, token=token) 114 | s.load() 115 | test_func = lambda x: x == '19' 116 | s = s.find(where('sdk').test(test_func)) 117 | ``` 118 | 119 | 4. Custom test with parameters: 120 | 121 | ``` python 122 | s = Selector(url=url, token=token) 123 | s.load() 124 | # return moblies which devices sdk is between m and n 125 | def test_func(val, m, n): 126 | return int(m) <= int(val) <= int(n) 127 | s = s.find(where('sdk').test(test_func, 19, 21)) 128 | ``` 129 | 130 | 5. When a field contains a list, you also can use the following methods: 131 | 132 | ``` python 133 | # Using a where: 134 | s = Selector(url=url, token=token) 135 | s.load() 136 | 137 | # Chrome is member of at least one of name groups 138 | s = s.find(where('browser')['apps'].any(where('name')=='Chrome')) 139 | 140 | s.refresh() 141 | # Chrome is only member of name groups 142 | s = s.find(where('browser')['apps'].all(where('name')=='Chrome')) 143 | ``` -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | pip==8.1.2 2 | bumpversion==0.5.3 3 | wheel==0.29.0 4 | watchdog==0.8.3 5 | flake8==2.6.0 6 | tox==2.3.1 7 | coverage==4.1 8 | Sphinx==1.4.8 9 | cryptography==1.7 10 | PyYAML==3.11 11 | pytest==3.0.3 12 | 13 | # add by juan 14 | tinydb==3.2.1 15 | requests==2.12.4 16 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.1.0 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:setup.py] 7 | search = version='{current_version}' 8 | replace = version='{new_version}' 9 | 10 | [bumpversion:file:stf_selector/__init__.py] 11 | search = __version__ = '{current_version}' 12 | replace = __version__ = '{new_version}' 13 | 14 | [bdist_wheel] 15 | universal = 1 16 | 17 | [flake8] 18 | exclude = docs 19 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from setuptools import setup 5 | 6 | with open('README.rst') as readme_file: 7 | readme = readme_file.read() 8 | 9 | with open('HISTORY.rst') as history_file: 10 | history = history_file.read() 11 | 12 | requirements = [ 13 | 'Click>=6.0', 14 | 'pytest>=3.0.3', 15 | 'tinydb>=3.2.1', 16 | 'requests>=2.12.4', 17 | # TODO: put package requirements here 18 | ] 19 | 20 | test_requirements = [ 21 | # TODO: put package test requirements here 22 | ] 23 | 24 | setup( 25 | name='stf_selector', 26 | version='0.1.0', 27 | description="stf devices stf_selector", 28 | long_description=readme + '\n\n' + history, 29 | author="Juan Liu", 30 | author_email='littlewei_liu@163.com', 31 | url='https://github.com/wliu_intern/stf_selector', 32 | packages=[ 33 | 'stf_selector', 34 | ], 35 | package_dir={'stf_selector': 36 | 'stf_selector'}, 37 | entry_points={ 38 | 'console_scripts': [ 39 | 'stf_selector=stf_selector.cli:main' 40 | ] 41 | }, 42 | include_package_data=True, 43 | install_requires=requirements, 44 | license="Apache Software License 2.0", 45 | zip_safe=False, 46 | keywords='stf_selector', 47 | classifiers=[ 48 | 'Development Status :: 2 - Pre-Alpha', 49 | 'Intended Audience :: Developers', 50 | 'License :: OSI Approved :: Apache Software License', 51 | 'Natural Language :: English', 52 | "Programming Language :: Python :: 2", 53 | 'Programming Language :: Python :: 2.6', 54 | 'Programming Language :: Python :: 2.7', 55 | 'Programming Language :: Python :: 3', 56 | 'Programming Language :: Python :: 3.3', 57 | 'Programming Language :: Python :: 3.4', 58 | 'Programming Language :: Python :: 3.5', 59 | ], 60 | test_suite='tests', 61 | tests_require=test_requirements 62 | ) 63 | -------------------------------------------------------------------------------- /stf_selector/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | __author__ = """Juan Liu""" 4 | __email__ = 'wliu_intern@xiaohongshu.com' 5 | __version__ = '0.1.0' 6 | -------------------------------------------------------------------------------- /stf_selector/query.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from tinydb import Query 5 | 6 | 7 | def where(key): 8 | return Query([key]) 9 | -------------------------------------------------------------------------------- /stf_selector/selector.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | stf devices stf_selector 4 | 5 | some fields we often use 6 | =============================================== 7 | field e.g. 8 | manufacturer OPPO 9 | version 5.1.1 10 | display {height:1920,width:1080} 11 | sdk 22 12 | serial 778d4f10 13 | platform android 14 | mode Plusm A 15 | .... ... 16 | =============================================== 17 | """ 18 | 19 | import logging 20 | 21 | from stf import STF 22 | from tinydb import TinyDB 23 | from tinydb.storages import MemoryStorage 24 | 25 | logger = logging.getLogger(__name__) 26 | 27 | TinyDB.DEFAULT_STORAGE = MemoryStorage 28 | 29 | 30 | class Selector(object): 31 | """ 32 | According to user's requirements to select devices 33 | """ 34 | 35 | def __init__(self, url=None, token=None): 36 | """ 37 | Construct method 38 | """ 39 | self._db = TinyDB(storage=MemoryStorage) 40 | self._url = url 41 | self._token = token 42 | 43 | def load(self): 44 | """ 45 | Use the data which got from stf platform to crate query db 46 | 47 | :return: the len of records in the db's table 48 | """ 49 | res = STF().devices(url=self._url, token=self._token) 50 | if res is not None: 51 | list_devices = res['devices'] 52 | self._db.insert_multiple(list_devices) 53 | return self.count() 54 | else: 55 | return 0 56 | 57 | def find(self, cond=None): 58 | """ 59 | According condition to filter devices and return 60 | :param cond: condition to filter devices 61 | :type cond: where 62 | :return: stf_selector object and its db contains devices 63 | """ 64 | if cond is not None: 65 | res = self._db.search(cond) 66 | self.purge() 67 | self._db.insert_multiple(res) 68 | return self 69 | 70 | def devices(self): 71 | """ 72 | return all devices that meeting the requirement 73 | :return: list of devices 74 | """ 75 | return self._db.all() 76 | 77 | def refresh(self): 78 | """ 79 | reload the devices info from stf 80 | :return: the len of records in the db's table 81 | """ 82 | self.purge() 83 | return self.load() 84 | 85 | def count(self): 86 | """ 87 | count the records in the db's table 88 | :return: the len of records in the db's table 89 | """ 90 | return len(self._db.all()) 91 | 92 | def purge(self): 93 | """ 94 | remove all the data from the db 95 | :return: 96 | """ 97 | self._db.purge() 98 | -------------------------------------------------------------------------------- /stf_selector/stf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """Contains the : class: 'STF'""" 5 | import requests 6 | import logging 7 | import json 8 | import yaml 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | 13 | class STF(object): 14 | """ 15 | Use this module to get devices from remote stf platform 16 | """ 17 | 18 | def __init__(self): 19 | pass 20 | 21 | def devices(self, url=None, token=None): 22 | """ 23 | Send request to stf and get devices 24 | 25 | :param url: the address of stf platform 26 | :type url: str 27 | :param token: token which used to login in stf 28 | like : '3e5dd447cd334d549c849d19707eb269df74cabd67......' 29 | :type token: dict 30 | :return: if 'success' is True, return list of devices. 31 | Otherwise, return None. 32 | """ 33 | if url is None: 34 | logger.info("Please set the request address!") 35 | return None 36 | if token is not None: 37 | auth = dict() 38 | auth["Authorization"] = 'Bearer ' + token 39 | auth = auth 40 | response = requests.get(url=url, headers=auth) 41 | res = response.json() 42 | if res is not None and res["success"] is True: 43 | return yaml.safe_load(json.dumps(res)) # unicode to str 44 | else: 45 | logger.info("stf response false:" + str(res)) 46 | return None 47 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import pytest 4 | 5 | 6 | @pytest.fixture() 7 | def generate_data(): 8 | data = \ 9 | { 10 | 'devices': [ 11 | { 12 | 'status': 3, 13 | 'product': 'R9PlusmA', 14 | 'browser': { 15 | 'selected': True, 16 | 'apps': [ 17 | { 18 | 'name': 'Browser', 19 | 'developer': 'GoogleInc.', 20 | 'selected': True, 21 | 'type': 'android-browser', 22 | 'system': True, 23 | 'id': 'com.android.browser/.RealBrowserActivity' 24 | } 25 | ] 26 | }, 27 | 'reverseForwards': [ 28 | 29 | ], 30 | 'battery': { 31 | 'status': 'full', 32 | 'source': 'usb', 33 | 'scale': 100, 34 | 'health': 'good', 35 | 'voltage': 4.262, 36 | 'temp': 29.7, 37 | 'level': 100 38 | }, 39 | 'statusChangedAt': '2016-10-21T13: 29: 16.111Z', 40 | 'operator': ',', 41 | 'owner': None, 42 | 'airplaneMode': False, 43 | 44 | 'presenceChangedAt': '2016-10-22T03: 28: 40.586Z', 45 | 'ready': True, 46 | 'using': False, 47 | 'serial': '778d4f10', 48 | 'createdAt': '2016-10-21T12: 30: 24.250Z', 49 | 'sdk': '22', 50 | 'network': { 51 | 'subtype': '', 52 | 'failover': False, 53 | 'connected': True, 54 | 'roaming': False, 55 | 'type': 'WIFI' 56 | }, 57 | 'remoteConnect': False, 58 | 'abi': 'arm64-v8a', 59 | 'remoteConnectUrl': None, 60 | 'platform': 'Android', 61 | 'version': '5.1.1', 62 | 'present': False, 63 | 'provider': { 64 | 'name': 'red-Inspiron-3647', 65 | 'channel': 'O1HNR2n6Q+iWYV2YAEZRhw==' 66 | }, 67 | 'model': 'R9PlusmA', 68 | 'manufacturer': 'OPPO', 69 | 'display': { 70 | 'secure': True, 71 | 'density': 3, 72 | 'url': 'ws: //10.12.147.98: 7424', 73 | 'height': 1920, 74 | 'xdpi': 370.7019958496094, 75 | 'width': 1080, 76 | 'fps': 60, 77 | 'rotation': 0, 78 | 'id': 0, 79 | 'ydpi': 369.4540100097656, 80 | 'size': 5.957783222198486 81 | }, 82 | 'channel': 'a/nbaUCQMduULvq/8HeNTwtCWNs=', 83 | 'phone': { 84 | 'imei': '862732035986079', 85 | 'iccid': None, 86 | 'phoneNumber': None, 87 | 'network': 'UNKNOWN' 88 | } 89 | }, 90 | { 91 | 'status': 3, 92 | 'product': 'N5117', 93 | 'browser': { 94 | 'selected': False, 95 | 'apps': [ 96 | { 97 | 'name': 'Browser', 98 | 'developer': 'GoogleInc.', 99 | 'selected': False, 100 | 'type': 'android-browser', 101 | 'system': True, 102 | 'id': 'com.android.browser/.RealBrowserActivity' 103 | } 104 | ] 105 | }, 106 | 'reverseForwards': [ 107 | 108 | ], 109 | 'statusChangedAt': '2016-12-15T03: 14: 22.417Z', 110 | 'operator': None, 111 | 'owner': None, 112 | 'airplaneMode': False, 113 | 'presenceChangedAt': '2016-12-21T03: 56: 37.509Z', 114 | 'ready': True, 115 | 'using': False, 116 | 'serial': '9be75a9b', 117 | 'createdAt': '2016-10-22T03: 31: 17.129Z', 118 | 'manufacturer': 'OPPO', 119 | 'network': { 120 | 'subtype': '', 121 | 'failover': False, 122 | 'connected': True, 123 | 'roaming': False, 124 | 'type': 'WIFI' 125 | }, 126 | 'remoteConnect': False, 127 | 'abi': 'armeabi-v7a', 128 | 'remoteConnectUrl': None, 129 | 'platform': 'Android', 130 | 'version': '4.3', 131 | 'present': False, 132 | 'provider': { 133 | 'name': 'red-Inspiron-3647', 134 | 'channel': 'DSvnAQnqRUe6/ROim+ogjQ==' 135 | }, 136 | 'model': 'N5117', 137 | 'sdk': '18', 138 | 'display': { 139 | 'secure': True, 140 | 'density': 2, 141 | 'url': 'ws: //localhost: 7516', 142 | 'height': 1280, 143 | 'xdpi': 309.96600341796875, 144 | 'width': 720, 145 | 'fps': 61, 146 | 'rotation': 0, 147 | 'id': 0, 148 | 'ydpi': 312.614990234375, 149 | 'size': 4.7074875831604 150 | }, 151 | 'channel': '8d6J8hsyZkqKz3ncKSkyzTWskM4=', 152 | 'phone': { 153 | 'imei': '864181023864435', 154 | 'iccid': None, 155 | 'phoneNumber': None, 156 | 'network': 'UNKNOWN' 157 | } 158 | }, 159 | { 160 | 'status': 1, 161 | 'product': 'kltezn', 162 | 'browser': { 163 | 'selected': False, 164 | 'apps': [ 165 | { 166 | 'name': 'Browser', 167 | 'developer': 'Samsung', 168 | 'selected': False, 169 | 'type': 'samsung-sbrowser', 170 | 'system': True, 171 | 'id': 'com.sec.android.app.sbrowser/.SBrowserLauncherActivity' 172 | } 173 | ] 174 | }, 175 | 'reverseForwards': [ 176 | 177 | ], 178 | 'battery': { 179 | 'status': 'charging', 180 | 'source': 'usb', 181 | 'scale': 100, 182 | 'health': 'good', 183 | 'voltage': 4.32, 184 | 'temp': 30.4, 185 | 'level': 100 186 | }, 187 | 'statusChangedAt': '2016-12-19T08: 10: 28.022Z', 188 | 'operator': None, 189 | 'owner': None, 190 | 'airplaneMode': False, 191 | 'presenceChangedAt': '2016-12-19T08: 10: 28.055Z', 192 | 'ready': False, 193 | 'using': False, 194 | 'serial': 'fe3eb2b6', 195 | 'createdAt': '2016-10-21T12: 42: 30.030Z', 196 | 'sdk': '23', 197 | 'network': { 198 | 'subtype': '', 199 | 'failover': False, 200 | 'connected': True, 201 | 'roaming': False, 202 | 'type': 'WIFI' 203 | }, 204 | 'remoteConnect': False, 205 | 'abi': 'armeabi-v7a', 206 | 'remoteConnectUrl': None, 207 | 'platform': 'Android', 208 | 'version': '6.0.1', 209 | 'present': False, 210 | 'provider': { 211 | 'name': 'red-Inspiron-3647', 212 | 'channel': 'DSvnAQnqRUe6/ROim+ogjQ==' 213 | }, 214 | 'model': 'SM-G9006V', 215 | 'manufacturer': 'SAMSUNG', 216 | 'display': { 217 | 'secure': True, 218 | 'density': 3, 219 | 'url': 'ws: //localhost: 7424', 220 | 'height': 1920, 221 | 'xdpi': 422.0299987792969, 222 | 'width': 1080, 223 | 'fps': 60, 224 | 'rotation': 0, 225 | 'id': 0, 226 | 'ydpi': 424.0690002441406, 227 | 'size': 5.200733661651611 228 | }, 229 | 'channel': '9ZhYZvGxjWkUaX2UmKDCRqKZmp0=', 230 | 'phone': { 231 | 'imei': '352621066378644', 232 | 'iccid': None, 233 | 'phoneNumber': None, 234 | 'network': 'UNKNOWN' 235 | } 236 | }, 237 | { 238 | 'status': 3, 239 | 'product': 'h3gduoszn', 240 | 'browser': { 241 | 'selected': True, 242 | 'apps': [ 243 | { 244 | 'name': 'Browser', 245 | 'developer': 'Samsung', 246 | 'selected': True, 247 | 'type': 'samsung-sbrowser', 248 | 'system': True, 249 | 'id': 'com.sec.android.app.sbrowser/.SBrowserMainActivity' 250 | } 251 | ] 252 | }, 253 | 'reverseForwards': [ 254 | 255 | ], 256 | 'battery': { 257 | 'status': 'full', 258 | 'source': 'usb', 259 | 'scale': 100, 260 | 'health': 'good', 261 | 'voltage': 4.327, 262 | 'temp': 32.1, 263 | 'level': 100 264 | }, 265 | 'statusChangedAt': '2016-12-19T07: 11: 23.626Z', 266 | 'operator': ',', 267 | 'owner': None, 268 | 'airplaneMode': False, 269 | 'presenceChangedAt': '2016-12-19T07: 11: 23.423Z', 270 | 'ready': True, 271 | 'using': False, 272 | 'serial': 'e1a0ee1a', 273 | 'createdAt': '2016-12-01T08: 36: 39.627Z', 274 | 'sdk': '21', 275 | 'network': { 276 | 'subtype': None, 277 | 'failover': False, 278 | 'connected': False, 279 | 'roaming': False, 280 | 'type': None 281 | }, 282 | 'remoteConnect': False, 283 | 'abi': 'armeabi-v7a', 284 | 'remoteConnectUrl': None, 285 | 'platform': 'Android', 286 | 'version': '5.0', 287 | 'present': True, 288 | 'provider': { 289 | 'name': 'red-Inspiron-3647', 290 | 'channel': 'DSvnAQnqRUe6/ROim+ogjQ==' 291 | }, 292 | 'model': 'SM-N9002', 293 | 'manufacturer': 'SAMSUNG', 294 | 'display': { 295 | 'secure': True, 296 | 'density': 3, 297 | 'url': 'ws: //localhost: 7664', 298 | 'height': 1920, 299 | 'xdpi': 386.3659973144531, 300 | 'width': 1080, 301 | 'fps': 60, 302 | 'rotation': 0, 303 | 'id': 0, 304 | 'ydpi': 387.0469970703125, 305 | 'size': 5.69398832321167 306 | }, 307 | 'channel': 'uAvfZRB+OfHpOoFZikYconvRw+I=', 308 | 'phone': { 309 | 'imei': '354224061720841', 310 | 'iccid': None, 311 | 'phoneNumber': None, 312 | 'network': 'UNKNOWN' 313 | } 314 | }, 315 | { 316 | 'status': 3, 317 | 'product': 'L39h', 318 | 'browser': { 319 | 'selected': False, 320 | 'apps': [ 321 | 322 | ] 323 | }, 324 | 'reverseForwards': [ 325 | 326 | ], 327 | 'battery': { 328 | 'status': 'full', 329 | 'source': 'usb', 330 | 'scale': 100, 331 | 'health': 'good', 332 | 'voltage': 4.287, 333 | 'temp': 29.1, 334 | 'level': 100 335 | }, 336 | 'statusChangedAt': '2016-12-15T03: 14: 31.020Z', 337 | 'operator': u'\u4e2d\u56fd\u8054\u901a', 338 | 'owner': None, 339 | 'airplaneMode': False, 340 | 'presenceChangedAt': '2016-12-19T07: 05: 44.377Z', 341 | 'ready': True, 342 | 'using': False, 343 | 'serial': 'BH90168J1J', 344 | 'createdAt': '2016-10-28T11: 52: 13.474Z', 345 | 'sdk': '19', 346 | 'network': { 347 | 'failover': False, 348 | 'manual': False, 349 | 'subtype': '', 350 | 'state': 'in_service', 351 | 'connected': True, 352 | 'operator': u'\u4e2d\u56fd\u8054\u901a', 353 | 'roaming': False, 354 | 'type': 'WIFI' 355 | }, 356 | 'remoteConnect': False, 357 | 'abi': 'armeabi-v7a', 358 | 'remoteConnectUrl': None, 359 | 'platform': 'Android', 360 | 'version': '4.4.2', 361 | 'present': False, 362 | 'provider': { 363 | 'name': 'red-Inspiron-3647', 364 | 'channel': 'DSvnAQnqRUe6/ROim+ogjQ==' 365 | }, 366 | 'model': 'L39h', 367 | 'manufacturer': 'SONY', 368 | 'display': { 369 | 'secure': True, 370 | 'density': 3, 371 | 'url': 'ws: //localhost: 7684', 372 | 'height': 1920, 373 | 'xdpi': 442.45098876953125, 374 | 'width': 1080, 375 | 'fps': 60, 376 | 'rotation': 0, 377 | 'id': 0, 378 | 'ydpi': 443.3450012207031, 379 | 'size': 4.971247673034668 380 | }, 381 | 'channel': 'ssK2BDmX4Pm6cWRxMTVPqGplC8s=', 382 | 'phone': { 383 | 'imei': '358094058424194', 384 | 'iccid': '89860115831012343258', 385 | 'phoneNumber': '+8618521771476', 386 | 'network': 'HSPA' 387 | } 388 | }, 389 | { 390 | 'status': 3, 391 | 'product': 'm7cdug', 392 | 'browser': { 393 | 'selected': False, 394 | 'apps': [ 395 | { 396 | 'name': 'Browser', 397 | 'developer': 'GoogleInc.', 398 | 'selected': False, 399 | 'type': 'android-browser', 400 | 'system': True, 401 | 'id': 'com.android.browser/.BrowserActivity' 402 | }, 403 | { 404 | 'name': 'Chrome', 405 | 'developer': 'GoogleInc.', 406 | 'selected': False, 407 | 'type': 'chrome', 408 | 'system': True, 409 | 'id': 'com.android.chrome/com.google.android.apps.chrome.Main' 410 | } 411 | ] 412 | }, 413 | 'reverseForwards': [ 414 | 415 | ], 416 | 'battery': { 417 | 'status': 'charging', 418 | 'source': 'usb', 419 | 'scale': 100, 420 | 'health': 'good', 421 | 'voltage': 3.733, 422 | 'temp': 36.4, 423 | 'level': 35 424 | }, 425 | 'statusChangedAt': '2016-10-28T12: 08: 56.434Z', 426 | 'operator': None, 427 | 'owner': None, 428 | 'airplaneMode': False, 429 | 'presenceChangedAt': '2016-10-28T12: 10: 29.571Z', 430 | 'ready': True, 431 | 'using': False, 432 | 'serial': 'HC34WW905103', 433 | 'createdAt': '2016-10-28T12: 06: 00.758Z', 434 | 'sdk': '17', 435 | 'network': { 436 | 'subtype': '', 437 | 'failover': False, 438 | 'connected': True, 439 | 'roaming': False, 440 | 'type': 'WIFI' 441 | }, 442 | 'remoteConnect': False, 443 | 'abi': 'armeabi-v7a', 444 | 'remoteConnectUrl': None, 445 | 'platform': 'Android', 446 | 'version': '4.2.2', 447 | 'present': False, 448 | 'provider': { 449 | 'name': 'red-Inspiron-3647', 450 | 'channel': '6VQ54XWTRsOI0H1tEPxOyg==' 451 | }, 452 | 'model': '802w', 453 | 'manufacturer': 'HTC', 454 | 'display': { 455 | 'secure': True, 456 | 'density': 3, 457 | 'url': 'ws: //localhost: 7584', 458 | 'height': 1920, 459 | 'xdpi': 472.9649963378906, 460 | 'width': 1080, 461 | 'fps': 60, 462 | 'rotation': 0, 463 | 'id': 0, 464 | 'ydpi': 473.4750061035156, 465 | 'size': 4.653842926025391 466 | }, 467 | 'channel': 'JUa9vSz2k34yeD8ooSyg5N8hU3g=', 468 | 'phone': { 469 | 'imei': '355868050315606', 470 | 'iccid': '89860114831009528060', 471 | 'phoneNumber': '+8614530607556', 472 | 'network': 'UNKNOWN' 473 | } 474 | }, 475 | { 476 | 'status': 3, 477 | 'product': 'R7', 478 | 'browser': { 479 | 'selected': True, 480 | 'apps': [ 481 | { 482 | 'name': 'Browser', 483 | 'developer': 'GoogleInc.', 484 | 'selected': True, 485 | 'type': 'android-browser', 486 | 'system': True, 487 | 'id': 'com.android.browser/.RealBrowserActivity' 488 | } 489 | ] 490 | }, 491 | 'reverseForwards': [ 492 | 493 | ], 494 | 'battery': { 495 | 'status': 'charging', 496 | 'source': 'usb', 497 | 'scale': 100, 498 | 'health': 'good', 499 | 'voltage': 4.165, 500 | 'temp': 29, 501 | 'level': 83 502 | }, 503 | 'statusChangedAt': '2016-12-26T01: 59: 41.431Z', 504 | 'operator': None, 505 | 'owner': None, 506 | 'airplaneMode': False, 507 | 'presenceChangedAt': '2016-12-26T01: 59: 41.992Z', 508 | 'ready': True, 509 | 'using': False, 510 | 'serial': 'MRO7VW6STGKZJNKZ', 511 | 'createdAt': '2016-11-28T10: 51: 32.099Z', 512 | 'sdk': '19', 513 | 'network': { 514 | 'subtype': '', 515 | 'failover': False, 516 | 'connected': True, 517 | 'roaming': False, 518 | 'type': 'WIFI' 519 | }, 520 | 'remoteConnect': False, 521 | 'abi': 'armeabi-v7a', 522 | 'remoteConnectUrl': None, 523 | 'platform': 'Android', 524 | 'version': '4.4.4', 525 | 'present': True, 526 | 'provider': { 527 | 'name': 'red-Inspiron-3647', 528 | 'channel': 'DSvnAQnqRUe6/ROim+ogjQ==' 529 | }, 530 | 'model': 'R7', 531 | 'manufacturer': 'OPPO', 532 | 'display': { 533 | 'secure': True, 534 | 'density': 3, 535 | 'url': 'ws: //localhost: 7596', 536 | 'height': 1920, 537 | 'xdpi': 442.45098876953125, 538 | 'width': 1080, 539 | 'fps': 59.06999969482422, 540 | 'rotation': 0, 541 | 'id': 0, 542 | 'ydpi': 439.35101318359375, 543 | 'size': 5.005581378936768 544 | }, 545 | 'channel': '4CnBXaOMdwgkYblOxCzsswrxHZ0=', 546 | 'phone': { 547 | 'imei': '868064022230822', 548 | 'iccid': None, 549 | 'phoneNumber': None, 550 | 'network': 'UNKNOWN' 551 | } 552 | }, 553 | { 554 | 'status': 3, 555 | 'product': 'M3s', 556 | 'browser': { 557 | 'selected': True, 558 | 'apps': [ 559 | { 560 | 'name': 'Browser', 561 | 'developer': 'GoogleInc.', 562 | 'selected': True, 563 | 'type': 'android-browser', 564 | 'system': True, 565 | 'id': 'com.android.browser/.BrowserActivity' 566 | } 567 | ] 568 | }, 569 | 'reverseForwards': [ 570 | 571 | ], 572 | 'battery': { 573 | 'status': 'full', 574 | 'source': 'usb', 575 | 'scale': 100, 576 | 'health': 'good', 577 | 'voltage': 4.417, 578 | 'temp': 34, 579 | 'level': 100 580 | }, 581 | 'statusChangedAt': '2016-12-07T09: 03: 30.854Z', 582 | 'operator': ',', 583 | 'owner': None, 584 | 'airplaneMode': False, 585 | 'presenceChangedAt': '2016-12-12T07: 35: 49.792Z', 586 | 'ready': True, 587 | 'using': False, 588 | 'serial': 'Y15QKBP323GGV', 589 | 'createdAt': '2016-12-07T09: 02: 41.845Z', 590 | 'sdk': '22', 591 | 'network': { 592 | 'subtype': '', 593 | 'failover': False, 594 | 'connected': True, 595 | 'roaming': False, 596 | 'type': 'WIFI' 597 | }, 598 | 'remoteConnect': False, 599 | 'abi': 'arm64-v8a', 600 | 'remoteConnectUrl': None, 601 | 'platform': 'Android', 602 | 'version': '5.1', 603 | 'present': False, 604 | 'provider': { 605 | 'name': 'red-Inspiron-3647', 606 | 'channel': 'pSi60xnaQP6anibXbtJvyw==' 607 | }, 608 | 'model': 'M3s', 609 | 'manufacturer': 'MEIZU', 610 | 'display': { 611 | 'secure': True, 612 | 'density': 2, 613 | 'url': 'ws: //localhost: 7452', 614 | 'height': 1280, 615 | 'xdpi': 320, 616 | 'width': 720, 617 | 'fps': 57.99000549316406, 618 | 'rotation': 0, 619 | 'id': 0, 620 | 'ydpi': 320, 621 | 'size': 4.589389801025391 622 | }, 623 | 'channel': 'x7RKS3VuDTxP/qHCqVAJYNBBJAg=', 624 | 'phone': { 625 | 'imei': '86229903536379', 626 | 'iccid': None, 627 | 'phoneNumber': None, 628 | 'network': 'UNKNOWN' 629 | } 630 | }, 631 | { 632 | 'status': 2, 633 | 'ready': False, 634 | 'reverseForwards': [ 635 | 636 | ], 637 | 'statusChangedAt': '2016-11-18T05: 55: 02.443Z', 638 | 'remoteConnectUrl': None, 639 | 'remoteConnect': False, 640 | 'presenceChangedAt': '2016-11-18T12: 03: 26.915Z', 641 | 'createdAt': '2016-11-18T05: 55: 01.519Z', 642 | 'provider': { 643 | 'name': 'red-Inspiron-3647', 644 | 'channel': '0e8BxImgQ9Wm38GOl6s7Pw==' 645 | }, 646 | 'owner': None, 647 | 'using': False, 648 | 'serial': 'd33cc1c4', 649 | 'present': False 650 | } 651 | ], 652 | 'success': True 653 | } 654 | return data 655 | -------------------------------------------------------------------------------- /tests/test_stf_selector.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | test_stf_selector 6 | ---------------------------------- 7 | 8 | Tests for `selector` module. 9 | """ 10 | 11 | from mock import patch 12 | from stf_selector.stf import STF 13 | from stf_selector.query import where 14 | from stf_selector.selector import Selector 15 | 16 | 17 | @patch.object(STF, 'devices') 18 | def test_find_without_cond(mock_devices, generate_data): 19 | """ 20 | test find method with no cond 21 | :return: len of devices 22 | """ 23 | mock_devices.return_value = generate_data 24 | s = Selector() 25 | s.load() 26 | s = s.find() 27 | assert s.count() == 9 28 | 29 | 30 | @patch.object(STF, 'devices') 31 | def test_find_with_one_cond(mock_devices, generate_data): 32 | """ 33 | test find method with one cond 34 | 35 | :param cond: condition to filter devices. 36 | like : where("sdk")==19 the details syntax 37 | See more at: http:// 38 | :type cond: where 39 | :return: len of device 40 | """ 41 | mock_devices.return_value = generate_data 42 | s = Selector() 43 | s.load() 44 | 45 | cond = where("sdk") == '19' 46 | s = s.find(cond=cond) 47 | assert s.count() == 2 48 | 49 | 50 | @patch.object(STF, 'devices') 51 | def test_find_with_multi_conds(mock_devices, generate_data): 52 | """ 53 | test find method with multi cond 54 | 55 | condition to filter devices. 56 | there are two ways to do muitl filter 57 | Firstly: 58 | like : (where("sdk")==19) & (where("manufacturer") == 'OPPO') 59 | like : (where("sdk")==19) | (where("manufacturer") == 'OPPO') 60 | or like :((where("manufacturer") == 'SAMSUNG') | (where("manufacturer") == 'OPPO')) & (where("sdk")==19) 61 | Secondly: 62 | s.find(cond=cond).find(cond=cond) 63 | or : s.find(cond=cond).find(cond=cond).find(...) 64 | See more at: http:// 65 | :type : where 66 | :return: len of device 67 | """ 68 | mock_devices.return_value = generate_data 69 | s = Selector() 70 | s.load() 71 | 72 | # you can code like 73 | cond = ((where("manufacturer") == 'SAMSUNG') 74 | | (where("manufacturer") == 'OPPO')) \ 75 | & (where("sdk") == '19') 76 | s = s.find(cond=cond) 77 | assert s.count() == 1 78 | -------------------------------------------------------------------------------- /travis_pypi_setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """Update encrypted deploy password in Travis config file 4 | """ 5 | 6 | 7 | from __future__ import print_function 8 | import base64 9 | import json 10 | import os 11 | from getpass import getpass 12 | import yaml 13 | from cryptography.hazmat.primitives.serialization import load_pem_public_key 14 | from cryptography.hazmat.backends import default_backend 15 | from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15 16 | 17 | 18 | try: 19 | from urllib import urlopen 20 | except: 21 | from urllib.request import urlopen 22 | 23 | 24 | GITHUB_REPO = 'wliu_intern/stf_selector' 25 | TRAVIS_CONFIG_FILE = os.path.join( 26 | os.path.dirname(os.path.abspath(__file__)), '.travis.yml') 27 | 28 | 29 | def load_key(pubkey): 30 | """Load public RSA key, with work-around for keys using 31 | incorrect header/footer format. 32 | 33 | Read more about RSA encryption with cryptography: 34 | https://cryptography.io/latest/hazmat/primitives/asymmetric/rsa/ 35 | """ 36 | try: 37 | return load_pem_public_key(pubkey.encode(), default_backend()) 38 | except ValueError: 39 | # workaround for https://github.com/travis-ci/travis-api/issues/196 40 | pubkey = pubkey.replace('BEGIN RSA', 'BEGIN').replace('END RSA', 'END') 41 | return load_pem_public_key(pubkey.encode(), default_backend()) 42 | 43 | 44 | def encrypt(pubkey, password): 45 | """Encrypt password using given RSA public key and encode it with base64. 46 | 47 | The encrypted password can only be decrypted by someone with the 48 | private key (in this case, only Travis). 49 | """ 50 | key = load_key(pubkey) 51 | encrypted_password = key.encrypt(password, PKCS1v15()) 52 | return base64.b64encode(encrypted_password) 53 | 54 | 55 | def fetch_public_key(repo): 56 | """Download RSA public key Travis will use for this repo. 57 | 58 | Travis API docs: http://docs.travis-ci.com/api/#repository-keys 59 | """ 60 | keyurl = 'https://api.travis-ci.org/repos/{0}/key'.format(repo) 61 | data = json.loads(urlopen(keyurl).read().decode()) 62 | if 'key' not in data: 63 | errmsg = "Could not find public key for repo: {}.\n".format(repo) 64 | errmsg += "Have you already added your GitHub repo to Travis?" 65 | raise ValueError(errmsg) 66 | return data['key'] 67 | 68 | 69 | def prepend_line(filepath, line): 70 | """Rewrite a file adding a line to its beginning. 71 | """ 72 | with open(filepath) as f: 73 | lines = f.readlines() 74 | 75 | lines.insert(0, line) 76 | 77 | with open(filepath, 'w') as f: 78 | f.writelines(lines) 79 | 80 | 81 | def load_yaml_config(filepath): 82 | with open(filepath) as f: 83 | return yaml.load(f) 84 | 85 | 86 | def save_yaml_config(filepath, config): 87 | with open(filepath, 'w') as f: 88 | yaml.dump(config, f, default_flow_style=False) 89 | 90 | 91 | def update_travis_deploy_password(encrypted_password): 92 | """Update the deploy section of the .travis.yml file 93 | to use the given encrypted password. 94 | """ 95 | config = load_yaml_config(TRAVIS_CONFIG_FILE) 96 | 97 | config['deploy']['password'] = dict(secure=encrypted_password) 98 | 99 | save_yaml_config(TRAVIS_CONFIG_FILE, config) 100 | 101 | line = ('# This file was autogenerated and will overwrite' 102 | ' each time you run travis_pypi_setup.py\n') 103 | prepend_line(TRAVIS_CONFIG_FILE, line) 104 | 105 | 106 | def main(args): 107 | public_key = fetch_public_key(args.repo) 108 | password = args.password or getpass('PyPI password: ') 109 | update_travis_deploy_password(encrypt(public_key, password.encode())) 110 | print("Wrote encrypted password to .travis.yml -- you're ready to deploy") 111 | 112 | 113 | if '__main__' == __name__: 114 | import argparse 115 | parser = argparse.ArgumentParser(description=__doc__) 116 | parser.add_argument('--repo', default=GITHUB_REPO, 117 | help='GitHub repo (default: %s)' % GITHUB_REPO) 118 | parser.add_argument('--password', 119 | help='PyPI password (will prompt if not provided)') 120 | 121 | args = parser.parse_args() 122 | main(args) 123 | --------------------------------------------------------------------------------