├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── demo.gif ├── docs ├── customizing.md ├── index.md ├── installation.md ├── new_issue.png └── options.md ├── poetry.lock ├── pyproject.toml ├── requirements ├── base.txt └── dev.txt ├── setup.py ├── statuspage ├── __init__.py ├── statuspage.py ├── template │ ├── statuspage.js │ ├── style.css │ ├── template.html │ └── translations.ini └── tests.py └── template ├── favicon.png └── logo.png /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | pull_request: 8 | branches: 9 | - 'master' 10 | 11 | jobs: 12 | build: 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | python-version: [3.7, 3.8, 3.9] 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v2 20 | - uses: actions/setup-python@v2 21 | with: 22 | python-version: ${{ matrix.python-version }} 23 | - name: Setup poetry 24 | run: | 25 | curl -sSL https://install.python-poetry.org | python3 - 26 | - name: build 27 | run: poetry build 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | mock/ 2 | htmlcov 3 | .coverage 4 | build 5 | __pycache__/ 6 | src 7 | dist 8 | *.egg-info 9 | .idea -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | after_success: 2 | - codecov 3 | 4 | before_install: 5 | - pip install codecov 6 | 7 | install: 8 | - pip install -r requirements/base.txt 9 | - pip install -r requirements/dev.txt 10 | 11 | language: python 12 | python: 13 | - "3.7" 14 | script: coverage run --source=statuspage statuspage/tests.py 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All enhancements and patches to statuspage will be documented in this file. 3 | This project adheres to [Semantic Versioning](http://semver.org/). 4 | 5 | ## 1.0 [2016-09-6] 6 | - Added polish translation, thanks @4364354235654345u5432576865432 7 | - Added russian translation, thanks @sobolevn 8 | - Fixed a localisation bug, thanks @martignoni 9 | - Added french localisation, thanks @FleuryK 10 | - Updated dependencies: jinja, requests, pygithub, click 11 | 12 | ## 0.8.1 [2016-09-6] 13 | - Fixed a silly encoding error on legacy python 14 | 15 | ## 0.8.0 [2016-09-2] 16 | - Added client side translation. 17 | - Added german translation. 18 | - It's now possible to add/remove systems from the CLI. 19 | 20 | ## 0.7.0 [2016-07-31] 21 | - Added markdown support. 22 | - Added upgrade command. 23 | - Added support for localtime. 24 | - Added support for a config file. 25 | 26 | ## 0.6.0 [2016-07-26] 27 | - Added an option to automate the update process. 28 | - Switch to PyGithub as pygithub-redux is no longer needed 29 | - Added an option to create private repositories 30 | - Beefed up the docs 31 | 32 | ## 0.5.1 [2016-07-26] 33 | - Updated dependencies: tqdm and pygithub-redux 34 | 35 | ## 0.5.0 [2016-07-26] 36 | - Systems and Panels are now ordered to make sure that no commit is issued when nothing changes (#12) 37 | - Refactored the code to make it easier to read 38 | 39 | ## 0.4.1 [2016-07-25] 40 | - Fixed a bug on python 3 where the hash function wasn't working. 41 | 42 | ## 0.4.0 [2016-07-25] 43 | - Only commit if content differs (@Jcpetrucci) 44 | 45 | ## 0.3.3 [2016-07-13] 46 | - issued new pypi release 47 | 48 | ## 0.3.2 [2016-07-13] 49 | - fixed packaging problems by using a module 50 | - minified and merged style.css with milligram.min.css 51 | 52 | ## 0.3.1 [2016-07-12] 53 | - fixed packaging problems 54 | 55 | ## 0.3 [2016-07-12] 56 | - statuspage is now available on PyPi 57 | 58 | ## 0.2 [2016-03-08] 59 | - Added support for GitHub organizations 60 | - Makes sure that non-collaborator issues/comments are not displayed 61 | 62 | ## 0.1 [2016-03-07] 63 | - Initial release 64 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2022 Jannis Gebauer 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include statuspage/template/* 2 | include README.md 3 | include CHANGELOG.md 4 | include LICENSE 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | *We are currently alpha-testing a fully automated statuspage GitHub app. Check out [corestatus.io](https://corestatus.io/) if you like to test it out.* 2 | 3 | # Statuspage 4 | 5 | [![ci](https://github.com/jayfk/statuspage/actions/workflows/ci.yml/badge.svg)](https://github.com/jayfk/statuspage/actions/workflows/ci.yml) 6 | [![codecov.io](https://codecov.io/github/jayfk/statuspage/coverage.svg?branch=master)](https://codecov.io/github/jayfk/statuspage?branch=master) 7 | 8 | A statuspage generator that lets you host your statuspage for free on GitHub. Uses 9 | issues to display incidents and labels for severity. 10 | 11 | ## Demo 12 | 13 | ![DEMO](https://github.com/jayfk/statuspage/blob/master/demo.gif) 14 | 15 | See a real status page generated by this here [demo site](https://jayfk.github.io/statuspage-demo/) 16 | 17 | ## Quickstart 18 | 19 | Install statuspage with pip: 20 | 21 | pip install statuspage 22 | 23 | *There are also binaries for macOS and Linux available, see [installation](docs/installation.md) for more.* 24 | 25 | Now, create an GitHub API token: 26 | 27 | - Go to your [Personal Access tokens](https://github.com/settings/tokens) page. 28 | - Click on `Generate new token`. 29 | - Make sure to check the `public_repo` and `write:repo_hook` scope. 30 | - Copy the token somewhere safe, you won't be able to see it again once you leave the page. 31 | 32 | To create a new status page, run: 33 | 34 | statuspage create --token= 35 | 36 | You'll be prompted for a repo name and the systems you want to show a status for. 37 | 38 | Name: mystatuspage 39 | Systems, eg (Website,API): Website, CDN, API 40 | 41 | *Please note: This will generate a new repo under that name. Make sure it doesn't exist already.* 42 | 43 | The command takes a couple of seconds to run. Once ready, it will output links to the issue tracker and your new status page. 44 | 45 | Create new issues at https://github.com//mystatuspage/issues 46 | Visit your new status page at https://.github.com/mystatuspage/ 47 | 48 | The generator will then print the `statuspage update` command filled with all the details you need to update your page. 49 | 50 | ## Create an issue 51 | 52 | To create a new issue, go to your newly created repo and click on `New Issue`. 53 | 54 | - Click on the cog icon next to labels on the right. 55 | - Choose the affected systems (black labels) 56 | - Choose a severity label (major outage, degraded performance, investigating) 57 | - Fill in the title, leave a comment and click on `Submit new issue`. 58 | 59 | ![Add New Issue](docs/new_issue.png) 60 | 61 | Now, update your status page. Go back to your commandline and type: 62 | 63 | statuspage update --token= 64 | Name: mystatuspage 65 | 66 | If you change the issue (eg. when you add a new label, create a comment or close the issue), you'll 67 | need to run `statuspage update` again. 68 | 69 | ## Adding and removing systems 70 | 71 | In order to add or remove a system, run: 72 | 73 | statuspage add_system --token= --name= --system= 74 | statuspage remove_system --token= --name= --system= 75 | 76 | ## Upgrading from previous versions 77 | 78 | First, install the latest version with pip, or grab the latest [binary](docs/installation.md): 79 | 80 | pip install statuspage --upgrade 81 | 82 | Updating your page to the latest version is now as simple as running: 83 | 84 | statuspage upgrade --token= --name= 85 | 86 | followed by an update: 87 | 88 | statuspage update --token= --name= 89 | 90 | ## Translations 91 | The generated status page is translated via JavaScript on the client side using [webL10n](https://github.com/fabi1cazenave/webL10n). It detects the visitors preferred language and translates all strings automatically. 92 | 93 | Translations are available for the following languages: 94 | 95 | - en 96 | - bg 97 | - de 98 | - kr 99 | - nl 100 | - pt 101 | - es 102 | - ru 103 | - fr 104 | - pl 105 | - zh-HK 106 | - zh-TW 107 | - zh-CN 108 | - it 109 | - fur 110 | - vn 111 | 112 | Want to add a translation? Open `translations.ini` and add it. Pull requests welcome! 113 | 114 | ## Customizing 115 | Want to change styles, the logo, or the footer? Check out [customizing](docs/customizing.md). 116 | 117 | ## Options 118 | 119 | Want to create a status page for an organisation, or a private one? See [options](docs/options.md). 120 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jayfk/statuspage/d750aba1fbfd45c5c8168c2cb19eadd7f629ffc5/demo.gif -------------------------------------------------------------------------------- /docs/customizing.md: -------------------------------------------------------------------------------- 1 | ## Customizing 2 | 3 | **Important:** All customizations have to happen in the `gh-pages` branch. If you are using the 4 | command line, make sure to 5 | 6 | git checkout gh-pages 7 | 8 | or, on the website, select the `gh-pages` branch before editing things. 9 | 10 | ### Template 11 | 12 | Don't edit the `template.html` file directly, as it will change with each upgrade. 13 | 14 | Instead, create a file called `config.json` in the root of your repository and change the defaults. Don't forget to run `statuspage update` afterwards. 15 | 16 | ```javascript 17 | { 18 | "footer": "Status page hosted by GitHub, generated with jayfk/statuspage", 19 | "logo": "https://raw.githubusercontent.com/jayfk/statuspage/master/template/logo.png", 20 | "title": "Status", 21 | "favicon": "https://raw.githubusercontent.com/jayfk/statuspage/master/template/favicon.png" 22 | } 23 | ``` 24 | 25 | Please note: `config.json` has to be valid JSON. The best way to validate it online is at [jsonlint.com](http://jsonlint.com/). 26 | 27 | ### Use a subdomain 28 | 29 | If you want to use your own domain to host your status page, you'll need to create a CNAME file 30 | in your repository and set up a CNAME record pointing to that page with your DNS provider. 31 | 32 | If you have e.g. the domain `mydomain.com`, your GitHub username is `myusername` and you want 33 | your status page to be reachable at `status.mydomain.com` 34 | 35 | 36 | - Create a `CNAME` file in the root of your repository 37 | 38 | status.mydomain.com 39 | 40 | - Go to your DNS provider and create a new CNAME record pointing to your 41 | 42 | 43 | Name Type Value 44 | status CNAME myusername.github.io 45 | 46 | See [Using a custom domain with GitHub Pages](https://help.github.com/articles/using-a-custom-domain-with-github-pages/) 47 | for more info. -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jayfk/statuspage/d750aba1fbfd45c5c8168c2cb19eadd7f629ffc5/docs/index.md -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ## Using pip 4 | The prefered way to install statuspage is with Pythons package manager pip: 5 | 6 | pip install statuspage 7 | 8 | ## Binaries 9 | ### macOS (64Bit) 10 | curl -L https://github.com/jayfk/statuspage/releases/download/0.6.0/statuspage-darwin-64 > /usr/local/bin/statuspage 11 | chmod +x /usr/local/bin/statuspage 12 | 13 | ### Linux (64Bit) 14 | curl -L https://github.com/jayfk/statuspage/releases/download/0.6.0/statuspage-linux-64 > /usr/local/bin/statuspage 15 | chmod +x /usr/local/bin/statuspage -------------------------------------------------------------------------------- /docs/new_issue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jayfk/statuspage/d750aba1fbfd45c5c8168c2cb19eadd7f629ffc5/docs/new_issue.png -------------------------------------------------------------------------------- /docs/options.md: -------------------------------------------------------------------------------- 1 | # Options 2 | 3 | ## Create a private status page 4 | 5 | *Please note: Your Github API token needs the `repo` scope.* 6 | 7 | To create a private status page, set the `--private` flag. 8 | 9 | statuspage create --private --token= 10 | 11 | This will create a private repository, however the GitHub page will be public. 12 | 13 | ## Use Organization Account 14 | 15 | *Please note: You need to have the proper permissions to create a new repository for the given 16 | organization.* 17 | 18 | In order to create/update a status page for an organization, add the name of the organization to 19 | the `--org` flag, e.g.: 20 | 21 | statuspage create --org=my-org --name=.. 22 | 23 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "certifi" 3 | version = "2021.10.8" 4 | description = "Python package for providing Mozilla's CA Bundle." 5 | category = "main" 6 | optional = false 7 | python-versions = "*" 8 | 9 | [[package]] 10 | name = "chardet" 11 | version = "3.0.4" 12 | description = "Universal encoding detector for Python 2 and 3" 13 | category = "main" 14 | optional = false 15 | python-versions = "*" 16 | 17 | [[package]] 18 | name = "click" 19 | version = "7.0" 20 | description = "Composable command line interface toolkit" 21 | category = "main" 22 | optional = false 23 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 24 | 25 | [[package]] 26 | name = "deprecated" 27 | version = "1.2.13" 28 | description = "Python @deprecated decorator to deprecate old python classes, functions or methods." 29 | category = "main" 30 | optional = false 31 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 32 | 33 | [package.dependencies] 34 | wrapt = ">=1.10,<2" 35 | 36 | [package.extras] 37 | dev = ["tox", "bump2version (<1)", "sphinx (<2)", "importlib-metadata (<3)", "importlib-resources (<4)", "configparser (<5)", "sphinxcontrib-websupport (<2)", "zipp (<2)", "PyTest (<5)", "PyTest-Cov (<2.6)", "pytest", "pytest-cov"] 38 | 39 | [[package]] 40 | name = "idna" 41 | version = "2.8" 42 | description = "Internationalized Domain Names in Applications (IDNA)" 43 | category = "main" 44 | optional = false 45 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 46 | 47 | [[package]] 48 | name = "jinja2" 49 | version = "2.10.3" 50 | description = "A very fast and expressive template engine." 51 | category = "main" 52 | optional = false 53 | python-versions = "*" 54 | 55 | [package.dependencies] 56 | MarkupSafe = ">=0.23" 57 | 58 | [package.extras] 59 | i18n = ["Babel (>=0.8)"] 60 | 61 | [[package]] 62 | name = "markdown2" 63 | version = "2.3.8" 64 | description = "A fast and complete Python implementation of Markdown" 65 | category = "main" 66 | optional = false 67 | python-versions = "*" 68 | 69 | [[package]] 70 | name = "markupsafe" 71 | version = "2.1.0" 72 | description = "Safely add untrusted strings to HTML/XML markup." 73 | category = "main" 74 | optional = false 75 | python-versions = ">=3.7" 76 | 77 | [[package]] 78 | name = "mock" 79 | version = "3.0.5" 80 | description = "Rolling backport of unittest.mock for all Pythons" 81 | category = "dev" 82 | optional = false 83 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 84 | 85 | [package.dependencies] 86 | six = "*" 87 | 88 | [package.extras] 89 | build = ["twine", "wheel", "blurb"] 90 | docs = ["sphinx"] 91 | test = ["pytest", "pytest-cov"] 92 | 93 | [[package]] 94 | name = "pygithub" 95 | version = "1.44.1" 96 | description = "Use the full Github API v3" 97 | category = "main" 98 | optional = false 99 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 100 | 101 | [package.dependencies] 102 | deprecated = "*" 103 | pyjwt = "*" 104 | requests = ">=2.14.0" 105 | six = "*" 106 | 107 | [package.extras] 108 | integrations = ["cryptography"] 109 | 110 | [[package]] 111 | name = "pyjwt" 112 | version = "2.3.0" 113 | description = "JSON Web Token implementation in Python" 114 | category = "main" 115 | optional = false 116 | python-versions = ">=3.6" 117 | 118 | [package.extras] 119 | crypto = ["cryptography (>=3.3.1)"] 120 | dev = ["sphinx", "sphinx-rtd-theme", "zope.interface", "cryptography (>=3.3.1)", "pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)", "mypy", "pre-commit"] 121 | docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] 122 | tests = ["pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)"] 123 | 124 | [[package]] 125 | name = "requests" 126 | version = "2.22.0" 127 | description = "Python HTTP for Humans." 128 | category = "main" 129 | optional = false 130 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 131 | 132 | [package.dependencies] 133 | certifi = ">=2017.4.17" 134 | chardet = ">=3.0.2,<3.1.0" 135 | idna = ">=2.5,<2.9" 136 | urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" 137 | 138 | [package.extras] 139 | security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)"] 140 | socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] 141 | 142 | [[package]] 143 | name = "six" 144 | version = "1.16.0" 145 | description = "Python 2 and 3 compatibility utilities" 146 | category = "main" 147 | optional = false 148 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 149 | 150 | [[package]] 151 | name = "tqdm" 152 | version = "4.15.0" 153 | description = "Fast, Extensible Progress Meter" 154 | category = "main" 155 | optional = false 156 | python-versions = "*" 157 | 158 | [[package]] 159 | name = "urllib3" 160 | version = "1.25.11" 161 | description = "HTTP library with thread-safe connection pooling, file post, and more." 162 | category = "main" 163 | optional = false 164 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 165 | 166 | [package.extras] 167 | brotli = ["brotlipy (>=0.6.0)"] 168 | secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] 169 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 170 | 171 | [[package]] 172 | name = "wrapt" 173 | version = "1.13.3" 174 | description = "Module for decorators, wrappers and monkey patching." 175 | category = "main" 176 | optional = false 177 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 178 | 179 | [metadata] 180 | lock-version = "1.1" 181 | python-versions = "^3.7" 182 | content-hash = "ee7fafa729dcad0412420868139ba5b4c932421ac9b0d0597caf697ae392a74c" 183 | 184 | [metadata.files] 185 | certifi = [ 186 | {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, 187 | {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, 188 | ] 189 | chardet = [ 190 | {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, 191 | {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, 192 | ] 193 | click = [ 194 | {file = "Click-7.0-py2.py3-none-any.whl", hash = "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13"}, 195 | {file = "Click-7.0.tar.gz", hash = "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"}, 196 | ] 197 | deprecated = [ 198 | {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"}, 199 | {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"}, 200 | ] 201 | idna = [ 202 | {file = "idna-2.8-py2.py3-none-any.whl", hash = "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"}, 203 | {file = "idna-2.8.tar.gz", hash = "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407"}, 204 | ] 205 | jinja2 = [ 206 | {file = "Jinja2-2.10.3-py2.py3-none-any.whl", hash = "sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f"}, 207 | {file = "Jinja2-2.10.3.tar.gz", hash = "sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de"}, 208 | ] 209 | markdown2 = [ 210 | {file = "markdown2-2.3.8-py2.py3-none-any.whl", hash = "sha256:882d3607fc023cdea0ac2cd0e1147617fcb0361cb1133d3ff095417f995ff270"}, 211 | {file = "markdown2-2.3.8.tar.gz", hash = "sha256:7ff88e00b396c02c8e1ecd8d176cfa418fb01fe81234dcea77803e7ce4f05dbe"}, 212 | ] 213 | markupsafe = [ 214 | {file = "MarkupSafe-2.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3028252424c72b2602a323f70fbf50aa80a5d3aa616ea6add4ba21ae9cc9da4c"}, 215 | {file = "MarkupSafe-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:290b02bab3c9e216da57c1d11d2ba73a9f73a614bbdcc027d299a60cdfabb11a"}, 216 | {file = "MarkupSafe-2.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e104c0c2b4cd765b4e83909cde7ec61a1e313f8a75775897db321450e928cce"}, 217 | {file = "MarkupSafe-2.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24c3be29abb6b34052fd26fc7a8e0a49b1ee9d282e3665e8ad09a0a68faee5b3"}, 218 | {file = "MarkupSafe-2.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204730fd5fe2fe3b1e9ccadb2bd18ba8712b111dcabce185af0b3b5285a7c989"}, 219 | {file = "MarkupSafe-2.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d3b64c65328cb4cd252c94f83e66e3d7acf8891e60ebf588d7b493a55a1dbf26"}, 220 | {file = "MarkupSafe-2.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:96de1932237abe0a13ba68b63e94113678c379dca45afa040a17b6e1ad7ed076"}, 221 | {file = "MarkupSafe-2.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:75bb36f134883fdbe13d8e63b8675f5f12b80bb6627f7714c7d6c5becf22719f"}, 222 | {file = "MarkupSafe-2.1.0-cp310-cp310-win32.whl", hash = "sha256:4056f752015dfa9828dce3140dbadd543b555afb3252507348c493def166d454"}, 223 | {file = "MarkupSafe-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:d4e702eea4a2903441f2735799d217f4ac1b55f7d8ad96ab7d4e25417cb0827c"}, 224 | {file = "MarkupSafe-2.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f0eddfcabd6936558ec020130f932d479930581171368fd728efcfb6ef0dd357"}, 225 | {file = "MarkupSafe-2.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ddea4c352a488b5e1069069f2f501006b1a4362cb906bee9a193ef1245a7a61"}, 226 | {file = "MarkupSafe-2.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09c86c9643cceb1d87ca08cdc30160d1b7ab49a8a21564868921959bd16441b8"}, 227 | {file = "MarkupSafe-2.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0a0abef2ca47b33fb615b491ce31b055ef2430de52c5b3fb19a4042dbc5cadb"}, 228 | {file = "MarkupSafe-2.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:736895a020e31b428b3382a7887bfea96102c529530299f426bf2e636aacec9e"}, 229 | {file = "MarkupSafe-2.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:679cbb78914ab212c49c67ba2c7396dc599a8479de51b9a87b174700abd9ea49"}, 230 | {file = "MarkupSafe-2.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:84ad5e29bf8bab3ad70fd707d3c05524862bddc54dc040982b0dbcff36481de7"}, 231 | {file = "MarkupSafe-2.1.0-cp37-cp37m-win32.whl", hash = "sha256:8da5924cb1f9064589767b0f3fc39d03e3d0fb5aa29e0cb21d43106519bd624a"}, 232 | {file = "MarkupSafe-2.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:454ffc1cbb75227d15667c09f164a0099159da0c1f3d2636aa648f12675491ad"}, 233 | {file = "MarkupSafe-2.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:142119fb14a1ef6d758912b25c4e803c3ff66920635c44078666fe7cc3f8f759"}, 234 | {file = "MarkupSafe-2.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b2a5a856019d2833c56a3dcac1b80fe795c95f401818ea963594b345929dffa7"}, 235 | {file = "MarkupSafe-2.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d1fb9b2eec3c9714dd936860850300b51dbaa37404209c8d4cb66547884b7ed"}, 236 | {file = "MarkupSafe-2.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62c0285e91414f5c8f621a17b69fc0088394ccdaa961ef469e833dbff64bd5ea"}, 237 | {file = "MarkupSafe-2.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc3150f85e2dbcf99e65238c842d1cfe69d3e7649b19864c1cc043213d9cd730"}, 238 | {file = "MarkupSafe-2.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f02cf7221d5cd915d7fa58ab64f7ee6dd0f6cddbb48683debf5d04ae9b1c2cc1"}, 239 | {file = "MarkupSafe-2.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d5653619b3eb5cbd35bfba3c12d575db2a74d15e0e1c08bf1db788069d410ce8"}, 240 | {file = "MarkupSafe-2.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7d2f5d97fcbd004c03df8d8fe2b973fe2b14e7bfeb2cfa012eaa8759ce9a762f"}, 241 | {file = "MarkupSafe-2.1.0-cp38-cp38-win32.whl", hash = "sha256:3cace1837bc84e63b3fd2dfce37f08f8c18aeb81ef5cf6bb9b51f625cb4e6cd8"}, 242 | {file = "MarkupSafe-2.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:fabbe18087c3d33c5824cb145ffca52eccd053061df1d79d4b66dafa5ad2a5ea"}, 243 | {file = "MarkupSafe-2.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:023af8c54fe63530545f70dd2a2a7eed18d07a9a77b94e8bf1e2ff7f252db9a3"}, 244 | {file = "MarkupSafe-2.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d66624f04de4af8bbf1c7f21cc06649c1c69a7f84109179add573ce35e46d448"}, 245 | {file = "MarkupSafe-2.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c532d5ab79be0199fa2658e24a02fce8542df196e60665dd322409a03db6a52c"}, 246 | {file = "MarkupSafe-2.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67ec74fada3841b8c5f4c4f197bea916025cb9aa3fe5abf7d52b655d042f956"}, 247 | {file = "MarkupSafe-2.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c653fde75a6e5eb814d2a0a89378f83d1d3f502ab710904ee585c38888816c"}, 248 | {file = "MarkupSafe-2.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:961eb86e5be7d0973789f30ebcf6caab60b844203f4396ece27310295a6082c7"}, 249 | {file = "MarkupSafe-2.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:598b65d74615c021423bd45c2bc5e9b59539c875a9bdb7e5f2a6b92dfcfc268d"}, 250 | {file = "MarkupSafe-2.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:599941da468f2cf22bf90a84f6e2a65524e87be2fce844f96f2dd9a6c9d1e635"}, 251 | {file = "MarkupSafe-2.1.0-cp39-cp39-win32.whl", hash = "sha256:e6f7f3f41faffaea6596da86ecc2389672fa949bd035251eab26dc6697451d05"}, 252 | {file = "MarkupSafe-2.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:b8811d48078d1cf2a6863dafb896e68406c5f513048451cd2ded0473133473c7"}, 253 | {file = "MarkupSafe-2.1.0.tar.gz", hash = "sha256:80beaf63ddfbc64a0452b841d8036ca0611e049650e20afcb882f5d3c266d65f"}, 254 | ] 255 | mock = [ 256 | {file = "mock-3.0.5-py2.py3-none-any.whl", hash = "sha256:d157e52d4e5b938c550f39eb2fd15610db062441a9c2747d3dbfa9298211d0f8"}, 257 | {file = "mock-3.0.5.tar.gz", hash = "sha256:83657d894c90d5681d62155c82bda9c1187827525880eda8ff5df4ec813437c3"}, 258 | ] 259 | pygithub = [ 260 | {file = "PyGithub-1.44.1.tar.gz", hash = "sha256:453896a1c3d46eb6724598daa21cf7ae9a83c6012126e840e3f7c665142fb04f"}, 261 | ] 262 | pyjwt = [ 263 | {file = "PyJWT-2.3.0-py3-none-any.whl", hash = "sha256:e0c4bb8d9f0af0c7f5b1ec4c5036309617d03d56932877f2f7a0beeb5318322f"}, 264 | {file = "PyJWT-2.3.0.tar.gz", hash = "sha256:b888b4d56f06f6dcd777210c334e69c737be74755d3e5e9ee3fe67dc18a0ee41"}, 265 | ] 266 | requests = [ 267 | {file = "requests-2.22.0-py2.py3-none-any.whl", hash = "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"}, 268 | {file = "requests-2.22.0.tar.gz", hash = "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4"}, 269 | ] 270 | six = [ 271 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 272 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 273 | ] 274 | tqdm = [ 275 | {file = "tqdm-4.15.0-py2.py3-none-any.whl", hash = "sha256:9f019dcbec25b5b4cb1bbf43e5c346e0d6a001fe4f598d70283d379e5314e202"}, 276 | {file = "tqdm-4.15.0.tar.gz", hash = "sha256:6ec1dc74efacf2cda936b4a6cf4082ce224c76763bdec9f17e437c8cfcaa9953"}, 277 | ] 278 | urllib3 = [ 279 | {file = "urllib3-1.25.11-py2.py3-none-any.whl", hash = "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e"}, 280 | {file = "urllib3-1.25.11.tar.gz", hash = "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2"}, 281 | ] 282 | wrapt = [ 283 | {file = "wrapt-1.13.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:e05e60ff3b2b0342153be4d1b597bbcfd8330890056b9619f4ad6b8d5c96a81a"}, 284 | {file = "wrapt-1.13.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:85148f4225287b6a0665eef08a178c15097366d46b210574a658c1ff5b377489"}, 285 | {file = "wrapt-1.13.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:2dded5496e8f1592ec27079b28b6ad2a1ef0b9296d270f77b8e4a3a796cf6909"}, 286 | {file = "wrapt-1.13.3-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:e94b7d9deaa4cc7bac9198a58a7240aaf87fe56c6277ee25fa5b3aa1edebd229"}, 287 | {file = "wrapt-1.13.3-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:498e6217523111d07cd67e87a791f5e9ee769f9241fcf8a379696e25806965af"}, 288 | {file = "wrapt-1.13.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ec7e20258ecc5174029a0f391e1b948bf2906cd64c198a9b8b281b811cbc04de"}, 289 | {file = "wrapt-1.13.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:87883690cae293541e08ba2da22cacaae0a092e0ed56bbba8d018cc486fbafbb"}, 290 | {file = "wrapt-1.13.3-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:f99c0489258086308aad4ae57da9e8ecf9e1f3f30fa35d5e170b4d4896554d80"}, 291 | {file = "wrapt-1.13.3-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6a03d9917aee887690aa3f1747ce634e610f6db6f6b332b35c2dd89412912bca"}, 292 | {file = "wrapt-1.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:936503cb0a6ed28dbfa87e8fcd0a56458822144e9d11a49ccee6d9a8adb2ac44"}, 293 | {file = "wrapt-1.13.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f9c51d9af9abb899bd34ace878fbec8bf357b3194a10c4e8e0a25512826ef056"}, 294 | {file = "wrapt-1.13.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:220a869982ea9023e163ba915077816ca439489de6d2c09089b219f4e11b6785"}, 295 | {file = "wrapt-1.13.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0877fe981fd76b183711d767500e6b3111378ed2043c145e21816ee589d91096"}, 296 | {file = "wrapt-1.13.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:43e69ffe47e3609a6aec0fe723001c60c65305784d964f5007d5b4fb1bc6bf33"}, 297 | {file = "wrapt-1.13.3-cp310-cp310-win32.whl", hash = "sha256:78dea98c81915bbf510eb6a3c9c24915e4660302937b9ae05a0947164248020f"}, 298 | {file = "wrapt-1.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:ea3e746e29d4000cd98d572f3ee2a6050a4f784bb536f4ac1f035987fc1ed83e"}, 299 | {file = "wrapt-1.13.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:8c73c1a2ec7c98d7eaded149f6d225a692caa1bd7b2401a14125446e9e90410d"}, 300 | {file = "wrapt-1.13.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:086218a72ec7d986a3eddb7707c8c4526d677c7b35e355875a0fe2918b059179"}, 301 | {file = "wrapt-1.13.3-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:e92d0d4fa68ea0c02d39f1e2f9cb5bc4b4a71e8c442207433d8db47ee79d7aa3"}, 302 | {file = "wrapt-1.13.3-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:d4a5f6146cfa5c7ba0134249665acd322a70d1ea61732723c7d3e8cc0fa80755"}, 303 | {file = "wrapt-1.13.3-cp35-cp35m-win32.whl", hash = "sha256:8aab36778fa9bba1a8f06a4919556f9f8c7b33102bd71b3ab307bb3fecb21851"}, 304 | {file = "wrapt-1.13.3-cp35-cp35m-win_amd64.whl", hash = "sha256:944b180f61f5e36c0634d3202ba8509b986b5fbaf57db3e94df11abee244ba13"}, 305 | {file = "wrapt-1.13.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:2ebdde19cd3c8cdf8df3fc165bc7827334bc4e353465048b36f7deeae8ee0918"}, 306 | {file = "wrapt-1.13.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:610f5f83dd1e0ad40254c306f4764fcdc846641f120c3cf424ff57a19d5f7ade"}, 307 | {file = "wrapt-1.13.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5601f44a0f38fed36cc07db004f0eedeaadbdcec90e4e90509480e7e6060a5bc"}, 308 | {file = "wrapt-1.13.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:e6906d6f48437dfd80464f7d7af1740eadc572b9f7a4301e7dd3d65db285cacf"}, 309 | {file = "wrapt-1.13.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:766b32c762e07e26f50d8a3468e3b4228b3736c805018e4b0ec8cc01ecd88125"}, 310 | {file = "wrapt-1.13.3-cp36-cp36m-win32.whl", hash = "sha256:5f223101f21cfd41deec8ce3889dc59f88a59b409db028c469c9b20cfeefbe36"}, 311 | {file = "wrapt-1.13.3-cp36-cp36m-win_amd64.whl", hash = "sha256:f122ccd12fdc69628786d0c947bdd9cb2733be8f800d88b5a37c57f1f1d73c10"}, 312 | {file = "wrapt-1.13.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:46f7f3af321a573fc0c3586612db4decb7eb37172af1bc6173d81f5b66c2e068"}, 313 | {file = "wrapt-1.13.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:778fd096ee96890c10ce96187c76b3e99b2da44e08c9e24d5652f356873f6709"}, 314 | {file = "wrapt-1.13.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0cb23d36ed03bf46b894cfec777eec754146d68429c30431c99ef28482b5c1df"}, 315 | {file = "wrapt-1.13.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:96b81ae75591a795d8c90edc0bfaab44d3d41ffc1aae4d994c5aa21d9b8e19a2"}, 316 | {file = "wrapt-1.13.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7dd215e4e8514004c8d810a73e342c536547038fb130205ec4bba9f5de35d45b"}, 317 | {file = "wrapt-1.13.3-cp37-cp37m-win32.whl", hash = "sha256:47f0a183743e7f71f29e4e21574ad3fa95676136f45b91afcf83f6a050914829"}, 318 | {file = "wrapt-1.13.3-cp37-cp37m-win_amd64.whl", hash = "sha256:fd76c47f20984b43d93de9a82011bb6e5f8325df6c9ed4d8310029a55fa361ea"}, 319 | {file = "wrapt-1.13.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b73d4b78807bd299b38e4598b8e7bd34ed55d480160d2e7fdaabd9931afa65f9"}, 320 | {file = "wrapt-1.13.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ec9465dd69d5657b5d2fa6133b3e1e989ae27d29471a672416fd729b429eb554"}, 321 | {file = "wrapt-1.13.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dd91006848eb55af2159375134d724032a2d1d13bcc6f81cd8d3ed9f2b8e846c"}, 322 | {file = "wrapt-1.13.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ae9de71eb60940e58207f8e71fe113c639da42adb02fb2bcbcaccc1ccecd092b"}, 323 | {file = "wrapt-1.13.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:51799ca950cfee9396a87f4a1240622ac38973b6df5ef7a41e7f0b98797099ce"}, 324 | {file = "wrapt-1.13.3-cp38-cp38-win32.whl", hash = "sha256:4b9c458732450ec42578b5642ac53e312092acf8c0bfce140ada5ca1ac556f79"}, 325 | {file = "wrapt-1.13.3-cp38-cp38-win_amd64.whl", hash = "sha256:7dde79d007cd6dfa65afe404766057c2409316135cb892be4b1c768e3f3a11cb"}, 326 | {file = "wrapt-1.13.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:981da26722bebb9247a0601e2922cedf8bb7a600e89c852d063313102de6f2cb"}, 327 | {file = "wrapt-1.13.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:705e2af1f7be4707e49ced9153f8d72131090e52be9278b5dbb1498c749a1e32"}, 328 | {file = "wrapt-1.13.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:25b1b1d5df495d82be1c9d2fad408f7ce5ca8a38085e2da41bb63c914baadff7"}, 329 | {file = "wrapt-1.13.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:77416e6b17926d953b5c666a3cb718d5945df63ecf922af0ee576206d7033b5e"}, 330 | {file = "wrapt-1.13.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:865c0b50003616f05858b22174c40ffc27a38e67359fa1495605f96125f76640"}, 331 | {file = "wrapt-1.13.3-cp39-cp39-win32.whl", hash = "sha256:0a017a667d1f7411816e4bf214646d0ad5b1da2c1ea13dec6c162736ff25a374"}, 332 | {file = "wrapt-1.13.3-cp39-cp39-win_amd64.whl", hash = "sha256:81bd7c90d28a4b2e1df135bfbd7c23aee3050078ca6441bead44c42483f9ebfb"}, 333 | {file = "wrapt-1.13.3.tar.gz", hash = "sha256:1fea9cd438686e6682271d36f3481a9f3636195578bab9ca3382e2f5f01fc185"}, 334 | ] 335 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "statuspage" 3 | version = "1.0" 4 | description = "A statuspage generator that lets you host your statuspage for free on Github." 5 | authors = ["Jannis Gebauer "] 6 | license = "MIT" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.7" 10 | tqdm = "4.15.0" 11 | pygithub = "1.44.1" 12 | markdown2 = "2.3.8" 13 | jinja2 = "2.10.3" 14 | requests = "2.22.0" 15 | click = "7.0" 16 | 17 | [tool.poetry.dev-dependencies] 18 | mock = "3.0.5" 19 | 20 | [build-system] 21 | requires = ["poetry>=0.12"] 22 | build-backend = "poetry.masonry.api" 23 | -------------------------------------------------------------------------------- /requirements/base.txt: -------------------------------------------------------------------------------- 1 | pygithub==1.44.1 2 | pygithub==1.44.1 3 | jinja2==2.10.3 4 | click==7.0 5 | tqdm==4.15.0 6 | requests==2.22.0 7 | markdown2==2.4.3 -------------------------------------------------------------------------------- /requirements/dev.txt: -------------------------------------------------------------------------------- 1 | pyinstaller==3.2.1 2 | mock==3.0.5 3 | coverage==5.0 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import io 5 | import sys 6 | 7 | from setuptools import setup 8 | __version__ = "1.0" 9 | 10 | with io.open('README.md', 'r', encoding='utf-8') as readme_file: 11 | readme = readme_file.read() 12 | 13 | with io.open('CHANGELOG.md', 'r', encoding='utf-8') as history_file: 14 | history = history_file.read() 15 | 16 | requirements = [ 17 | 'pygithub', 18 | 'click', 19 | 'jinja2', 20 | 'tqdm', 21 | 'requests', 22 | 'markdown2' 23 | ] 24 | 25 | long_description = readme + '\n\n' + history 26 | 27 | setup( 28 | name='statuspage', 29 | version=__version__, 30 | description=('A statuspage generator that lets you host your statuspage for free on Github.'), 31 | long_description=long_description, 32 | author='Jannis Gebauer', 33 | author_email='ja.geb@me.com', 34 | url='https://github.com/jayfk/statuspage', 35 | entry_points=''' 36 | [console_scripts] 37 | statuspage=statuspage.statuspage:cli 38 | ''', 39 | packages=['statuspage'], 40 | package_data={'': ["template/*"]}, 41 | include_package_data=True, 42 | install_requires=requirements, 43 | license='MIT', 44 | zip_safe=False, 45 | classifiers=[ 46 | 'Development Status :: 5 - Production/Stable', 47 | 'Environment :: Console', 48 | 'Intended Audience :: Developers', 49 | 'Natural Language :: English', 50 | 'License :: OSI Approved :: MIT License', 51 | 'Programming Language :: Python', 52 | "Programming Language :: Python :: 3", 53 | 'Programming Language :: Python :: 3.7', 54 | 'Programming Language :: Python :: 3.8', 55 | 'Programming Language :: Python :: 3.9', 56 | 'Programming Language :: Python :: Implementation :: CPython', 57 | 'Programming Language :: Python :: Implementation :: PyPy', 58 | 'Topic :: Software Development', 59 | ], 60 | keywords="statuspage" 61 | ) 62 | -------------------------------------------------------------------------------- /statuspage/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jayfk/statuspage/d750aba1fbfd45c5c8168c2cb19eadd7f629ffc5/statuspage/__init__.py -------------------------------------------------------------------------------- /statuspage/statuspage.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import, print_function 3 | 4 | import sys, os 5 | import hashlib 6 | import base64 7 | from datetime import datetime, timedelta 8 | import requests 9 | from requests.exceptions import ConnectionError 10 | from github import Github, UnknownObjectException, GithubException 11 | import click 12 | from jinja2 import Template 13 | from tqdm import tqdm 14 | from collections import OrderedDict 15 | import markdown2 16 | import json 17 | 18 | __version__ = "1.0" 19 | 20 | ROOT = os.path.dirname(os.path.realpath(__file__)) 21 | 22 | PY3 = sys.version_info >= (3, 0) 23 | 24 | COLORED_LABELS = ( 25 | ("1192FC", "investigating",), 26 | ("FFA500", "degraded performance"), 27 | ("FF4D4D", "major outage", ) 28 | ) 29 | 30 | STATUSES = [status for _, status in COLORED_LABELS] 31 | 32 | SYSTEM_LABEL_COLOR = "171717" 33 | 34 | TEMPLATES = [ 35 | "template.html", 36 | "style.css", 37 | "statuspage.js", 38 | "translations.ini" 39 | ] 40 | 41 | DEFAULT_CONFIG = { 42 | "footer": "Status page hosted by GitHub, generated with jayfk/statuspage", 43 | "logo": "https://raw.githubusercontent.com/jayfk/statuspage/main/template/logo.png", 44 | "title": "Status", 45 | "favicon": "https://raw.githubusercontent.com/jayfk/statuspage/main/template/favicon.png" 46 | } 47 | 48 | 49 | @click.group() 50 | @click.version_option(__version__, '-v', '--version') 51 | def cli(): # pragma: no cover 52 | pass 53 | 54 | 55 | @cli.command() 56 | @click.option('--name', prompt='Name', help='') 57 | @click.option('--token', prompt='GitHub API Token', help='') 58 | @click.option('--org', help='GitHub Organization', default=False) 59 | @click.option('--systems', prompt='Systems, eg (Website,API)', help='') 60 | @click.option('--private/--public', default=False) 61 | def create(token, name, systems, org, private): 62 | run_create(name=name, token=token, systems=systems, org=org, private=private) 63 | 64 | 65 | @cli.command() 66 | @click.option('--name', prompt='Name', help='') 67 | @click.option('--org', help='GitHub Organization', default=False) 68 | @click.option('--token', prompt='GitHub API Token', help='') 69 | def update(name, token, org): 70 | run_update(name=name, token=token, org=org) 71 | 72 | 73 | @cli.command() 74 | @click.option('--name', prompt='Name', help='') 75 | @click.option('--org', help='GitHub Organization', default=False) 76 | @click.option('--token', prompt='GitHub API Token', help='') 77 | def upgrade(name, token, org): 78 | run_upgrade(name=name, token=token, org=org) 79 | 80 | 81 | @cli.command() 82 | @click.option('--name', prompt='Name', help='') 83 | @click.option('--org', help='GitHub Organization', default=False) 84 | @click.option('--token', prompt='GitHub API Token', help='') 85 | @click.option('--system', prompt='System', help='System to add') 86 | @click.option('--prompt/--no-prompt', default=True) 87 | def add_system(name, token, org, system, prompt): 88 | run_add_system(name=name, token=token, org=org, system=system, prompt=prompt) 89 | 90 | 91 | @cli.command() 92 | @click.option('--name', prompt='Name', help='') 93 | @click.option('--org', help='GitHub Organization', default=False) 94 | @click.option('--token', prompt='GitHub API Token', help='') 95 | @click.option('--system', prompt='System', help='System to remove') 96 | @click.option('--prompt/--no-prompt', default=True) 97 | def remove_system(name, token, org, system, prompt): 98 | run_remove_system(name=name, token=token, org=org, system=system, prompt=prompt) 99 | 100 | 101 | def run_add_system(name, token, org, system, prompt): 102 | """ 103 | Adds a new system to the repo. 104 | """ 105 | repo = get_repo(token=token, org=org, name=name) 106 | try: 107 | repo.create_label(name=system.strip(), color=SYSTEM_LABEL_COLOR) 108 | click.secho("Successfully added new system {}".format(system), fg="green") 109 | if prompt and click.confirm("Run update to re-generate the page?"): 110 | run_update(name=name, token=token, org=org) 111 | except GithubException as e: 112 | if e.status == 422: 113 | click.secho( 114 | "Unable to add new system {}, it already exists.".format(system), fg="yellow") 115 | return 116 | raise 117 | 118 | 119 | def run_remove_system(name, token, org, system, prompt): 120 | """ 121 | Removes a system from the repo. 122 | """ 123 | repo = get_repo(token=token, org=org, name=name) 124 | try: 125 | label = repo.get_label(name=system.strip()) 126 | label.delete() 127 | click.secho("Successfully deleted {}".format(system), fg="green") 128 | if prompt and click.confirm("Run update to re-generate the page?"): 129 | run_update(name=name, token=token, org=org) 130 | except UnknownObjectException: 131 | click.secho("Unable to remove system {}, it does not exist.".format(system), fg="yellow") 132 | 133 | 134 | def run_upgrade(name, token, org): 135 | click.echo("Upgrading...") 136 | 137 | repo = get_repo(token=token, name=name, org=org) 138 | files = get_files(repo=repo) 139 | head_sha = repo.get_git_ref("heads/gh-pages").object.sha 140 | 141 | # add all the template files to the gh-pages branch 142 | for template in tqdm(TEMPLATES, desc="Updating template files"): 143 | with open(os.path.join(ROOT, "template", template), "r") as f: 144 | content = f.read() 145 | if template in files: 146 | repo_template = repo.get_contents( 147 | path=template, 148 | ref=head_sha, 149 | ) 150 | if not is_same_content( 151 | content, 152 | base64.b64decode(repo_template.content).decode('utf-8') 153 | ): 154 | repo.update_file( 155 | path=template, 156 | sha=repo_template.sha, 157 | message="upgrade", 158 | content=content, 159 | branch="gh-pages" 160 | ) 161 | else: 162 | repo.create_file( 163 | path=template, 164 | message="upgrade", 165 | content=content, 166 | branch="gh-pages" 167 | ) 168 | 169 | 170 | def run_update(name, token, org): 171 | click.echo("Generating..") 172 | repo = get_repo(token=token, name=name, org=org) 173 | issues = get_issues(repo) 174 | 175 | # get the SHA of the current HEAD 176 | sha = repo.get_git_ref("heads/gh-pages").object.sha 177 | 178 | # get the template from the repo 179 | template_file = repo.get_contents( 180 | path="template.html", 181 | ref=sha 182 | ) 183 | 184 | systems = get_systems(repo, issues) 185 | incidents = get_incidents(repo, issues) 186 | panels = get_panels(systems) 187 | 188 | # render the template 189 | config = get_config(repo) 190 | template = Template(template_file.decoded_content.decode("utf-8")) 191 | content = template.render({ 192 | "systems": systems, "incidents": incidents, "panels": panels, "config": config 193 | }) 194 | 195 | # create/update the index.html with the template 196 | try: 197 | # get the index.html file, we need the sha to update it 198 | index = repo.get_contents( 199 | path="index.html", 200 | ref=sha, 201 | ) 202 | 203 | if is_same_content(content, base64.b64decode(index.content).decode('utf-8')): 204 | click.echo("Local status matches remote status, no need to commit.") 205 | return False 206 | 207 | repo.update_file( 208 | path="index.html", 209 | sha=index.sha, 210 | message="update index", 211 | content=content, 212 | branch="gh-pages" 213 | ) 214 | except UnknownObjectException: 215 | # index.html does not exist, create it 216 | repo.create_file( 217 | path="index.html", 218 | message="initial", 219 | content=content, 220 | branch="gh-pages", 221 | ) 222 | 223 | 224 | def run_create(name, token, systems, org, private): 225 | gh = Github(token) 226 | 227 | if org: 228 | entity = gh.get_organization(org) 229 | else: 230 | entity = gh.get_user() 231 | 232 | description="Visit this site at https://{login}.github.io/{name}/".format( 233 | login=entity.login, 234 | name=name 235 | ) 236 | 237 | # create the repo 238 | repo = entity.create_repo(name=name, description=description, private=private) 239 | 240 | # get all labels an delete them 241 | for label in tqdm(list(repo.get_labels()), "Deleting initial labels"): 242 | label.delete() 243 | 244 | # create new status labels 245 | for color, label in tqdm(COLORED_LABELS, desc="Creating status labels"): 246 | repo.create_label(name=label, color=color) 247 | 248 | # create system labels 249 | for label in tqdm(systems.split(","), desc="Creating system labels"): 250 | repo.create_label(name=label.strip(), color=SYSTEM_LABEL_COLOR) 251 | 252 | # add an empty file to main, otherwise we won't be able to create the gh-pages 253 | # branch 254 | repo.create_file( 255 | path="README.md", 256 | message="initial", 257 | content=description, 258 | ) 259 | 260 | # create the gh-pages branch 261 | ref = repo.get_git_ref("heads/main") 262 | repo.create_git_ref(ref="refs/heads/gh-pages", sha=ref.object.sha) 263 | 264 | # add all the template files to the gh-pages branch 265 | for template in tqdm(TEMPLATES, desc="Adding template files"): 266 | with open(os.path.join(ROOT, "template", template), "r", encoding='utf-8') as f: 267 | repo.create_file( 268 | path=template, 269 | message="initial", 270 | content=f.read(), 271 | branch="gh-pages" 272 | ) 273 | 274 | # set the gh-pages branch to be the default branch 275 | repo.edit(name=name, default_branch="gh-pages") 276 | 277 | # run an initial update to add content to the index 278 | run_update(token=token, name=name, org=org) 279 | 280 | click.echo("\nCreate new issues at https://github.com/{login}/{name}/issues".format( 281 | login=entity.login, 282 | name=name 283 | )) 284 | click.echo("Visit your new status page at https://{login}.github.io/{name}/".format( 285 | login=entity.login, 286 | name=name 287 | )) 288 | 289 | click.secho("\nYour status page is now set up and ready!\n", fg="green") 290 | click.echo("Please note: You need to run the 'statuspage update' command whenever you update or create an issue.\n") 291 | 292 | click.echo("\nIn order to update this status page, run the following command:") 293 | click.echo("statuspage update --name={name} --token={token} {org}".format( 294 | name=name, token=token, org="--org=" + entity.login if org else "")) 295 | 296 | 297 | 298 | def iter_systems(labels): 299 | for label in labels: 300 | if label.color == SYSTEM_LABEL_COLOR: 301 | yield label.name 302 | 303 | 304 | def get_files(repo): 305 | """ 306 | Get a list of all files. 307 | """ 308 | return [file.path for file in repo.get_contents("/", ref="gh-pages")] 309 | 310 | 311 | def get_config(repo): 312 | """ 313 | Get the config for the repo, merged with the default config. Returns the default config if 314 | no config file is found. 315 | """ 316 | files = get_files(repo) 317 | config = DEFAULT_CONFIG 318 | if "config.json" in files: 319 | # get the config file, parse JSON and merge it with the default config 320 | config_file = repo.get_contents('config.json', ref="gh-pages") 321 | try: 322 | repo_config = json.loads(config_file.decoded_content.decode("utf-8")) 323 | config.update(repo_config) 324 | except ValueError: 325 | click.secho("WARNING: Unable to parse config file. Using defaults.", fg="yellow") 326 | return config 327 | 328 | 329 | def get_severity(labels): 330 | label_map = dict(COLORED_LABELS) 331 | for label in labels: 332 | if label.color in label_map: 333 | return label_map[label.color] 334 | return None 335 | 336 | 337 | def get_panels(systems): 338 | # initialize and fill the panels with affected systems 339 | panels = OrderedDict() 340 | for system, data in systems.items(): 341 | if data["status"] != "operational": 342 | if data["status"] in panels: 343 | panels[data["status"]].append(system) 344 | else: 345 | panels[data["status"]] = [system, ] 346 | return panels 347 | 348 | 349 | def get_repo(token, name, org): 350 | gh = Github(token) 351 | if org: 352 | return gh.get_organization(org).get_repo(name=name) 353 | return gh.get_user().get_repo(name=name) 354 | 355 | 356 | def get_collaborators(repo): 357 | return [col.login for col in repo.get_collaborators()] 358 | 359 | 360 | def get_systems(repo, issues): 361 | systems = OrderedDict() 362 | # get all systems and mark them as operational 363 | for name in sorted(iter_systems(labels=repo.get_labels())): 364 | systems[name] = { 365 | "status": "operational", 366 | } 367 | 368 | for issue in issues: 369 | if issue.state == "open": 370 | labels = issue.get_labels() 371 | severity = get_severity(labels) 372 | affected_systems = list(iter_systems(labels)) 373 | # shit is hitting the fan RIGHT NOW. Mark all affected systems 374 | for affected_system in affected_systems: 375 | systems[affected_system]["status"] = severity 376 | return systems 377 | 378 | 379 | def get_incidents(repo, issues): 380 | # loop over all issues in the past 90 days to get current and past incidents 381 | incidents = [] 382 | collaborators = get_collaborators(repo=repo) 383 | for issue in issues: 384 | labels = issue.get_labels() 385 | affected_systems = sorted(iter_systems(labels)) 386 | severity = get_severity(labels) 387 | 388 | # make sure that non-labeled issues are not displayed 389 | if not affected_systems or (severity is None and issue.state != "closed"): 390 | continue 391 | 392 | # make sure that the user that created the issue is a collaborator 393 | if issue.user.login not in collaborators: 394 | continue 395 | 396 | # create an incident 397 | incident = { 398 | "created": issue.created_at, 399 | "title": issue.title, 400 | "systems": affected_systems, 401 | "severity": severity, 402 | "closed": issue.state == "closed", 403 | "body": markdown2.markdown(issue.body), 404 | "updates": [] 405 | } 406 | 407 | for comment in issue.get_comments(): 408 | # add comments by collaborators only 409 | if comment.user.login in collaborators: 410 | incident["updates"].append({ 411 | "created": comment.created_at, 412 | "body": markdown2.markdown(comment.body) 413 | }) 414 | 415 | incidents.append(incident) 416 | 417 | # sort incidents by date 418 | return sorted(incidents, key=lambda i: i["created"], reverse=True) 419 | 420 | 421 | def get_issues(repo): 422 | return repo.get_issues(state="all", since=datetime.now() - timedelta(days=90)) 423 | 424 | 425 | def is_same_content(c1, c2): 426 | def sha1(c): 427 | if PY3: 428 | if isinstance(c, str): 429 | c = bytes(c, "utf-8") 430 | else: 431 | c = c.encode("utf-8") 432 | return hashlib.sha1(c) 433 | return sha1(c1).hexdigest() == sha1(c2).hexdigest() 434 | 435 | 436 | if __name__ == '__main__': # pragma: no cover 437 | cli() 438 | -------------------------------------------------------------------------------- /statuspage/template/style.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Milligram v1.1.0 3 | * http://milligram.github.io 4 | * 5 | * Copyright (c) 2016 CJ Patoilo 6 | * Licensed under the MIT license 7 | */ 8 | 9 | 10 | html{box-sizing:border-box;font-size:62.5%}body{color:#606c76;font-family:"Roboto","Helvetica Neue","Helvetica","Arial",sans-serif;font-size:1.6em;font-weight:300;letter-spacing:.01em;line-height:1.6}*,*:after,*:before{box-sizing:inherit}blockquote{border-left:.3rem solid #d1d1d1;margin-left:0;margin-right:0;padding:1rem 1.5rem}blockquote *:last-child{margin:0}.button,button,input[type='button'],input[type='reset'],input[type='submit']{background-color:#9b4dca;border:.1rem solid #9b4dca;border-radius:.4rem;color:#fff;cursor:pointer;display:inline-block;font-size:1.1rem;font-weight:700;height:3.8rem;letter-spacing:.1rem;line-height:3.8rem;padding:0 3rem;text-align:center;text-decoration:none;text-transform:uppercase;white-space:nowrap}.button:hover,.button:focus,button:hover,button:focus,input[type='button']:hover,input[type='button']:focus,input[type='reset']:hover,input[type='reset']:focus,input[type='submit']:hover,input[type='submit']:focus{background-color:#606c76;border-color:#606c76;color:#fff;outline:0}.button.button-disabled,.button[disabled],button.button-disabled,button[disabled],input[type='button'].button-disabled,input[type='button'][disabled],input[type='reset'].button-disabled,input[type='reset'][disabled],input[type='submit'].button-disabled,input[type='submit'][disabled]{opacity:.5;cursor:default}.button.button-disabled:hover,.button.button-disabled:focus,.button[disabled]:hover,.button[disabled]:focus,button.button-disabled:hover,button.button-disabled:focus,button[disabled]:hover,button[disabled]:focus,input[type='button'].button-disabled:hover,input[type='button'].button-disabled:focus,input[type='button'][disabled]:hover,input[type='button'][disabled]:focus,input[type='reset'].button-disabled:hover,input[type='reset'].button-disabled:focus,input[type='reset'][disabled]:hover,input[type='reset'][disabled]:focus,input[type='submit'].button-disabled:hover,input[type='submit'].button-disabled:focus,input[type='submit'][disabled]:hover,input[type='submit'][disabled]:focus{background-color:#9b4dca;border-color:#9b4dca}.button.button-outline,button.button-outline,input[type='button'].button-outline,input[type='reset'].button-outline,input[type='submit'].button-outline{color:#9b4dca;background-color:transparent}.button.button-outline:hover,.button.button-outline:focus,button.button-outline:hover,button.button-outline:focus,input[type='button'].button-outline:hover,input[type='button'].button-outline:focus,input[type='reset'].button-outline:hover,input[type='reset'].button-outline:focus,input[type='submit'].button-outline:hover,input[type='submit'].button-outline:focus{color:#606c76;background-color:transparent;border-color:#606c76}.button.button-outline.button-disabled:hover,.button.button-outline.button-disabled:focus,.button.button-outline[disabled]:hover,.button.button-outline[disabled]:focus,button.button-outline.button-disabled:hover,button.button-outline.button-disabled:focus,button.button-outline[disabled]:hover,button.button-outline[disabled]:focus,input[type='button'].button-outline.button-disabled:hover,input[type='button'].button-outline.button-disabled:focus,input[type='button'].button-outline[disabled]:hover,input[type='button'].button-outline[disabled]:focus,input[type='reset'].button-outline.button-disabled:hover,input[type='reset'].button-outline.button-disabled:focus,input[type='reset'].button-outline[disabled]:hover,input[type='reset'].button-outline[disabled]:focus,input[type='submit'].button-outline.button-disabled:hover,input[type='submit'].button-outline.button-disabled:focus,input[type='submit'].button-outline[disabled]:hover,input[type='submit'].button-outline[disabled]:focus{color:#9b4dca;border-color:inherit}.button.button-clear,button.button-clear,input[type='button'].button-clear,input[type='reset'].button-clear,input[type='submit'].button-clear{color:#9b4dca;background-color:transparent;border-color:transparent}.button.button-clear:hover,.button.button-clear:focus,button.button-clear:hover,button.button-clear:focus,input[type='button'].button-clear:hover,input[type='button'].button-clear:focus,input[type='reset'].button-clear:hover,input[type='reset'].button-clear:focus,input[type='submit'].button-clear:hover,input[type='submit'].button-clear:focus{color:#606c76;background-color:transparent;border-color:transparent}.button.button-clear.button-disabled:hover,.button.button-clear.button-disabled:focus,.button.button-clear[disabled]:hover,.button.button-clear[disabled]:focus,button.button-clear.button-disabled:hover,button.button-clear.button-disabled:focus,button.button-clear[disabled]:hover,button.button-clear[disabled]:focus,input[type='button'].button-clear.button-disabled:hover,input[type='button'].button-clear.button-disabled:focus,input[type='button'].button-clear[disabled]:hover,input[type='button'].button-clear[disabled]:focus,input[type='reset'].button-clear.button-disabled:hover,input[type='reset'].button-clear.button-disabled:focus,input[type='reset'].button-clear[disabled]:hover,input[type='reset'].button-clear[disabled]:focus,input[type='submit'].button-clear.button-disabled:hover,input[type='submit'].button-clear.button-disabled:focus,input[type='submit'].button-clear[disabled]:hover,input[type='submit'].button-clear[disabled]:focus{color:#9b4dca}code{background:#f4f5f6;border-radius:.4rem;font-size:86%;padding:.2rem .5rem;margin:0 .2rem;white-space:nowrap}pre{background:#f4f5f6;border-left:.3rem solid #9b4dca;font-family:"Menlo","Consolas","Bitstream Vera Sans Mono","DejaVu Sans Mono","Monaco",monospace}pre>code{background:transparent;border-radius:0;display:block;padding:1rem 1.5rem;white-space:pre}hr{border:0;border-top:.1rem solid #f4f5f6;margin-bottom:3.5rem;margin-top:3rem}input[type='email'],input[type='number'],input[type='password'],input[type='search'],input[type='tel'],input[type='text'],input[type='url'],textarea,select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent;border:.1rem solid #d1d1d1;border-radius:.4rem;box-shadow:none;height:3.8rem;padding:.6rem 1rem;width:100%}input[type='email']:focus,input[type='number']:focus,input[type='password']:focus,input[type='search']:focus,input[type='tel']:focus,input[type='text']:focus,input[type='url']:focus,textarea:focus,select:focus{border:.1rem solid #9b4dca;outline:0}select{padding:.6rem 3rem .6rem 1rem;background:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIiAgIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIgICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgICB4bWxuczpzb2RpcG9kaT0iaHR0cDovL3NvZGlwb2RpLnNvdXJjZWZvcmdlLm5ldC9EVEQvc29kaXBvZGktMC5kdGQiICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiICAgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMjkgMTQiICAgaGVpZ2h0PSIxNHB4IiAgIGlkPSJMYXllcl8xIiAgIHZlcnNpb249IjEuMSIgICB2aWV3Qm94PSIwIDAgMjkgMTQiICAgd2lkdGg9IjI5cHgiICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIgICBpbmtzY2FwZTp2ZXJzaW9uPSIwLjQ4LjQgcjk5MzkiICAgc29kaXBvZGk6ZG9jbmFtZT0iY2FyZXQtZ3JheS5zdmciPjxtZXRhZGF0YSAgICAgaWQ9Im1ldGFkYXRhMzAzOSI+PHJkZjpSREY+PGNjOldvcmsgICAgICAgICByZGY6YWJvdXQ9IiI+PGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+PGRjOnR5cGUgICAgICAgICAgIHJkZjpyZXNvdXJjZT0iaHR0cDovL3B1cmwub3JnL2RjL2RjbWl0eXBlL1N0aWxsSW1hZ2UiIC8+PC9jYzpXb3JrPjwvcmRmOlJERj48L21ldGFkYXRhPjxkZWZzICAgICBpZD0iZGVmczMwMzciIC8+PHNvZGlwb2RpOm5hbWVkdmlldyAgICAgcGFnZWNvbG9yPSIjZmZmZmZmIiAgICAgYm9yZGVyY29sb3I9IiM2NjY2NjYiICAgICBib3JkZXJvcGFjaXR5PSIxIiAgICAgb2JqZWN0dG9sZXJhbmNlPSIxMCIgICAgIGdyaWR0b2xlcmFuY2U9IjEwIiAgICAgZ3VpZGV0b2xlcmFuY2U9IjEwIiAgICAgaW5rc2NhcGU6cGFnZW9wYWNpdHk9IjAiICAgICBpbmtzY2FwZTpwYWdlc2hhZG93PSIyIiAgICAgaW5rc2NhcGU6d2luZG93LXdpZHRoPSI5MDMiICAgICBpbmtzY2FwZTp3aW5kb3ctaGVpZ2h0PSI1OTQiICAgICBpZD0ibmFtZWR2aWV3MzAzNSIgICAgIHNob3dncmlkPSJ0cnVlIiAgICAgaW5rc2NhcGU6em9vbT0iMTIuMTM3OTMxIiAgICAgaW5rc2NhcGU6Y3g9Ii00LjExOTMxODJlLTA4IiAgICAgaW5rc2NhcGU6Y3k9IjciICAgICBpbmtzY2FwZTp3aW5kb3cteD0iNTAyIiAgICAgaW5rc2NhcGU6d2luZG93LXk9IjMwMiIgICAgIGlua3NjYXBlOndpbmRvdy1tYXhpbWl6ZWQ9IjAiICAgICBpbmtzY2FwZTpjdXJyZW50LWxheWVyPSJMYXllcl8xIj48aW5rc2NhcGU6Z3JpZCAgICAgICB0eXBlPSJ4eWdyaWQiICAgICAgIGlkPSJncmlkMzA0MSIgLz48L3NvZGlwb2RpOm5hbWVkdmlldz48cG9seWdvbiAgICAgcG9pbnRzPSIwLjE1LDAgMTQuNSwxNC4zNSAyOC44NSwwICIgICAgIGlkPSJwb2x5Z29uMzAzMyIgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAuMzU0MTEzODcsMCwwLDAuNDgzMjkxMSw5LjMyNDE1NDUsMy42MjQ5OTkyKSIgICAgIHN0eWxlPSJmaWxsOiNkMWQxZDE7ZmlsbC1vcGFjaXR5OjEiIC8+PC9zdmc+) center right no-repeat}select:focus{background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIiAgIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIgICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgICB4bWxuczpzb2RpcG9kaT0iaHR0cDovL3NvZGlwb2RpLnNvdXJjZWZvcmdlLm5ldC9EVEQvc29kaXBvZGktMC5kdGQiICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiICAgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMjkgMTQiICAgaGVpZ2h0PSIxNHB4IiAgIGlkPSJMYXllcl8xIiAgIHZlcnNpb249IjEuMSIgICB2aWV3Qm94PSIwIDAgMjkgMTQiICAgd2lkdGg9IjI5cHgiICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIgICBpbmtzY2FwZTp2ZXJzaW9uPSIwLjQ4LjQgcjk5MzkiICAgc29kaXBvZGk6ZG9jbmFtZT0iY2FyZXQuc3ZnIj48bWV0YWRhdGEgICAgIGlkPSJtZXRhZGF0YTMwMzkiPjxyZGY6UkRGPjxjYzpXb3JrICAgICAgICAgcmRmOmFib3V0PSIiPjxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PjxkYzp0eXBlICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPjwvY2M6V29yaz48L3JkZjpSREY+PC9tZXRhZGF0YT48ZGVmcyAgICAgaWQ9ImRlZnMzMDM3IiAvPjxzb2RpcG9kaTpuYW1lZHZpZXcgICAgIHBhZ2Vjb2xvcj0iI2ZmZmZmZiIgICAgIGJvcmRlcmNvbG9yPSIjNjY2NjY2IiAgICAgYm9yZGVyb3BhY2l0eT0iMSIgICAgIG9iamVjdHRvbGVyYW5jZT0iMTAiICAgICBncmlkdG9sZXJhbmNlPSIxMCIgICAgIGd1aWRldG9sZXJhbmNlPSIxMCIgICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwIiAgICAgaW5rc2NhcGU6cGFnZXNoYWRvdz0iMiIgICAgIGlua3NjYXBlOndpbmRvdy13aWR0aD0iOTAzIiAgICAgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iNTk0IiAgICAgaWQ9Im5hbWVkdmlldzMwMzUiICAgICBzaG93Z3JpZD0idHJ1ZSIgICAgIGlua3NjYXBlOnpvb209IjEyLjEzNzkzMSIgICAgIGlua3NjYXBlOmN4PSItNC4xMTkzMTgyZS0wOCIgICAgIGlua3NjYXBlOmN5PSI3IiAgICAgaW5rc2NhcGU6d2luZG93LXg9IjUwMiIgICAgIGlua3NjYXBlOndpbmRvdy15PSIzMDIiICAgICBpbmtzY2FwZTp3aW5kb3ctbWF4aW1pemVkPSIwIiAgICAgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0iTGF5ZXJfMSI+PGlua3NjYXBlOmdyaWQgICAgICAgdHlwZT0ieHlncmlkIiAgICAgICBpZD0iZ3JpZDMwNDEiIC8+PC9zb2RpcG9kaTpuYW1lZHZpZXc+PHBvbHlnb24gICAgIHBvaW50cz0iMjguODUsMCAwLjE1LDAgMTQuNSwxNC4zNSAiICAgICBpZD0icG9seWdvbjMwMzMiICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLjM1NDExMzg3LDAsMCwwLjQ4MzI5MTEsOS4zMjQxNTUzLDMuNjI1KSIgICAgIHN0eWxlPSJmaWxsOiM5YjRkY2Y7ZmlsbC1vcGFjaXR5OjEiIC8+PC9zdmc+)}textarea{padding-bottom:.6rem;padding-top:.6rem;min-height:6.5rem}label,legend{font-size:1.6rem;font-weight:700;display:block;margin-bottom:.5rem}fieldset{border-width:0;padding:0}input[type='checkbox'],input[type='radio']{display:inline}.label-inline{font-weight:normal;display:inline-block;margin-left:.5rem}.container{margin:0 auto;max-width:112rem;padding:0 2rem;position:relative;width:100%}.row{display:flex;flex-direction:column;padding:0;width:100%}.row .row-wrap{flex-wrap:wrap}.row .row-no-padding{padding:0}.row .row-no-padding>.column{padding:0}.row .row-top{align-items:flex-start}.row .row-bottom{align-items:flex-end}.row .row-center{align-items:center}.row .row-stretch{align-items:stretch}.row .row-baseline{align-items:baseline}.row .column{display:block;flex:1;margin-left:0;max-width:100%;width:100%}.row .column .col-top{align-self:flex-start}.row .column .col-bottom{align-self:flex-end}.row .column .col-center{align-self:center}.row .column.column-offset-10{margin-left:10%}.row .column.column-offset-20{margin-left:20%}.row .column.column-offset-25{margin-left:25%}.row .column.column-offset-33,.row .column.column-offset-34{margin-left:33.3333%}.row .column.column-offset-50{margin-left:50%}.row .column.column-offset-66,.row .column.column-offset-67{margin-left:66.6666%}.row .column.column-offset-75{margin-left:75%}.row .column.column-offset-80{margin-left:80%}.row .column.column-offset-90{margin-left:90%}.row .column.column-10{flex:0 0 10%;max-width:10%}.row .column.column-20{flex:0 0 20%;max-width:20%}.row .column.column-25{flex:0 0 25%;max-width:25%}.row .column.column-33,.row .column.column-34{flex:0 0 33.3333%;max-width:33.3333%}.row .column.column-40{flex:0 0 40%;max-width:40%}.row .column.column-50{flex:0 0 50%;max-width:50%}.row .column.column-60{flex:0 0 60%;max-width:60%}.row .column.column-66,.row .column.column-67{flex:0 0 66.6666%;max-width:66.6666%}.row .column.column-75{flex:0 0 75%;max-width:75%}.row .column.column-80{flex:0 0 80%;max-width:80%}.row .column.column-90{flex:0 0 90%;max-width:90%}@media (min-width: 40rem){.row{flex-direction:row;margin-left:-1rem;width:calc(100% + 2.0rem)}.row .column{margin-bottom:inherit;padding:0 1rem}}a{color:#9b4dca;text-decoration:none}a:hover{color:#606c76}dl,ol,ul{margin-top:0;padding-left:0}dl ul,dl ol,ol ul,ol ol,ul ul,ul ol{font-size:90%;margin:1.5rem 0 1.5rem 3rem}dl{list-style:none}ul{list-style:circle inside}ol{list-style:decimal inside}dt,dd,li{margin-bottom:1rem}.button,button{margin-bottom:1rem}input,textarea,select,fieldset{margin-bottom:1.5rem}pre,blockquote,dl,figure,table,p,ul,ol,form{margin-bottom:2.5rem}table{width:100%}th,td{border-bottom:.1rem solid #e1e1e1;padding:1.2rem 1.5rem;text-align:left}th:first-child,td:first-child{padding-left:0}th:last-child,td:last-child{padding-right:0}p{margin-top:0}h1,h2,h3,h4,h5,h6{font-weight:300;margin-bottom:2rem;margin-top:0}h1{font-size:4rem;letter-spacing:-0.1rem;line-height:1.2}h2{font-size:3.6rem;letter-spacing:-0.1rem;line-height:1.25}h3{font-size:3rem;letter-spacing:-0.1rem;line-height:1.3}h4{font-size:2.4rem;letter-spacing:-0.08rem;line-height:1.35}h5{font-size:1.8rem;letter-spacing:-0.05rem;line-height:1.5}h6{font-size:1.6rem;letter-spacing:0;line-height:1.4}@media (min-width: 40rem){h1{font-size:5rem}h2{font-size:4.2rem}h3{font-size:3.6rem}h4{font-size:3rem}h5{font-size:2.4rem}h6{font-size:1.5rem}}.float-right{float:right}.float-left{float:left}.clearfix{*zoom:1}.clearfix:after,.clearfix:before{content:"";display:table}.clearfix:after{clear:both} 11 | 12 | /*# sourceMappingURL=milligram.min.css.map */ 13 | .navigation,.wrapper{display:block;width:100%}img{max-width:100%}.wrapper{position:relative;overflow:hidden}.container{padding-top:7.5rem;padding-bottom:7.5rem;margin-bottom:0;max-width:80rem}.footer .container,.navigation .container{padding-top:0;padding-bottom:0}.footer p{font-size:1.3rem}.navigation{left:0;position:fixed;right:0;top:0;max-width:100vw;z-index:99;background:#f4f5f6;border-bottom:.1rem solid #d1d1d1;height:5.2rem}.navigation .img{position:relative;top:.3rem;height:2rem}.navigation .title,.navigation-title{color:#606c76;display:inline;font-family:'Helvetica Neue',Arial,sans-serif;line-height:5.2rem;font-size:1.6rem;padding:0;position:relative;text-decoration:none}.panel{color:#fff;padding:20px;font-size:1.2em;font-weight:600;border-radius:.5rem;margin-bottom:20px}.label.operational,.panel.operational{background-color:#0DE877}.label.degraded.performance,.panel.degraded.performance{background-color:orange}.label.investigating,.panel.investigating{background-color:#1192FC}.label.major.outage,.panel.major.outage{background-color:#FF4D4D}.label.system{background-color:#171717}ul.systems{border:.1rem solid #e1e1e1;border-radius:.5rem}ul.systems>li{list-style:none;border-bottom:.1rem solid #e1e1e1;padding:15px;margin-bottom:0}ul.systems>li:last-child{border:none}.systems .status{float:right}.status.operational{color:#0DE877}.status.degraded.performance{color:orange}.status.investigating{color:#1192FC}.status.major.outage{color:#FF4D4D}hr{margin-top:1.5rem;margin-bottom:1.5rem;border-top:.3rem solid #f4f5f6}h4{padding-top:3rem}.incident .title{font-size:2rem;font-weight:700}#main{padding-top:8.5rem}.label{border-radius:.4rem;color:#fff;padding:.4rem .6rem;font-size:1.5rem;font-weight:700;margin-left:.5rem}.incident{padding-top:2rem;padding-bottom:2rem}.incident .date{font-size:2rem;font-weight:700} -------------------------------------------------------------------------------- /statuspage/template/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ config.title }} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 29 | 30 |
31 | {% if not panels %} 32 |
33 | All Systems Operational 34 |
35 | {% else %} 36 | {% for severity, systems in panels.items() if systems %} 37 |
38 | {{ severity.capitalize() }} on {% for system in systems %}{{ system }}{% if not loop.last %}, {% endif %}{% endfor %}. 39 |
40 | {% endfor %} 41 | {% endif %} 42 | 43 |

