├── .gitignore ├── .pre-commit-config.yaml ├── CHANGES.rst ├── LICENSE ├── MANIFEST.in ├── Makefile ├── Pipfile ├── Pipfile.lock ├── README.rst ├── patrol ├── __init__.py ├── __main__.py ├── cli.py ├── config.py ├── patrol.py ├── sentry_api_client │ ├── __init__.py │ ├── api.py │ └── resource.py └── utils.py ├── pyproject.toml ├── pytest.ini ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── conftest.py └── test_patrol.py /.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 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # Environments 83 | .env 84 | .venv 85 | env/ 86 | venv/ 87 | ENV/ 88 | env.bak/ 89 | venv.bak/ 90 | 91 | # Spyder project settings 92 | .spyderproject 93 | .spyproject 94 | 95 | # Rope project settings 96 | .ropeproject 97 | 98 | # mkdocs documentation 99 | /site 100 | 101 | # mypy 102 | .mypy_cache/ 103 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: git@github.com:pre-commit/pre-commit-hooks 3 | rev: v2.1.0 4 | hooks: 5 | - id: debug-statements 6 | - id: trailing-whitespace 7 | args: [--markdown-linebreak-ext=md] 8 | - id: check-merge-conflict 9 | - id: check-executables-have-shebangs 10 | - id: check-ast 11 | - id: check-byte-order-marker 12 | - id: check-json 13 | - id: check-symlinks 14 | - id: check-vcs-permalinks 15 | - id: check-xml 16 | - id: check-yaml 17 | - id: detect-aws-credentials 18 | args: ['--allow-missing-credentials'] 19 | - id: detect-private-key 20 | - id: forbid-new-submodules 21 | - id: no-commit-to-branch 22 | - id: flake8 23 | args: ['--exclude=docs/*,*migrations*', '--ignore=E501'] 24 | 25 | - repo: local 26 | hooks: 27 | - id: check-datetime-now 28 | name: check_datetime_now 29 | description: Prefer datetime.utcnow() 30 | language: pygrep 31 | entry: 'datetime\.now\(\)' 32 | types: [python] 33 | 34 | - repo: https://github.com/pre-commit/mirrors-isort 35 | rev: v4.3.16 36 | hooks: 37 | - id: isort 38 | 39 | - repo: https://github.com/pre-commit/pygrep-hooks 40 | rev: v1.3.0 41 | hooks: 42 | - id: python-check-mock-methods 43 | 44 | - repo: https://github.com/python/black 45 | rev: stable 46 | hooks: 47 | - id: black 48 | 49 | - repo: git@github.com:olist/hulks.git 50 | rev: 0.4.0 51 | hooks: 52 | - id: check-filename 53 | - id: check-invalid-domains 54 | - id: check-logger 55 | - id: check-mutable-defaults 56 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ~~~~~~~~~ 3 | 4 | 1.0.0 5 | ----- 6 | 7 | * Update simple-rest-client version 8 | * Drop python 3.5 support 9 | 10 | 0.2.0 11 | ----- 12 | 13 | * Add command to list projects 14 | 15 | 0.1.1 16 | ----- 17 | 18 | * Fix fetch event command 19 | * Use json output for fetch commands 20 | 21 | 0.1.0 22 | ----- 23 | 24 | * Add command to fetch issue 25 | * Add command to fetch event 26 | * Add command to update issue status 27 | 28 | 0.0.4 29 | ----- 30 | 31 | * Use generator on patrol 32 | * Fix pagination 33 | 34 | 0.0.3 35 | ----- 36 | 37 | * Add dependencies to setup.py install_requires 38 | * Fix typo on __init__.py file 39 | 40 | 0.0.2 41 | ----- 42 | 43 | * Improve setup.py (based on https://github.com/kennethreitz/setup.py) 44 | * Add Changelog for versioning 45 | 46 | 47 | 0.0.1 48 | ----- 49 | 50 | * First version. 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Danilo Shiga 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst CHANGES.rst LICENSE 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean-pyc clean-build clean 2 | 3 | help: 4 | @echo "clean - remove all build, test, coverage and Python artifacts" 5 | @echo "clean-build - remove build artifacts" 6 | @echo "clean-pyc - remove Python file artifacts" 7 | @echo "lint - check style with flake8" 8 | @echo "release - package and upload a release" 9 | @echo "dist - package" 10 | 11 | clean: clean-build clean-pyc 12 | 13 | clean-build: 14 | rm -fr build/ 15 | rm -fr dist/ 16 | rm -fr *.egg-info 17 | 18 | clean-pyc: 19 | find . -name '*.pyc' -exec rm -f {} + 20 | find . -name '*.pyo' -exec rm -f {} + 21 | find . -name '*~' -exec rm -f {} + 22 | find . -name '__pycache__' -exec rm -fr {} + 23 | 24 | lint: 25 | SKIP=no-commit-to-branch pre-commit run -a -v 26 | flake8 patrol tests 27 | 28 | test: 29 | python setup.py test 30 | 31 | release: clean 32 | python setup.py upload 33 | 34 | dist: clean 35 | python setup.py sdist 36 | python setup.py bdist_wheel 37 | ls -l dist 38 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.python.org/simple" 3 | verify_ssl = true 4 | 5 | [dev-packages] 6 | ipdb = "*" 7 | pre-commit = "*" 8 | pytest = "*" 9 | pytest-cov = "*" 10 | 11 | [packages] 12 | click = "*" 13 | prettyconf = "*" 14 | simple-rest-client = "*" 15 | 16 | [requires] 17 | python_full_version="3.7.4" 18 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "76939b7d41da5d12430134003b3eebc5ce7f10ffd46c35043b6fae2191d02ed3" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_full_version": "3.7.4" 9 | }, 10 | "sources": [ 11 | { 12 | "url": "https://pypi.python.org/simple", 13 | "verify_ssl": true 14 | } 15 | ] 16 | }, 17 | "default": { 18 | "certifi": { 19 | "hashes": [ 20 | "sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50", 21 | "sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef" 22 | ], 23 | "version": "==2019.9.11" 24 | }, 25 | "chardet": { 26 | "hashes": [ 27 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 28 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 29 | ], 30 | "version": "==3.0.4" 31 | }, 32 | "click": { 33 | "hashes": [ 34 | "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", 35 | "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" 36 | ], 37 | "version": "==7.0" 38 | }, 39 | "h11": { 40 | "hashes": [ 41 | "sha256:acca6a44cb52a32ab442b1779adf0875c443c689e9e028f8d831a3769f9c5208", 42 | "sha256:f2b1ca39bfed357d1f19ac732913d5f9faa54a5062eca7d2ec3a916cfb7ae4c7" 43 | ], 44 | "version": "==0.8.1" 45 | }, 46 | "h2": { 47 | "hashes": [ 48 | "sha256:ac377fcf586314ef3177bfd90c12c7826ab0840edeb03f0f24f511858326049e", 49 | "sha256:b8a32bd282594424c0ac55845377eea13fa54fe4a8db012f3a198ed923dc3ab4" 50 | ], 51 | "version": "==3.1.1" 52 | }, 53 | "hpack": { 54 | "hashes": [ 55 | "sha256:0edd79eda27a53ba5be2dfabf3b15780928a0dff6eb0c60a3d6767720e970c89", 56 | "sha256:8eec9c1f4bfae3408a3f30500261f7e6a65912dc138526ea054f9ad98892e9d2" 57 | ], 58 | "version": "==3.0.0" 59 | }, 60 | "hstspreload": { 61 | "hashes": [ 62 | "sha256:83976f655cf2dc77b7911367307bd202a75fff201fd0a764314098a881a469d9" 63 | ], 64 | "version": "==2019.9.30" 65 | }, 66 | "httpx": { 67 | "hashes": [ 68 | "sha256:c1fd72b4bd73bf7c4b04e6a91b69c28dcaa1a21f994043722d7b7141b9162154" 69 | ], 70 | "version": "==0.7.4" 71 | }, 72 | "hyperframe": { 73 | "hashes": [ 74 | "sha256:5187962cb16dcc078f23cb5a4b110098d546c3f41ff2d4038a9896893bbd0b40", 75 | "sha256:a9f5c17f2cc3c719b917c4f33ed1c61bd1f8dfac4b1bd23b7c80b3400971b41f" 76 | ], 77 | "version": "==5.2.0" 78 | }, 79 | "idna": { 80 | "hashes": [ 81 | "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", 82 | "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" 83 | ], 84 | "version": "==2.8" 85 | }, 86 | "prettyconf": { 87 | "hashes": [ 88 | "sha256:c6b4f18fbdfd59fc431ffe3e94f4d793afdf1261fc254e103cf4e105ca94e1ed" 89 | ], 90 | "version": "==2.0.1" 91 | }, 92 | "python-status": { 93 | "hashes": [ 94 | "sha256:4e9c824754e3669a4a6565fd4c3d6c0dd4130e779c541ad168e71110ebce3e53" 95 | ], 96 | "version": "==1.0.1" 97 | }, 98 | "rfc3986": { 99 | "hashes": [ 100 | "sha256:0344d0bd428126ce554e7ca2b61787b6a28d2bbd19fc70ed2dd85efe31176405", 101 | "sha256:df4eba676077cefb86450c8f60121b9ae04b94f65f85b69f3f731af0516b7b18" 102 | ], 103 | "version": "==1.3.2" 104 | }, 105 | "simple-rest-client": { 106 | "hashes": [ 107 | "sha256:ae032cd35241046ece23380042a56a13f20d387dd9129c65f48f0fc446880bd4", 108 | "sha256:c7796524c3942ef7b09d53511d19b433e9be002d800953911e4cb57ef61cbe19" 109 | ], 110 | "version": "==1.0.0" 111 | } 112 | }, 113 | "develop": { 114 | "aspy.yaml": { 115 | "hashes": [ 116 | "sha256:463372c043f70160a9ec950c3f1e4c3a82db5fca01d334b6bc89c7164d744bdc", 117 | "sha256:e7c742382eff2caed61f87a39d13f99109088e5e93f04d76eb8d4b28aa143f45" 118 | ], 119 | "version": "==1.3.0" 120 | }, 121 | "atomicwrites": { 122 | "hashes": [ 123 | "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", 124 | "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6" 125 | ], 126 | "version": "==1.3.0" 127 | }, 128 | "attrs": { 129 | "hashes": [ 130 | "sha256:ec20e7a4825331c1b5ebf261d111e16fa9612c1f7a5e1f884f12bd53a664dfd2", 131 | "sha256:f913492e1663d3c36f502e5e9ba6cd13cf19d7fab50aa13239e420fef95e1396" 132 | ], 133 | "version": "==19.2.0" 134 | }, 135 | "backcall": { 136 | "hashes": [ 137 | "sha256:38ecd85be2c1e78f77fd91700c76e14667dc21e2713b63876c0eb901196e01e4", 138 | "sha256:bbbf4b1e5cd2bdb08f915895b51081c041bac22394fdfcfdfbe9f14b77c08bf2" 139 | ], 140 | "version": "==0.1.0" 141 | }, 142 | "cfgv": { 143 | "hashes": [ 144 | "sha256:edb387943b665bf9c434f717bf630fa78aecd53d5900d2e05da6ad6048553144", 145 | "sha256:fbd93c9ab0a523bf7daec408f3be2ed99a980e20b2d19b50fc184ca6b820d289" 146 | ], 147 | "version": "==2.0.1" 148 | }, 149 | "coverage": { 150 | "hashes": [ 151 | "sha256:08907593569fe59baca0bf152c43f3863201efb6113ecb38ce7e97ce339805a6", 152 | "sha256:0be0f1ed45fc0c185cfd4ecc19a1d6532d72f86a2bac9de7e24541febad72650", 153 | "sha256:141f08ed3c4b1847015e2cd62ec06d35e67a3ac185c26f7635f4406b90afa9c5", 154 | "sha256:19e4df788a0581238e9390c85a7a09af39c7b539b29f25c89209e6c3e371270d", 155 | "sha256:23cc09ed395b03424d1ae30dcc292615c1372bfba7141eb85e11e50efaa6b351", 156 | "sha256:245388cda02af78276b479f299bbf3783ef0a6a6273037d7c60dc73b8d8d7755", 157 | "sha256:331cb5115673a20fb131dadd22f5bcaf7677ef758741312bee4937d71a14b2ef", 158 | "sha256:386e2e4090f0bc5df274e720105c342263423e77ee8826002dcffe0c9533dbca", 159 | "sha256:3a794ce50daee01c74a494919d5ebdc23d58873747fa0e288318728533a3e1ca", 160 | "sha256:60851187677b24c6085248f0a0b9b98d49cba7ecc7ec60ba6b9d2e5574ac1ee9", 161 | "sha256:63a9a5fc43b58735f65ed63d2cf43508f462dc49857da70b8980ad78d41d52fc", 162 | "sha256:6b62544bb68106e3f00b21c8930e83e584fdca005d4fffd29bb39fb3ffa03cb5", 163 | "sha256:6ba744056423ef8d450cf627289166da65903885272055fb4b5e113137cfa14f", 164 | "sha256:7494b0b0274c5072bddbfd5b4a6c6f18fbbe1ab1d22a41e99cd2d00c8f96ecfe", 165 | "sha256:826f32b9547c8091679ff292a82aca9c7b9650f9fda3e2ca6bf2ac905b7ce888", 166 | "sha256:93715dffbcd0678057f947f496484e906bf9509f5c1c38fc9ba3922893cda5f5", 167 | "sha256:9a334d6c83dfeadae576b4d633a71620d40d1c379129d587faa42ee3e2a85cce", 168 | "sha256:af7ed8a8aa6957aac47b4268631fa1df984643f07ef00acd374e456364b373f5", 169 | "sha256:bf0a7aed7f5521c7ca67febd57db473af4762b9622254291fbcbb8cd0ba5e33e", 170 | "sha256:bf1ef9eb901113a9805287e090452c05547578eaab1b62e4ad456fcc049a9b7e", 171 | "sha256:c0afd27bc0e307a1ffc04ca5ec010a290e49e3afbe841c5cafc5c5a80ecd81c9", 172 | "sha256:dd579709a87092c6dbee09d1b7cfa81831040705ffa12a1b248935274aee0437", 173 | "sha256:df6712284b2e44a065097846488f66840445eb987eb81b3cc6e4149e7b6982e1", 174 | "sha256:e07d9f1a23e9e93ab5c62902833bf3e4b1f65502927379148b6622686223125c", 175 | "sha256:e2ede7c1d45e65e209d6093b762e98e8318ddeff95317d07a27a2140b80cfd24", 176 | "sha256:e4ef9c164eb55123c62411f5936b5c2e521b12356037b6e1c2617cef45523d47", 177 | "sha256:eca2b7343524e7ba246cab8ff00cab47a2d6d54ada3b02772e908a45675722e2", 178 | "sha256:eee64c616adeff7db37cc37da4180a3a5b6177f5c46b187894e633f088fb5b28", 179 | "sha256:ef824cad1f980d27f26166f86856efe11eff9912c4fed97d3804820d43fa550c", 180 | "sha256:efc89291bd5a08855829a3c522df16d856455297cf35ae827a37edac45f466a7", 181 | "sha256:fa964bae817babece5aa2e8c1af841bebb6d0b9add8e637548809d040443fee0", 182 | "sha256:ff37757e068ae606659c28c3bd0d923f9d29a85de79bf25b2b34b148473b5025" 183 | ], 184 | "version": "==4.5.4" 185 | }, 186 | "decorator": { 187 | "hashes": [ 188 | "sha256:86156361c50488b84a3f148056ea716ca587df2f0de1d34750d35c21312725de", 189 | "sha256:f069f3a01830ca754ba5258fde2278454a0b5b79e0d7f5c13b3b97e57d4acff6" 190 | ], 191 | "version": "==4.4.0" 192 | }, 193 | "identify": { 194 | "hashes": [ 195 | "sha256:4f1fe9a59df4e80fcb0213086fcf502bc1765a01ea4fe8be48da3b65afd2a017", 196 | "sha256:d8919589bd2a5f99c66302fec0ef9027b12ae150b0b0213999ad3f695fc7296e" 197 | ], 198 | "version": "==1.4.7" 199 | }, 200 | "importlib-metadata": { 201 | "hashes": [ 202 | "sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26", 203 | "sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af" 204 | ], 205 | "markers": "python_version < '3.8'", 206 | "version": "==0.23" 207 | }, 208 | "ipdb": { 209 | "hashes": [ 210 | "sha256:473fdd798a099765f093231a8b1fabfa95b0b682fce12de0c74b61a4b4d8ee57" 211 | ], 212 | "version": "==0.12.2" 213 | }, 214 | "ipython": { 215 | "hashes": [ 216 | "sha256:c4ab005921641e40a68e405e286e7a1fcc464497e14d81b6914b4fd95e5dee9b", 217 | "sha256:dd76831f065f17bddd7eaa5c781f5ea32de5ef217592cf019e34043b56895aa1" 218 | ], 219 | "version": "==7.8.0" 220 | }, 221 | "ipython-genutils": { 222 | "hashes": [ 223 | "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8", 224 | "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8" 225 | ], 226 | "version": "==0.2.0" 227 | }, 228 | "jedi": { 229 | "hashes": [ 230 | "sha256:786b6c3d80e2f06fd77162a07fed81b8baa22dde5d62896a790a331d6ac21a27", 231 | "sha256:ba859c74fa3c966a22f2aeebe1b74ee27e2a462f56d3f5f7ca4a59af61bfe42e" 232 | ], 233 | "version": "==0.15.1" 234 | }, 235 | "more-itertools": { 236 | "hashes": [ 237 | "sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832", 238 | "sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4" 239 | ], 240 | "version": "==7.2.0" 241 | }, 242 | "nodeenv": { 243 | "hashes": [ 244 | "sha256:ad8259494cf1c9034539f6cced78a1da4840a4b157e23640bc4a0c0546b0cb7a" 245 | ], 246 | "version": "==1.3.3" 247 | }, 248 | "packaging": { 249 | "hashes": [ 250 | "sha256:28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47", 251 | "sha256:d9551545c6d761f3def1677baf08ab2a3ca17c56879e70fecba2fc4dde4ed108" 252 | ], 253 | "version": "==19.2" 254 | }, 255 | "parso": { 256 | "hashes": [ 257 | "sha256:63854233e1fadb5da97f2744b6b24346d2750b85965e7e399bec1620232797dc", 258 | "sha256:666b0ee4a7a1220f65d367617f2cd3ffddff3e205f3f16a0284df30e774c2a9c" 259 | ], 260 | "version": "==0.5.1" 261 | }, 262 | "pexpect": { 263 | "hashes": [ 264 | "sha256:2094eefdfcf37a1fdbfb9aa090862c1a4878e5c7e0e7e7088bdb511c558e5cd1", 265 | "sha256:9e2c1fd0e6ee3a49b28f95d4b33bc389c89b20af6a1255906e90ff1262ce62eb" 266 | ], 267 | "markers": "sys_platform != 'win32'", 268 | "version": "==4.7.0" 269 | }, 270 | "pickleshare": { 271 | "hashes": [ 272 | "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca", 273 | "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56" 274 | ], 275 | "version": "==0.7.5" 276 | }, 277 | "pluggy": { 278 | "hashes": [ 279 | "sha256:0db4b7601aae1d35b4a033282da476845aa19185c1e6964b25cf324b5e4ec3e6", 280 | "sha256:fa5fa1622fa6dd5c030e9cad086fa19ef6a0cf6d7a2d12318e10cb49d6d68f34" 281 | ], 282 | "version": "==0.13.0" 283 | }, 284 | "pre-commit": { 285 | "hashes": [ 286 | "sha256:1d3c0587bda7c4e537a46c27f2c84aa006acc18facf9970bf947df596ce91f3f", 287 | "sha256:fa78ff96e8e9ac94c748388597693f18b041a181c94a4f039ad20f45287ba44a" 288 | ], 289 | "version": "==1.18.3" 290 | }, 291 | "prompt-toolkit": { 292 | "hashes": [ 293 | "sha256:11adf3389a996a6d45cc277580d0d53e8a5afd281d0c9ec71b28e6f121463780", 294 | "sha256:2519ad1d8038fd5fc8e770362237ad0364d16a7650fb5724af6997ed5515e3c1", 295 | "sha256:977c6583ae813a37dc1c2e1b715892461fcbdaa57f6fc62f33a528c4886c8f55" 296 | ], 297 | "version": "==2.0.9" 298 | }, 299 | "ptyprocess": { 300 | "hashes": [ 301 | "sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0", 302 | "sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f" 303 | ], 304 | "version": "==0.6.0" 305 | }, 306 | "py": { 307 | "hashes": [ 308 | "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", 309 | "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53" 310 | ], 311 | "version": "==1.8.0" 312 | }, 313 | "pygments": { 314 | "hashes": [ 315 | "sha256:71e430bc85c88a430f000ac1d9b331d2407f681d6f6aec95e8bcfbc3df5b0127", 316 | "sha256:881c4c157e45f30af185c1ffe8d549d48ac9127433f2c380c24b84572ad66297" 317 | ], 318 | "version": "==2.4.2" 319 | }, 320 | "pyparsing": { 321 | "hashes": [ 322 | "sha256:6f98a7b9397e206d78cc01df10131398f1c8b8510a2f4d97d9abd82e1aacdd80", 323 | "sha256:d9338df12903bbf5d65a0e4e87c2161968b10d2e489652bb47001d82a9b028b4" 324 | ], 325 | "version": "==2.4.2" 326 | }, 327 | "pytest": { 328 | "hashes": [ 329 | "sha256:13c1c9b22127a77fc684eee24791efafcef343335d855e3573791c68588fe1a5", 330 | "sha256:d8ba7be9466f55ef96ba203fc0f90d0cf212f2f927e69186e1353e30bc7f62e5" 331 | ], 332 | "version": "==5.2.0" 333 | }, 334 | "pytest-cov": { 335 | "hashes": [ 336 | "sha256:2b097cde81a302e1047331b48cadacf23577e431b61e9c6f49a1170bbe3d3da6", 337 | "sha256:e00ea4fdde970725482f1f35630d12f074e121a23801aabf2ae154ec6bdd343a" 338 | ], 339 | "version": "==2.7.1" 340 | }, 341 | "pyyaml": { 342 | "hashes": [ 343 | "sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9", 344 | "sha256:01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4", 345 | "sha256:5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8", 346 | "sha256:5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696", 347 | "sha256:7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34", 348 | "sha256:7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9", 349 | "sha256:87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73", 350 | "sha256:9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299", 351 | "sha256:a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b", 352 | "sha256:b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae", 353 | "sha256:b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681", 354 | "sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41", 355 | "sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8" 356 | ], 357 | "version": "==5.1.2" 358 | }, 359 | "six": { 360 | "hashes": [ 361 | "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", 362 | "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" 363 | ], 364 | "version": "==1.12.0" 365 | }, 366 | "toml": { 367 | "hashes": [ 368 | "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", 369 | "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" 370 | ], 371 | "version": "==0.10.0" 372 | }, 373 | "traitlets": { 374 | "hashes": [ 375 | "sha256:9c4bd2d267b7153df9152698efb1050a5d84982d3384a37b2c1f7723ba3e7835", 376 | "sha256:c6cb5e6f57c5a9bdaa40fa71ce7b4af30298fbab9ece9815b5d995ab6217c7d9" 377 | ], 378 | "version": "==4.3.2" 379 | }, 380 | "virtualenv": { 381 | "hashes": [ 382 | "sha256:680af46846662bb38c5504b78bad9ed9e4f3ba2d54f54ba42494fdf94337fe30", 383 | "sha256:f78d81b62d3147396ac33fc9d77579ddc42cc2a98dd9ea38886f616b33bc7fb2" 384 | ], 385 | "version": "==16.7.5" 386 | }, 387 | "wcwidth": { 388 | "hashes": [ 389 | "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", 390 | "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c" 391 | ], 392 | "version": "==0.1.7" 393 | }, 394 | "zipp": { 395 | "hashes": [ 396 | "sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e", 397 | "sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335" 398 | ], 399 | "version": "==0.6.0" 400 | } 401 | } 402 | } 403 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Sentry Patrol 2 | ============= 3 | 4 | Sentry Patrol offers a CLI (Command Line Interface) to the Sentry API. 5 | 6 | The project is still in alpha state and in active development 7 | 8 | .. contents:: **Table of Contents** 9 | 10 | Setup 11 | ~~~~~ 12 | 13 | .. code:: bash 14 | 15 | pipenv install --dev 16 | python setup.py develop 17 | export SENTRY_API_TOKEN= 18 | 19 | 20 | How to get your sentry API Token 21 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 22 | 23 | You have to access the Sentry API Page (https://sentry.io/api/). Then you 24 | must log in and get you API Auth Token. 25 | 26 | 27 | How to use 28 | ~~~~~~~~~~ 29 | 30 | For a list of commands and options, check the help output with: 31 | 32 | .. code:: bash 33 | 34 | patrol --help 35 | -------------------------------------------------------------------------------- /patrol/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daneoshiga/sentry-patrol/746932c5469316e402fb69d9c10a03051130f875/patrol/__init__.py -------------------------------------------------------------------------------- /patrol/__main__.py: -------------------------------------------------------------------------------- 1 | from .cli import cli 2 | 3 | if __name__ == "__main__": 4 | cli() 5 | -------------------------------------------------------------------------------- /patrol/cli.py: -------------------------------------------------------------------------------- 1 | import click 2 | from patrol.config import ISSUE_STATUSES, SENTRY_API_TOKEN 3 | from patrol.patrol import Patrol 4 | from patrol.utils import print_json 5 | 6 | patrol = Patrol(SENTRY_API_TOKEN) 7 | 8 | 9 | @click.group() 10 | def cli(): 11 | pass 12 | 13 | 14 | @cli.command(help="Lists project events") 15 | @click.argument("organization") 16 | @click.argument("project_name") 17 | def events(organization, project_name): 18 | click.secho("events for {}".format(project_name)) 19 | for event in patrol.events(organization, project_name): 20 | click.echo("{eventID}: {message}".format(**event)) 21 | 22 | 23 | @cli.command(help="Lists project events") 24 | @click.argument("organization") 25 | @click.argument("project_name") 26 | @click.argument("event_id") 27 | def event(organization, project_name, event_id): 28 | click.secho("event {} for {}".format(event_id, project_name)) 29 | event = patrol.event(organization, project_name, event_id) 30 | print_json(event) 31 | 32 | 33 | @cli.command(help="Lists project issues") 34 | @click.argument("organization") 35 | @click.argument("project_name") 36 | def issues(organization, project_name): 37 | click.secho("issues for {}".format(project_name)) 38 | for issue in patrol.issues(organization, project_name): 39 | click.echo("{id}: {title}".format(**issue)) 40 | 41 | 42 | @cli.command(help="Retrive issue") 43 | @click.argument("issue_id") 44 | def issue(issue_id): 45 | issue = patrol.issue(issue_id) 46 | print_json(issue) 47 | 48 | 49 | @cli.command(help="Update issue") 50 | @click.argument("issue_id") 51 | @click.option("--status", type=click.Choice(ISSUE_STATUSES)) 52 | def update_issue_status(issue_id, status): 53 | data = {"status": status} 54 | issue = patrol.update_issue(issue_id, data) 55 | click.echo("{id}: {title} - {status}".format(**issue)) 56 | 57 | 58 | @cli.command(help="Lists organization's project") 59 | @click.argument("organization") 60 | def projects(organization): 61 | for project in patrol.projects(organization): 62 | click.echo("{name}".format(**project)) 63 | -------------------------------------------------------------------------------- /patrol/config.py: -------------------------------------------------------------------------------- 1 | import click 2 | from prettyconf import config 3 | 4 | APP_NAME = "patrol" 5 | 6 | config.starting_path = click.get_app_dir(APP_NAME) 7 | 8 | SENTRY_API_TOKEN = config("SENTRY_API_TOKEN") 9 | 10 | ISSUE_STATUSES = ("resolved", "unresolved", "resolvedInNextRelease", "ignored") 11 | -------------------------------------------------------------------------------- /patrol/patrol.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from .sentry_api_client.api import get_api_instance 4 | 5 | 6 | class Patrol: 7 | def __init__(self, sentry_api_token, timeout=None): 8 | self.headers = {"Authorization": "Bearer {}".format(sentry_api_token)} 9 | self.timeout = timeout 10 | self.api = get_api_instance(sentry_api_token, timeout) 11 | 12 | def _fetch_resources(self, endpoint, organization, project): 13 | endpoint = getattr(self.api, endpoint) 14 | method = getattr(endpoint, "list") 15 | 16 | resources = method(organization, project) 17 | yield from resources.body 18 | 19 | next_link = resources.client_response.links["next"] 20 | while next_link["results"] == "true": 21 | response = requests.get( 22 | next_link["url"], timeout=self.timeout, headers=self.headers 23 | ) 24 | yield from response.json() 25 | next_link = response.links["next"] 26 | 27 | def events(self, organization, project): 28 | return self._fetch_resources("project_events", organization, project) 29 | 30 | def event(self, organization, project, event_id): 31 | return self.api.project_events.fetch(organization, project, event_id).body 32 | 33 | def issues(self, organization, project): 34 | return self._fetch_resources("project_issues", organization, project) 35 | 36 | def issue(self, issue_id): 37 | return self.api.issues.fetch(issue_id).body 38 | 39 | def update_issue(self, issue_id, data): 40 | return self.api.issues.update(issue_id, body=data).body 41 | 42 | def projects(self, organization): 43 | return self._fetch_resources("projects", organization, None) 44 | -------------------------------------------------------------------------------- /patrol/sentry_api_client/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daneoshiga/sentry-patrol/746932c5469316e402fb69d9c10a03051130f875/patrol/sentry_api_client/__init__.py -------------------------------------------------------------------------------- /patrol/sentry_api_client/api.py: -------------------------------------------------------------------------------- 1 | from simple_rest_client.api import API 2 | 3 | from ..sentry_api_client import resource 4 | 5 | 6 | def get_api_instance(token="", timeout=None): 7 | headers = { 8 | "Authorization": "Bearer {}".format(token), 9 | "Content-Type": "application/json", 10 | } 11 | 12 | api_root_url = "https://sentry.io/api/0/" 13 | 14 | api = API( 15 | api_root_url=api_root_url, 16 | headers=headers, 17 | json_encode_body=True, 18 | timeout=timeout, 19 | ) 20 | 21 | api.add_resource(resource_name="issues", resource_class=resource.Issues) 22 | api.add_resource( 23 | resource_name="project_events", resource_class=resource.ProjectEvents 24 | ) 25 | api.add_resource( 26 | resource_name="project_issues", resource_class=resource.ProjectIssues 27 | ) 28 | api.add_resource(resource_name="projects", resource_class=resource.Projects) 29 | 30 | return api 31 | -------------------------------------------------------------------------------- /patrol/sentry_api_client/resource.py: -------------------------------------------------------------------------------- 1 | from simple_rest_client.resource import Resource 2 | 3 | 4 | class ProjectEvents(Resource): 5 | actions = { 6 | "fetch": {"method": "GET", "url": "projects/{}/{}/events/{}/"}, 7 | "list": {"method": "GET", "url": "projects/{}/{}/events/"}, 8 | } 9 | 10 | 11 | class ProjectIssues(Resource): 12 | actions = {"list": {"method": "GET", "url": "projects/{}/{}/issues/"}} 13 | 14 | 15 | class Issues(Resource): 16 | actions = { 17 | "fetch": {"method": "GET", "url": "issues/{}/"}, 18 | "update": {"method": "PUT", "url": "issues/{}/"}, 19 | } 20 | 21 | 22 | class Projects(Resource): 23 | actions = {"list": {"method": "GET", "url": "organizations/{}/projects/"}} 24 | -------------------------------------------------------------------------------- /patrol/utils.py: -------------------------------------------------------------------------------- 1 | import decimal 2 | import json 3 | 4 | import click 5 | 6 | 7 | def json_encode_decimal(obj): 8 | if isinstance(obj, decimal.Decimal): 9 | return str(obj) 10 | raise TypeError(repr(obj) + " is not JSON serializable") 11 | 12 | 13 | def print_json(data): 14 | return click.echo(json.dumps(data, default=json_encode_decimal, indent=2)) 15 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | target-version = ['py37'] 3 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | testpaths = tests 3 | addopts = -vv --cov=patrol --cov-report=term-missing 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.rst 3 | 4 | [wheel] 5 | universal = 1 6 | 7 | [aliases] 8 | test=pytest 9 | 10 | [tool:pytest] 11 | addopts = --verbose 12 | 13 | [isort] 14 | multi_line_output=3 15 | include_trailing_comma=True 16 | force_grid_wrap=0 17 | use_parentheses=True 18 | line_length=88 19 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python 2 | 3 | 4 | import io 5 | import os 6 | import re 7 | import sys 8 | from shutil import rmtree 9 | 10 | from setuptools import Command, find_packages, setup 11 | 12 | NAME = "sentry-patrol" 13 | DESCRIPTION = "Command Line program to interact with sentry API" 14 | URL = "https://github.com/daneoshiga/sentry-patrol" 15 | EMAIL = "daniloshiga@gmail.com" 16 | AUTHOR = "Danilo Shiga" 17 | 18 | here = os.path.abspath(os.path.dirname(__file__)) 19 | 20 | with io.open(os.path.join(here, "README.rst"), encoding="utf-8") as f: 21 | long_description = "\n" + f.read() 22 | 23 | 24 | class VersionCommand(Command): 25 | description = "Print current version." 26 | user_options = [] 27 | 28 | _version = "0.0.0" 29 | 30 | @staticmethod 31 | def read_version(): 32 | with open(os.path.join(here, "CHANGES.rst")) as changes: 33 | for line in changes: 34 | VersionCommand._version = line.strip() 35 | if re.search(r"^[0-9]+\.[0-9]+(\.[0-9]+)?$", VersionCommand._version): 36 | break 37 | 38 | return VersionCommand._version 39 | 40 | def initialize_options(self): 41 | pass 42 | 43 | def finalize_options(self): 44 | pass 45 | 46 | def run(self): 47 | print(self.read_version()) 48 | 49 | 50 | # Note: To use the 'upload' functionality of this file, you must: 51 | # $ pip install twine 52 | class UploadCommand(Command): 53 | description = "Build and publish the package." 54 | user_options = [] 55 | 56 | @staticmethod 57 | def status(s): 58 | """Prints things in bold.""" 59 | print("\033[1m{0}\033[0m".format(s)) 60 | 61 | def initialize_options(self): 62 | pass 63 | 64 | def finalize_options(self): 65 | pass 66 | 67 | def run(self): 68 | try: 69 | self.status("Removing previous builds…") 70 | rmtree(os.path.join(here, "dist")) 71 | except OSError: 72 | pass 73 | 74 | self.status("Building Source and Wheel (universal) distribution…") 75 | os.system("{0} setup.py sdist bdist_wheel --universal".format(sys.executable)) 76 | 77 | self.status("Uploading the package to PyPi via Twine…") 78 | os.system("twine upload dist/*") 79 | 80 | sys.exit() 81 | 82 | 83 | setup( 84 | name=NAME, 85 | version=VersionCommand.read_version(), 86 | description=DESCRIPTION, 87 | author=AUTHOR, 88 | author_email=EMAIL, 89 | url=URL, 90 | long_description=long_description, 91 | packages=find_packages(exclude=["docs", "tests", "tests.*", "Pipfile*"]), 92 | entry_points={"console_scripts": ["patrol=patrol.cli:cli"]}, 93 | zip_safe=False, 94 | include_package_data=True, 95 | license="MIT", 96 | keywords="sentry cli patrol", 97 | classifiers=[ 98 | "Development Status :: 4 - Beta", 99 | "Environment :: Console", 100 | "Intended Audience :: Developers", 101 | "License :: OSI Approved :: MIT License", 102 | "Natural Language :: English", 103 | "Operating System :: OS Independent", 104 | "Programming Language :: Python :: 3 :: Only", 105 | "Programming Language :: Python :: 3.6", 106 | "Programming Language :: Python :: 3.7", 107 | "Topic :: System :: Distributed Computing", 108 | ], 109 | cmdclass={"upload": UploadCommand, "version": VersionCommand}, 110 | setup_requires=["pytest-runner"], 111 | install_requires=["click>=6.7,<6.8", "prettyconf", "simple-rest-client>=1.0.0"], 112 | tests_require=["pytest", "pytest-cov"], 113 | ) 114 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daneoshiga/sentry-patrol/746932c5469316e402fb69d9c10a03051130f875/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | import pytest 4 | from click.testing import CliRunner 5 | from patrol.patrol import Patrol 6 | 7 | 8 | @pytest.fixture 9 | def cli_runner(): 10 | return CliRunner() 11 | 12 | 13 | @pytest.fixture 14 | def patrol(): 15 | patrol = Patrol("TOKEN") 16 | patrol.api = mock.Mock() 17 | return patrol 18 | 19 | 20 | @pytest.fixture 21 | def event(): 22 | return {"eventID": "id", "message": "message"} 23 | 24 | 25 | @pytest.fixture 26 | def issue(): 27 | return {"id": "id", "title": "title", "status": "unresolved"} 28 | -------------------------------------------------------------------------------- /tests/test_patrol.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | from patrol import cli 4 | 5 | 6 | @mock.patch("patrol.patrol.Patrol.events") 7 | def test_events(mock_events, cli_runner): 8 | mock_events.return_value = [] 9 | result = cli_runner.invoke(cli.events, ["organization", "project"]) 10 | 11 | assert result.exit_code == 0 12 | assert mock_events.called 13 | 14 | 15 | @mock.patch("patrol.patrol.Patrol.event") 16 | def test_event(mock_event, cli_runner, event): 17 | mock_event.return_value = event 18 | result = cli_runner.invoke(cli.event, ["organization", "project", "event_id"]) 19 | 20 | assert result.exit_code == 0 21 | assert mock_event.called 22 | 23 | 24 | @mock.patch("patrol.patrol.Patrol.issues") 25 | def test_issues(mock_issues, cli_runner): 26 | mock_issues.return_value = [] 27 | result = cli_runner.invoke(cli.issues, ["organization", "project"]) 28 | 29 | assert result.exit_code == 0 30 | assert mock_issues.called 31 | 32 | 33 | @mock.patch("patrol.patrol.Patrol.issue") 34 | def test_issue(mock_issue, cli_runner, issue): 35 | mock_issue.return_value = issue 36 | result = cli_runner.invoke(cli.issue, ["issue_id"]) 37 | 38 | assert result.exit_code == 0 39 | assert mock_issue.called 40 | 41 | 42 | @mock.patch("patrol.patrol.Patrol.update_issue") 43 | def test_update_issue(mock_issue, cli_runner, issue): 44 | mock_issue.return_value = issue 45 | result = cli_runner.invoke( 46 | cli.update_issue_status, ["issue_id", "--status", "unresolved"] 47 | ) 48 | 49 | assert result.exit_code == 0 50 | assert mock_issue.called 51 | 52 | 53 | @mock.patch("patrol.patrol.Patrol.projects") 54 | def test_projects(mock_projects, cli_runner): 55 | mock_projects.return_value = [] 56 | result = cli_runner.invoke(cli.projects, ["organization"]) 57 | 58 | assert result.exit_code == 0 59 | assert mock_projects.called 60 | --------------------------------------------------------------------------------