Systems

44 |
    45 | {% for system, data in systems.items() %} 46 |
  • 47 | {{ system }} {{ data.status }} 48 |
  • 49 | {% endfor %} 50 |
51 | 52 |

Incidents

53 | {% if incidents %} 54 | {% for incident in incidents %} 55 |
56 | {{ incident.created }} UTC 57 | 58 | {% if incident.closed %} 59 | resolved 60 | {% else %} 61 | {{ incident.severity }} 62 | {% endif %} 63 | {% for system in incident.systems %} 64 | {{ system }} 65 | {% endfor %} 66 |
67 | 68 | {{ incident.title }} 69 | {{ incident.body|safe }} 70 | {% for update in incident.updates %} 71 |

Update {{ update.created }} UTC

72 | {{ update.body|safe }} 73 | {% endfor %} 74 |
75 | {% endfor %} 76 | {% else %} 77 | No incidents in the past 90 days. 78 | {% endif %} 79 |
80 | 81 | 82 |
83 |
84 |
85 |

{{ config.footer }}

86 |
87 |
88 | 89 |
90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /statuspage/template/translations.ini: -------------------------------------------------------------------------------- 1 | [en] 2 | Major-outage = Major outage 3 | Degraded-performance = Degraded performance 4 | Investigating = Investigating 5 | major-outage = major outage 6 | degraded-performance = degraded performance 7 | investigating = investigating 8 | on = on 9 | systems = Systems 10 | systems-operational = All Systems operational. 11 | incidents = Incidents 12 | resolved = resolved 13 | no-incidents = No incidents in the past 90 days. 14 | operational = operational 15 | [bg] 16 | Major-outage = Сериозен проблем 17 | Degraded-performance = Влошена производителност 18 | Investigating = Идут работы 19 | major-outage = сериозен проблем 20 | degraded-performance = влошена производителност 21 | investigating = проучване 22 | on = on 23 | systems = Системи 24 | systems-operational = Всички системи са в изправност. 25 | incidents = Инциденти 26 | resolved = решени 27 | no-incidents = Няма инциденти за последните 90 дни. 28 | operational = в изправност 29 | [de] 30 | Major-outage = Schwere Störung 31 | Degraded-performance = Leichte Störung 32 | Investigating = Untersuche Vorfall 33 | major-outage = schwere Störung 34 | degraded-performance = leichte Störung 35 | investigating = untersuche Vorfall 36 | on = auf 37 | systems = Systeme 38 | systems-operational = Alle Systeme laufen einwandfrei. 39 | incidents = Vorfälle 40 | resolved = gelöst 41 | no-incidents = Keine Vorfälle in den vergangenen 90 Tagen. 42 | operational = operational 43 | [kr] 44 | Major-outage = 오류가 발생하였습니다. 45 | Degraded-performance = 성능 저하 46 | Investigating = 원인 파악중 47 | major-outage = 오류가 발생했다 48 | degraded-performance = 성능 저하 49 | investigating = 원인 파악중 50 | on = 온 51 | systems = 시스템 52 | systems-operational = 모든 시스템이 정상적으로 작동중 입니다. 53 | incidents = 문제 54 | resolved = 해경됨 55 | no-incidents = 지난 90일 동안 아무 문제가 없었어요. 56 | operational = 정상 작동중 57 | [nl] 58 | Major-outage = Grote storing 59 | Degraded-performance = Kleine storing 60 | Investigating = Incident wordt onderzocht 61 | major-outage = grote storing 62 | degraded-performance = kleine storing 63 | investigating = incident wordt onderzocht 64 | on = op 65 | systems = Onderdelen 66 | systems-operational = Alle onderdelen zijn operationeel 67 | incidents = Incidenten 68 | resolved = opgelost 69 | no-incidents = Geen incidenten in de afgelopen 90 dagen. 70 | operational = operationeel 71 | [pt] 72 | Major-outage = Sem serviço 73 | Degraded-performance = Performance reduzida 74 | Investigating = Investigando 75 | major-outage = sem serviço 76 | degraded-performance = Performance reduzida 77 | investigating = investigando 78 | on = de 79 | systems = Sistemas 80 | systems-operational = Todos sistemas operantes. 81 | incidents = Incidentes 82 | resolved = resolvido 83 | no-incidents = Sem incidentes nos últimos 90 dias. 84 | operational = operacional 85 | [es] 86 | Major-outage = Incidente grave 87 | Degraded-performance = Rendimiento degradado 88 | Investigating = Investigando 89 | major-outage = incidente grave 90 | degraded-performance = rendimiento degradado 91 | investigating = investigando 92 | on = en 93 | systems = Sistemas 94 | systems-operational = Todos los sistemas están operativos. 95 | incidents = Incidentes 96 | resolved = resuelto 97 | no-incidents = Sin indicentes en los últimos 90 días. 98 | operational = operacional 99 | [ru] 100 | Major-outage = Масшатбные перебои 101 | Degraded-performance = Ухудшение производительности 102 | Investigating = Идут работы 103 | major-outage = масшатбные перебои 104 | degraded-performance = ухудшение производительности 105 | investigating = идут работы 106 | on = on 107 | systems = Системы 108 | systems-operational = Все системы работают. 109 | incidents = Инциденты 110 | resolved = решены 111 | no-incidents = Не было инцидентов за последние 90 дней. 112 | operational = системы работают 113 | [fr] 114 | Major-outage = Panne majeure 115 | Degraded-performance = Performance dégradée 116 | Investigating = Investigation 117 | major-outage = panne majeure 118 | degraded-performance = performance dégradée 119 | investigating = investigation 120 | on = sur 121 | systems = Systèmes 122 | systems-operational = Tous les systèmes sont opérationnels. 123 | incidents = Incidents 124 | resolved = résolu 125 | no-incidents = Aucun incident au cours des 90 derniers jours. 126 | operational = opérationnel 127 | [pl] 128 | Major-outage = Poważna przerwa 129 | Degraded-performance = Pogorszona wydajność 130 | Investigating = Dochodzenie 131 | major-outage = poważna przerwa 132 | degraded-performance = zdegradowana wydajność 133 | investigating = dochodzenie 134 | on = w 135 | systems = Systemy 136 | systems-operational = Wszystkie systemy działają. 137 | incidents = Incydenty 138 | resolved = rozwiążany 139 | no-incidents = Brak zdarzeń w ciągu ostatnich 90 dni. 140 | operational = operacyjny 141 | [zh-HK] 142 | Major-outage = 重大事故 143 | Degraded-performance = 性能下降 144 | Investigating = 調查中 145 | major-outage = 重大事故 146 | degraded-performance = 性能下降 147 | investigating = 調查中 148 | on = 上線 149 | systems = 系統 150 | systems-operational = 系統正常工作 151 | incidents = 事故 152 | resolved = 解決了 153 | no-incidents = 在90天中沒有任何事故 154 | operational = 正常運行中 155 | [zh-TW] 156 | Major-outage = 重大事故 157 | Degraded-performance = 性能下降 158 | Investigating = 調查中 159 | major-outage = 重大事故 160 | degraded-performance = 性能下降 161 | investigating = 調查中 162 | on = 上線 163 | systems = 系統 164 | systems-operational = 系統正常工作 165 | incidents = 事故 166 | resolved = 解決了 167 | no-incidents = 在90天中沒有任何事故 168 | operational = 正常運行中 169 | [zh-CN] 170 | Major-outage = 重大事故 171 | Degraded-performance = 性能下降 172 | Investigating = 调查中 173 | major-outage = 重大事故 174 | degraded-performance = 性能下降 175 | investigating = 调查中 176 | on = 上线 177 | systems = 系统 178 | systems-operational = 系统正常工作 179 | incidents = 事故 180 | resolved = 解决了 181 | no-incidents = 在90天中没有任何事故 182 | operational = 正常运行中 183 | [it] 184 | Major-outage = Interruzione grave 185 | Degraded-performance = Prestazioni degradate 186 | Investigating = Investigando 187 | major-outage = interruzione grave 188 | degraded-performance = prestazioni degradate 189 | investigating = investigando 190 | on = a 191 | systems = Sistemi 192 | systems-operational = Tutti i stistemi sono operativi. 193 | incidents = Incidenti 194 | resolved = risolto 195 | no-incidents = Nessun incidente nei precedenti 90 giorni. 196 | operational = operativo 197 | [fur] 198 | Major-outage = Interuzion grâf 199 | Degraded-performance = Prestazions degradades 200 | Investigating = Investigânt 201 | major-outage = interuzion grâf 202 | degraded-performance = prestazions degradades 203 | investigating = investigânt 204 | on = a 205 | systems = Sistems 206 | systems-operational = Ducj i sistems son operatîfs. 207 | incidents = Incidens 208 | resolved = justât 209 | no-incidents = Nessun incident intai precedents 90 zornadis. 210 | operational = operatîf 211 | [vn] 212 | Major-outage = Sự cố lớn 213 | Degraded-performance = Hiệu suất giảm 214 | Investigating = Đang điều tra nguyên nhân 215 | major-outage = Sự cố lớn 216 | degraded-performance = Hiệu suất giảm 217 | investigating = Đang điều tra nguyên nhân 218 | on = Đang hoạt động 219 | systems = Các hệ thống 220 | systems-operational = Các hệ thống đều hoạt động 221 | incidents = Sự cố 222 | resolved = Đã khắc phục 223 | no-incidents = Không có sự cố 224 | operational = Đang hoạt động 225 | -------------------------------------------------------------------------------- /statuspage/tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import, print_function, unicode_literals 3 | import unittest 4 | import traceback 5 | from datetime import datetime 6 | from unittest import TestCase 7 | from mock import patch, Mock 8 | from click.testing import CliRunner 9 | from statuspage import cli, update, upgrade, create, iter_systems, get_severity, SYSTEM_LABEL_COLOR 10 | from github import UnknownObjectException 11 | import codecs 12 | 13 | class CLITestCase(TestCase): 14 | 15 | def setUp(self): 16 | self.patcher = patch('statuspage.Github') 17 | self.gh = self.patcher.start() 18 | 19 | # setup mocked label 20 | self.label = Mock() 21 | self.label.color = "171717" 22 | self.label.name = "Website" 23 | 24 | self.label1 = Mock() 25 | self.label1.color = "171717" 26 | self.label1.name = "API" 27 | 28 | self.gh().get_user().get_repo().get_labels.return_value = [self.label, self.label1] 29 | 30 | # set up mocked issue 31 | self.issue = Mock() 32 | self.issue.created_at = datetime.now() 33 | self.issue.state = "open" 34 | self.issue_label = Mock() 35 | self.issue_label.color = "FF4D4D" 36 | self.issue_label.name = "major outage" 37 | self.issue.get_labels.return_value = [self.issue_label, self.label] 38 | self.issue.user.login = "some-dude" 39 | self.comment = Mock() 40 | self.comment.user.login = "some-dude" 41 | self.issue.get_comments.return_value = [self.comment, ] 42 | 43 | self.issue1 = Mock() 44 | self.issue1.created_at = datetime.now() 45 | self.issue1.state = "open" 46 | self.issue1.user.login = "some-dude" 47 | self.issue1.get_labels.return_value = [self.issue_label, self.label1] 48 | self.issue1.get_comments.return_value = [self.comment, ] 49 | 50 | self.gh().get_user().get_repo().get_issues.return_value = [self.issue, self.issue1] 51 | self.template = Mock() 52 | self.template.decoded_content = b"some foo" 53 | self.template.content = codecs.encode(b"some other foo", "base64") 54 | self.gh().get_user().get_repo().get_contents.return_value = self.template 55 | self.gh().get_organization().get_repo().get_contents.return_value = self.template 56 | 57 | self.collaborator = Mock() 58 | self.collaborator.login = "some-dude" 59 | 60 | self.gh().get_user().get_repo().get_collaborators.return_value = [self.collaborator,] 61 | self.gh().get_organization().get_repo().get_collaborators.return_value = [self.collaborator,] 62 | 63 | def tearDown(self): 64 | 65 | self.patcher.stop() 66 | 67 | @patch("statuspage.run_update") 68 | def test_create(self, run_update): 69 | 70 | label = Mock() 71 | self.gh().get_user().create_repo().get_labels.return_value = [label,] 72 | 73 | runner = CliRunner() 74 | result = runner.invoke( 75 | create, 76 | ["--name", "testrepo", "--token", "token", "--systems", "sys1,sys2"] 77 | ) 78 | 79 | self.assertEqual(result.exit_code, 0) 80 | 81 | self.gh.assert_called_with("token") 82 | 83 | @patch("statuspage.run_update") 84 | def test_create_org(self, run_update): 85 | 86 | runner = CliRunner() 87 | result = runner.invoke( 88 | create, 89 | ["--name", "testrepo", 90 | "--token", "token", 91 | "--systems", "sys1,sys2", 92 | "--org", "some"] 93 | ) 94 | 95 | self.assertEqual(result.exit_code, 0) 96 | 97 | self.gh.assert_called_with("token") 98 | self.gh().get_organization.assert_called_with("some") 99 | 100 | def test_update(self): 101 | """ 102 | runner = CliRunner() 103 | result = runner.invoke(update, ["--name", "testrepo", "--token", "token"]) 104 | self.assertEqual(result.exit_code, 0) 105 | 106 | self.gh.assert_called_with("token") 107 | self.gh().get_user().get_repo.assert_called_with(name="testrepo") 108 | self.gh().get_user().get_repo().get_labels.assert_called_once_with() 109 | """ 110 | 111 | def test_dont_update_when_nothing_changes(self): 112 | """ 113 | runner = CliRunner() 114 | self.template.content = codecs.encode(b"some foo", "base64") 115 | result = runner.invoke(update, ["--name", "testrepo", "--token", "token"]) 116 | self.assertEqual(result.exit_code, 0) 117 | self.gh.assert_called_with("token") 118 | self.gh().get_user().get_repo.assert_called_with(name="testrepo") 119 | self.gh().get_user().get_repo().get_labels.assert_called_once_with() 120 | self.gh().get_user().get_repo().update_file.assert_not_called() 121 | """ 122 | 123 | def test_update_org(self): 124 | 125 | runner = CliRunner() 126 | result = runner.invoke(update, ["--name", "testrepo", "--token", "token", "--org", "some"]) 127 | 128 | self.assertEqual(result.exit_code, 0) 129 | 130 | self.gh.assert_called_with("token") 131 | 132 | self.gh().get_organization().get_repo.assert_called_with(name="testrepo") 133 | self.gh().get_organization().get_repo().get_labels.assert_called_once_with() 134 | 135 | def test_update_index_does_not_exist(self): 136 | """ 137 | self.gh().get_user().get_repo().update_file.side_effect = UnknownObjectException(status=404, data="foo") 138 | 139 | runner = CliRunner() 140 | result = runner.invoke(update, ["--name", "testrepo", "--token", "token"]) 141 | self.assertEqual(result.exit_code, 0) 142 | 143 | self.gh.assert_called_with("token") 144 | 145 | self.gh().get_user().get_repo.assert_called_with(name="testrepo") 146 | self.gh().get_user().get_repo().get_labels.assert_called_once_with() 147 | self.gh().get_user().get_repo().create_file.assert_called_once_with( 148 | branch='gh-pages', 149 | content='some foo', 150 | message='initial', 151 | path='/index.html' 152 | ) 153 | """ 154 | 155 | def test_update_non_labeled_issue_not_displayed(self): 156 | """ 157 | self.issue.get_labels.return_value = [] 158 | 159 | runner = CliRunner() 160 | result = runner.invoke(update, ["--name", "testrepo", "--token", "token"]) 161 | self.assertEqual(result.exit_code, 0) 162 | 163 | # make sure that get_comments is not called for the first issue but for the second 164 | self.issue.get_comments.assert_not_called() 165 | self.issue1.get_comments.assert_called_once_with() 166 | """ 167 | 168 | def test_update_non_colaborator_issue_not_displayed(self): 169 | """ 170 | self.issue.user.login = "some-other-dude" 171 | 172 | runner = CliRunner() 173 | result = runner.invoke(update, ["--name", "testrepo", "--token", "token"]) 174 | self.assertEqual(result.exit_code, 0) 175 | 176 | # make sure that get_comments is not called for the first issue but for the second 177 | self.issue.get_comments.assert_not_called() 178 | self.issue1.get_comments.assert_called_once_with() 179 | """ 180 | 181 | def test_dont_upgrade_when_nothing_changes(self): 182 | runner = CliRunner() 183 | self.template.content = codecs.encode(b"some foo", "base64") 184 | result = runner.invoke(upgrade, ["--name", "testrepo", "--token", "token"]) 185 | self.assertEqual(result.exit_code, 0) 186 | self.gh.assert_called_with("token") 187 | self.gh().get_user().get_repo.assert_called_with(name="testrepo") 188 | self.gh().get_user().get_repo().update_file.assert_not_called() 189 | 190 | 191 | class UtilTestCase(TestCase): 192 | 193 | def test_iter_systems(self): 194 | label1 = Mock() 195 | label2 = Mock() 196 | label1.name = "website" 197 | label1.color = SYSTEM_LABEL_COLOR 198 | 199 | self.assertEqual( 200 | list(iter_systems([label1, label2])), 201 | ["website", ] 202 | ) 203 | 204 | self.assertEqual( 205 | list(iter_systems([label2])), 206 | [] 207 | ) 208 | 209 | def test_severity(self): 210 | label1 = Mock() 211 | label2 = Mock() 212 | label1.color = "FF4D4D" 213 | 214 | self.assertEqual( 215 | get_severity([label1, label2]), 216 | "major outage" 217 | ) 218 | 219 | label1.color = "000000" 220 | 221 | self.assertEqual( 222 | get_severity([label1, label2]), 223 | None 224 | ) 225 | 226 | 227 | 228 | 229 | if __name__ == '__main__': 230 | unittest.main() 231 | -------------------------------------------------------------------------------- /template/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jayfk/statuspage/d750aba1fbfd45c5c8168c2cb19eadd7f629ffc5/template/favicon.png -------------------------------------------------------------------------------- /template/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jayfk/statuspage/d750aba1fbfd45c5c8168c2cb19eadd7f629ffc5/template/logo.png --------------------------------------------------------------------------------