├── web ├── static │ └── .gitkeep ├── config │ ├── prod.env.js │ ├── dev.env.js │ └── index.js ├── build │ ├── logo.png │ ├── vue-loader.conf.js │ ├── build.js │ ├── check-versions.js │ ├── webpack.base.conf.js │ ├── utils.js │ ├── webpack.dev.conf.js │ └── webpack.prod.conf.js ├── src │ ├── assets │ │ └── logo.png │ ├── router │ │ └── index.js │ ├── .gitrepo │ ├── main.js │ ├── App.vue │ └── components │ │ └── PrRatioTable.vue ├── .eslintrc.js ├── README.md ├── index.html └── package.json ├── .gitignore ├── config.ini ├── genesis.json ├── Pipfile ├── .travis.yml ├── master-wallet ├── run.sh ├── LICENSE.md ├── Dockerfile ├── README.md ├── pr_metrics.py └── Pipfile.lock /web/static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/config/prod.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | NODE_ENV: '"production"' 4 | } 5 | -------------------------------------------------------------------------------- /web/build/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PicnicSupermarket/pr-leaderboard/HEAD/web/build/logo.png -------------------------------------------------------------------------------- /web/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PicnicSupermarket/pr-leaderboard/HEAD/web/src/assets/logo.png -------------------------------------------------------------------------------- /web/config/dev.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const merge = require('webpack-merge') 3 | const prodEnv = require('./prod.env') 4 | 5 | module.exports = merge(prodEnv, { 6 | NODE_ENV: '"development"' 7 | }) 8 | -------------------------------------------------------------------------------- /web/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": "plugin:vue/recommended", 3 | "plugins": [ 4 | "standard", 5 | "promise" 6 | ], 7 | "rules": { 8 | "semi": [2, "always"] 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist 4 | venv 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | 9 | # Editor directories and files 10 | .idea 11 | .vscode 12 | *.iml 13 | *.suo 14 | *.ntvs* 15 | *.njsproj 16 | *.sln 17 | -------------------------------------------------------------------------------- /config.ini: -------------------------------------------------------------------------------- 1 | [Github] 2 | AccessToken = github_access_token 3 | Organisation = PicnicSupermarket 4 | 5 | [OAuth] 6 | Secret = github_oauth_secret 7 | ReturnUrl = https://localhost:80/#/ 8 | ClientId = github_client_id 9 | 10 | [Ethereum] 11 | PaymentWalletAddress = bca7692b5d80548f7579ee14a6f7189e4f54013e 12 | PaymentWalletPassword = password 13 | -------------------------------------------------------------------------------- /web/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Router from 'vue-router'; 3 | import PrRatioTable from '../components/PrRatioTable'; 4 | 5 | Vue.use(Router); 6 | 7 | export default new Router({ 8 | routes: [ 9 | { 10 | path: '/', 11 | name: 'PR Ratio', 12 | component: PrRatioTable 13 | } 14 | ] 15 | }); 16 | -------------------------------------------------------------------------------- /genesis.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "chainId": 15, 4 | "homesteadBlock": 0, 5 | "eip155Block": 0, 6 | "eip158Block": 0 7 | }, 8 | "difficulty": "0x400", 9 | "gasLimit": "0x2100000", 10 | "alloc": { 11 | "bca7692b5d80548f7579ee14a6f7189e4f54013e": 12 | { "balance": "0x999999000000000000000000" } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | aiohttp = "==3.6.2" 10 | asyncio = "==3.4.3" 11 | requests = "==2.22.0" 12 | retry = "==0.9.2" 13 | web3 = "==5.4.0" 14 | aiohttp_cors = "==0.7.0" 15 | Faker = "==3.0.0" 16 | PyGithub = "==1.44.1" 17 | flake8 = "==3.7.9" 18 | black = "==19.10b0" 19 | 20 | [requires] 21 | python_version = "3.7" 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: generic 2 | dist: bionic 3 | before_install: 4 | - sudo apt-get update 5 | - sudo apt-get install python3-pip python3-setuptools 6 | - pip3 install --user pipenv 7 | install: 8 | - pipenv install --dev --deploy 9 | cache: pip 10 | before_script: 11 | - pipenv run flake8 --max-line-length=88 ./ 12 | - pipenv run black --check ./ 13 | script: 14 | - yarn --cwd web install 15 | - yarn --cwd web build 16 | - docker build . -t pr-game:latest 17 | -------------------------------------------------------------------------------- /master-wallet: -------------------------------------------------------------------------------- 1 | {"address":"bca7692b5d80548f7579ee14a6f7189e4f54013e","crypto":{"cipher":"aes-128-ctr","ciphertext":"cd498933319673f713ff19bea01fbfb84e2a57ad37457dd4c7d97de51fb370e9","cipherparams":{"iv":"04322d1b2f0c636fc163fc6ed0112e26"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"edcb3ac7061c04a63a03f1a8fc329e89ee5119172d0b978987e24be1bea08995"},"mac":"76fb25a82640baeba160e9eeb890e8804a2c6ec960830e6081600e8d3bcbfb0d"},"id":"a4938789-9826-476b-bdff-0fb0b9df3681","version":3} 2 | -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- 1 | # pr-ratio 2 | 3 | > PR ratio leaderboard. 4 | 5 | ## Build Setup 6 | 7 | ``` bash 8 | # install dependencies 9 | yarn install 10 | 11 | # build for production with minification 12 | yarn run build 13 | 14 | # build for production and view the bundle analyzer report 15 | yarn run build --report 16 | ``` 17 | 18 | For a detailed explanation on how things work, check out the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader). 19 | -------------------------------------------------------------------------------- /web/src/.gitrepo: -------------------------------------------------------------------------------- 1 | ; DO NOT EDIT (unless you know what you are doing) 2 | ; 3 | ; This subdirectory is a git "subrepo", and this file is maintained by the 4 | ; git-subrepo command. See https://github.com/git-commands/git-subrepo#readme 5 | ; 6 | [subrepo] 7 | remote = https://github.com/vuetifyjs/templates-common.git 8 | branch = subrepo/webpack-src 9 | commit = 090741fa8ba4da0c6f85db64eff64550704123e1 10 | parent = e05204fc0583a8c99f1963ce873eba1266838215 11 | method = merge 12 | cmdver = 0.4.0 13 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Hall of Fame 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /web/build/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const config = require('../config') 4 | const isProduction = process.env.NODE_ENV === 'production' 5 | const sourceMapEnabled = isProduction 6 | ? config.build.productionSourceMap 7 | : config.dev.cssSourceMap 8 | 9 | module.exports = { 10 | loaders: utils.cssLoaders({ 11 | sourceMap: sourceMapEnabled, 12 | extract: isProduction 13 | }), 14 | cssSourceMap: sourceMapEnabled, 15 | cacheBusting: config.dev.cacheBusting, 16 | transformToRequire: { 17 | video: ['src', 'poster'], 18 | source: 'src', 19 | img: 'src', 20 | image: 'xlink:href' 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | test "$(ls /pr-blockchain 2>/dev/null)" || \ 3 | mkdir /pr-blockchain/keystore && \ 4 | cp master-wallet /pr-blockchain/keystore && \ 5 | geth --identity "PrEthNode" --nodiscover --networkid 1999 --datadir /pr-blockchain init genesis.json && \ 6 | cp master-wallet /pr-blockchain/keystore 7 | geth --identity "PrEthNode" --ipcpath "$HOME/geth.ipc" \ 8 | --datadir pr-blockchain \ 9 | --nodiscover \ 10 | --nousb \ 11 | --syncmode "full" \ 12 | --mine --minerthreads 1 \ 13 | --networkid 1999 \ 14 | --rpc --rpcport "8545" --rpcaddr "0.0.0.0" --rpccorsdomain "*" --rpcapi="db,eth,net,web3,personal,web3" \ 15 | --etherbase '0xbca7692B5d80548f7579EE14A6F7189E4f54013e' & 16 | pipenv run python ./pr_metrics.py & 17 | pipenv run python -m http.server 8081 18 | -------------------------------------------------------------------------------- /web/src/main.js: -------------------------------------------------------------------------------- 1 | // The Vue build version to load with the `import` command 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias. 3 | import Vue from 'vue'; 4 | import App from './App'; 5 | import router from './router'; 6 | import Vuetify, { 7 | VApp, 8 | VCard, 9 | VDataTable, 10 | VDialog, 11 | VNavigationDrawer, 12 | VFooter, 13 | VList, 14 | VBtn, 15 | VIcon, 16 | VToolbar, 17 | VTooltip 18 | } from 'vuetify/lib'; 19 | import VGrid from 'vuetify/lib/components/VGrid'; 20 | import transitions from 'vuetify/lib/components/transitions'; 21 | import "vuetify/dist/vuetify.min.css"; 22 | 23 | Vue.use(Vuetify, { 24 | components: { 25 | VApp, 26 | VCard, 27 | VDataTable, 28 | VDialog, 29 | VNavigationDrawer, 30 | VFooter, 31 | VList, 32 | VBtn, 33 | VIcon, 34 | VGrid, 35 | VToolbar, 36 | VTooltip, 37 | transitions 38 | } 39 | }); 40 | 41 | Vue.config.productionTip = false; 42 | 43 | /* eslint-disable no-new */ 44 | new Vue({ 45 | el: '#app', 46 | router, 47 | components: {App}, 48 | template: '' 49 | }); 50 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Picnic Technologies BV 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 | -------------------------------------------------------------------------------- /web/build/build.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | require('./check-versions')() 3 | 4 | process.env.NODE_ENV = 'production' 5 | 6 | const ora = require('ora') 7 | const rm = require('rimraf') 8 | const path = require('path') 9 | const chalk = require('chalk') 10 | const webpack = require('webpack') 11 | const config = require('../config') 12 | const webpackConfig = require('./webpack.prod.conf') 13 | 14 | const spinner = ora('building for production...') 15 | spinner.start() 16 | 17 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { 18 | if (err) throw err 19 | webpack(webpackConfig, (err, stats) => { 20 | spinner.stop() 21 | if (err) throw err 22 | process.stdout.write(stats.toString({ 23 | colors: true, 24 | modules: false, 25 | children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build. 26 | chunks: false, 27 | chunkModules: false 28 | }) + '\n\n') 29 | 30 | if (stats.hasErrors()) { 31 | console.log(chalk.red(' Build failed with errors.\n')) 32 | process.exit(1) 33 | } 34 | 35 | console.log(chalk.cyan(' Build complete.\n')) 36 | console.log(chalk.yellow( 37 | ' Tip: built files are meant to be served over an HTTP server.\n' + 38 | ' Opening index.html over file:// won\'t work.\n' 39 | )) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:disco 2 | 3 | ENV PATH=/usr/lib/go-1.9/bin:$PATH 4 | 5 | # Install requirements for setting up apt repositories 6 | RUN set -eu pipefail \ 7 | && apt-get update \ 8 | && apt-get install -y curl software-properties-common \ 9 | apt-transport-https 10 | 11 | # Install all required apt repositories 12 | RUN set -eu pipefail \ 13 | && add-apt-repository -y ppa:ethereum/ethereum \ 14 | && curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ 15 | && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list \ 16 | && curl -sL https://deb.nodesource.com/setup_8.x | bash - 17 | 18 | # Install dependencies 19 | RUN set -eu pipefail \ 20 | && apt-get update \ 21 | && apt-get install -y \ 22 | ethereum \ 23 | python3 \ 24 | python3-dev \ 25 | python3-pip \ 26 | nodejs \ 27 | yarn 28 | 29 | # Install Python dependencies 30 | ENV LC_ALL=C.UTF-8 31 | ENV LANG=C.UTF-8 32 | ADD Pipfile.lock Pipfile config.ini ./ 33 | RUN set -eu pipefail \ 34 | && python3 -m pip install --upgrade pip \ 35 | && python3 -m pip install pipenv \ 36 | && python3 -m pipenv install --deploy 37 | 38 | # Install Javascript dependencies 39 | ADD web/dist/ ./ 40 | 41 | # Add the rest of the source 42 | ADD pr_metrics.py run.sh genesis.json master-wallet ./ 43 | 44 | VOLUME ["/pr-blockchain", "/data"] 45 | EXPOSE 9999 8081 30303 8545 46 | ENTRYPOINT ["sh", "run.sh"] 47 | -------------------------------------------------------------------------------- /web/build/check-versions.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const chalk = require('chalk') 3 | const semver = require('semver') 4 | const packageConfig = require('../package.json') 5 | const shell = require('shelljs') 6 | 7 | function exec (cmd) { 8 | return require('child_process').execSync(cmd).toString().trim() 9 | } 10 | 11 | const versionRequirements = [ 12 | { 13 | name: 'node', 14 | currentVersion: semver.clean(process.version), 15 | versionRequirement: packageConfig.engines.node 16 | } 17 | ] 18 | 19 | if (shell.which('npm')) { 20 | versionRequirements.push({ 21 | name: 'npm', 22 | currentVersion: exec('npm --version'), 23 | versionRequirement: packageConfig.engines.npm 24 | }) 25 | } 26 | 27 | module.exports = function () { 28 | const warnings = [] 29 | 30 | for (let i = 0; i < versionRequirements.length; i++) { 31 | const mod = versionRequirements[i] 32 | 33 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 34 | warnings.push(mod.name + ': ' + 35 | chalk.red(mod.currentVersion) + ' should be ' + 36 | chalk.green(mod.versionRequirement) 37 | ) 38 | } 39 | } 40 | 41 | if (warnings.length) { 42 | console.log('') 43 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 44 | console.log() 45 | 46 | for (let i = 0; i < warnings.length; i++) { 47 | const warning = warnings[i] 48 | console.log(' ' + warning) 49 | } 50 | 51 | console.log() 52 | process.exit(1) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /web/src/App.vue: -------------------------------------------------------------------------------- 1 | 76 | 77 | 93 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pr-ratio", 3 | "version": "1.0.0", 4 | "description": "PR ratio leaderboard.", 5 | "private": true, 6 | "scripts": { 7 | "start": "npm run dev", 8 | "lint": "eslint --ext .js,.vue src", 9 | "build": "node build/build.js" 10 | }, 11 | "dependencies": { 12 | "axios": "^0.18.0", 13 | "vue": "^2.5.2", 14 | "vue-router": "^3.0.1", 15 | "vuetify": "^1.0.0" 16 | }, 17 | "devDependencies": { 18 | "autoprefixer": "^7.1.2", 19 | "babel-core": "^6.22.1", 20 | "babel-eslint": "^7.1.1", 21 | "babel-helper-vue-jsx-merge-props": "^2.0.3", 22 | "babel-loader": "^7.1.1", 23 | "babel-plugin-syntax-jsx": "^6.18.0", 24 | "babel-plugin-transform-imports": "^1.4.1", 25 | "babel-plugin-transform-runtime": "^6.22.0", 26 | "babel-plugin-transform-vue-jsx": "^3.5.0", 27 | "babel-preset-env": "^1.3.2", 28 | "babel-preset-stage-2": "^6.22.0", 29 | "chalk": "^2.0.1", 30 | "copy-webpack-plugin": "^4.0.1", 31 | "css-loader": "^0.28.0", 32 | "eslint": "^3.19.0", 33 | "eslint-config-standard": "^10.2.1", 34 | "eslint-friendly-formatter": "^3.0.0", 35 | "eslint-loader": "^1.7.1", 36 | "eslint-plugin-html": "^3.0.0", 37 | "eslint-plugin-import": "^2.7.0", 38 | "eslint-plugin-node": "^5.2.0", 39 | "eslint-plugin-promise": "^3.4.0", 40 | "eslint-plugin-standard": "^3.0.1", 41 | "eslint-plugin-vue": "^4.0.0", 42 | "extract-text-webpack-plugin": "^3.0.0", 43 | "file-loader": "^1.1.4", 44 | "friendly-errors-webpack-plugin": "^1.6.1", 45 | "html-webpack-plugin": "^2.30.1", 46 | "node-notifier": "^5.1.2", 47 | "optimize-css-assets-webpack-plugin": "^3.2.0", 48 | "ora": "^1.2.0", 49 | "portfinder": "^1.0.13", 50 | "postcss-import": "^11.0.0", 51 | "postcss-loader": "^2.0.8", 52 | "postcss-url": "^7.2.1", 53 | "rimraf": "^2.6.0", 54 | "semver": "^5.3.0", 55 | "shelljs": "^0.7.6", 56 | "stylus": "^0.54.5", 57 | "stylus-loader": "^3.0.1", 58 | "uglifyjs-webpack-plugin": "^1.1.1", 59 | "url-loader": "^0.5.8", 60 | "vue-loader": "^13.3.0", 61 | "vue-style-loader": "^3.0.1", 62 | "vue-template-compiler": "^2.5.2", 63 | "webpack": "^3.6.0", 64 | "webpack-bundle-analyzer": "^2.9.0", 65 | "webpack-dev-server": "^2.9.1", 66 | "webpack-merge": "^4.1.0" 67 | }, 68 | "engines": { 69 | "node": ">= 6.0.0", 70 | "npm": ">= 3.0.0" 71 | }, 72 | "browserslist": [ 73 | "> 1%", 74 | "last 2 versions", 75 | "not ie <= 8" 76 | ] 77 | } 78 | -------------------------------------------------------------------------------- /web/config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // Template version: 1.2.8 3 | // see http://vuejs-templates.github.io/webpack for documentation. 4 | 5 | const path = require('path') 6 | 7 | module.exports = { 8 | dev: { 9 | 10 | // Paths 11 | assetsSubDirectory: 'static', 12 | assetsPublicPath: '/', 13 | proxyTable: {}, 14 | 15 | // Various Dev Server settings 16 | host: 'localhost', // can be overwritten by process.env.HOST 17 | port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined 18 | autoOpenBrowser: false, 19 | errorOverlay: true, 20 | notifyOnErrors: true, 21 | poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions- 22 | 23 | // Use Eslint Loader? 24 | // If true, your code will be linted during bundling and 25 | // linting errors and warnings will be shown in the console. 26 | useEslint: true, 27 | // If true, eslint errors and warnings will also be shown in the error overlay 28 | // in the browser. 29 | showEslintErrorsInOverlay: false, 30 | 31 | /** 32 | * Source Maps 33 | */ 34 | 35 | // https://webpack.js.org/configuration/devtool/#development 36 | devtool: 'cheap-module-eval-source-map', 37 | 38 | // If you have problems debugging vue-files in devtools, 39 | // set this to false - it *may* help 40 | // https://vue-loader.vuejs.org/en/options.html#cachebusting 41 | cacheBusting: true, 42 | 43 | cssSourceMap: true, 44 | }, 45 | 46 | build: { 47 | // Template for index.html 48 | index: path.resolve(__dirname, '../dist/index.html'), 49 | 50 | // Paths 51 | assetsRoot: path.resolve(__dirname, '../dist'), 52 | assetsSubDirectory: 'static', 53 | assetsPublicPath: '/', 54 | 55 | /** 56 | * Source Maps 57 | */ 58 | 59 | productionSourceMap: true, 60 | // https://webpack.js.org/configuration/devtool/#production 61 | devtool: '#source-map', 62 | 63 | // Gzip off by default as many popular static hosts such as 64 | // Surge or Netlify already gzip all static assets for you. 65 | // Before setting to `true`, make sure to: 66 | // npm install --save-dev compression-webpack-plugin 67 | productionGzip: false, 68 | productionGzipExtensions: ['js', 'css'], 69 | 70 | // Run the build command with an extra argument to 71 | // View the bundle analyzer report after build finishes: 72 | // `npm run build --report` 73 | // Set to `true` or `false` to always turn it on or off 74 | bundleAnalyzerReport: process.env.npm_config_report 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /web/build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const path = require('path'); 3 | const utils = require('./utils'); 4 | const config = require('../config'); 5 | const vueLoaderConfig = require('./vue-loader.conf'); 6 | 7 | function resolve (dir) { 8 | return path.join(__dirname, '..', dir); 9 | } 10 | 11 | const createLintingRule = () => ({ 12 | test: /\.(js|vue)$/, 13 | loader: 'eslint-loader', 14 | enforce: 'pre', 15 | include: [resolve('src'), resolve('test')], 16 | options: { 17 | formatter: require('eslint-friendly-formatter'), 18 | emitWarning: !config.dev.showEslintErrorsInOverlay 19 | } 20 | }); 21 | 22 | module.exports = { 23 | context: path.resolve(__dirname, '../'), 24 | entry: { 25 | app: './src/main.js' 26 | }, 27 | output: { 28 | path: config.build.assetsRoot, 29 | filename: '[name].js', 30 | publicPath: process.env.NODE_ENV === 'production' 31 | ? config.build.assetsPublicPath 32 | : config.dev.assetsPublicPath 33 | }, 34 | resolve: { 35 | extensions: ['.js', '.vue', '.json'], 36 | alias: { 37 | 'vue$': 'vue/dist/vue.esm.js', 38 | '@': resolve('src'), 39 | } 40 | }, 41 | module: { 42 | rules: [ 43 | ...(config.dev.useEslint ? [createLintingRule()] : []), 44 | { 45 | test: /\.vue$/, 46 | loader: 'vue-loader', 47 | options: vueLoaderConfig 48 | }, 49 | { 50 | test: /\.js$/, 51 | loader: 'babel-loader', 52 | include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')] 53 | }, 54 | { 55 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 56 | loader: 'url-loader', 57 | options: { 58 | limit: 10000, 59 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 60 | } 61 | }, 62 | { 63 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, 64 | loader: 'url-loader', 65 | options: { 66 | limit: 10000, 67 | name: utils.assetsPath('media/[name].[hash:7].[ext]') 68 | } 69 | }, 70 | { 71 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 72 | loader: 'url-loader', 73 | options: { 74 | limit: 10000, 75 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]') 76 | } 77 | } 78 | ] 79 | }, 80 | node: { 81 | // prevent webpack from injecting useless setImmediate polyfill because Vue 82 | // source contains it (although only uses it if it's native). 83 | setImmediate: false, 84 | // prevent webpack from injecting mocks to Node native modules 85 | // that does not make sense for the client 86 | dgram: 'empty', 87 | fs: 'empty', 88 | net: 'empty', 89 | tls: 'empty', 90 | child_process: 'empty' 91 | } 92 | }; 93 | -------------------------------------------------------------------------------- /web/build/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const config = require('../config') 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 5 | const packageConfig = require('../package.json') 6 | 7 | exports.assetsPath = function (_path) { 8 | const assetsSubDirectory = process.env.NODE_ENV === 'production' 9 | ? config.build.assetsSubDirectory 10 | : config.dev.assetsSubDirectory 11 | 12 | return path.posix.join(assetsSubDirectory, _path) 13 | } 14 | 15 | exports.cssLoaders = function (options) { 16 | options = options || {} 17 | 18 | const cssLoader = { 19 | loader: 'css-loader', 20 | options: { 21 | sourceMap: options.sourceMap 22 | } 23 | } 24 | 25 | const postcssLoader = { 26 | loader: 'postcss-loader', 27 | options: { 28 | sourceMap: options.sourceMap, 29 | plugins: () => [require('autoprefixer')] 30 | } 31 | } 32 | 33 | // generate loader string to be used with extract text plugin 34 | function generateLoaders (loader, loaderOptions) { 35 | const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader] 36 | 37 | if (loader) { 38 | loaders.push({ 39 | loader: loader + '-loader', 40 | options: Object.assign({}, loaderOptions, { 41 | sourceMap: options.sourceMap 42 | }) 43 | }) 44 | } 45 | 46 | // Extract CSS when that option is specified 47 | // (which is the case during production build) 48 | if (options.extract) { 49 | return ExtractTextPlugin.extract({ 50 | use: loaders, 51 | fallback: 'vue-style-loader' 52 | }) 53 | } else { 54 | return ['vue-style-loader'].concat(loaders) 55 | } 56 | } 57 | 58 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html 59 | return { 60 | css: generateLoaders(), 61 | postcss: generateLoaders(), 62 | less: generateLoaders('less'), 63 | sass: generateLoaders('sass', {indentedSyntax: true }), 64 | scss: generateLoaders('sass'), 65 | stylus: generateLoaders('stylus'), 66 | styl: generateLoaders('stylus') 67 | } 68 | } 69 | 70 | // Generate loaders for standalone style files (outside of .vue) 71 | exports.styleLoaders = function (options) { 72 | const output = [] 73 | const loaders = exports.cssLoaders(options) 74 | 75 | for (const extension in loaders) { 76 | const loader = loaders[extension] 77 | output.push({ 78 | test: new RegExp('\\.' + extension + '$'), 79 | use: loader 80 | }) 81 | } 82 | 83 | return output 84 | } 85 | 86 | exports.createNotifierCallback = () => { 87 | const notifier = require('node-notifier') 88 | 89 | return (severity, errors) => { 90 | if (severity !== 'error') return 91 | 92 | const error = errors[0] 93 | const filename = error.file && error.file.split('!').pop() 94 | 95 | notifier.notify({ 96 | title: packageConfig.name, 97 | message: severity + ': ' + error.name, 98 | subtitle: filename || '', 99 | icon: path.join(__dirname, 'logo.png') 100 | }) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PR Game 2 | A leaderboard to rank developers based on their contributions to code reviewing, coupled with an Ethereum based 3 | cryptocurrency reward scheme. 4 | 5 | ![Dashboard](https://miro.medium.com/max/2680/1*aT1einIrArKy8U4P7FHw1A.png) 6 | 7 | ## About 8 | The PR Game is a gamification of the code review process. We all love writing code, and some of us love reviewing code. 9 | But for a large number of us, code reviewing isn't how we necessarily want to spend our time but a necessity it is! 10 | The PR Game adds an extra incentive to code reviewing in the form of a cryptocurrency where developers mine coins based 11 | on their contributions to code reviews. 12 | 13 | The default score is simply the ratio between the number of PRs that a developer has reviewed versus the number of PRs 14 | that they have authored. This score can easily be adopted in the `pr_metrics.py` to take into account other PR related 15 | metrics such as number or length of comments in the PR, or decaying PR lead time for example. The possibilities are 16 | numerous, whatever KPI it is you are looking to optimise. 17 | 18 | Checkout the original [blog post](https://blog.picnic.nl/crypto-incentives-for-code-reviews-71a0be53d130). 19 | 20 | ## Setup 21 | 22 | ### Configuration 23 | To setup the PR Game for your organisation there are a few properties that need to be configured in the `config.ini` 24 | file. 25 | 26 | #### GitHub 27 | To scrape pull request information from your organisations GitHub you need to 28 | [create a Github app](https://developer.github.com/apps/building-github-apps/creating-a-github-app/) organisation app 29 | then you need to configure the `AccessToken` and `Organisation` name in the `config.ini` file accordingly. 30 | 31 | The dashboard is open to all users and authentication is up to you as the owner. Users who have a score below 1 aren't 32 | listed and their names are randomised. To allow for users in your organisation to sign into the dashboard to reveal 33 | their position then you need to setup and 34 | [configure your GitHub app](https://developer.github.com/apps/building-github-apps/creating-a-github-app/) provide an 35 | OAuth flow. Once this has been done you can configure the `Secret`, `ReturnUrl` and `ClientId` accordingly in the 36 | `config.ini`. 37 | 38 | #### Ethereum 39 | The default `PaymentWalletAddress` in the `config.ini` is configured to be the same as which the Ethereum node is initialised. 40 | This account is gifted a large quantity of ETH and is responsible for paying out the developers periodically. The default 41 | developer account password is configured in `PaymentWalletPassword` and can be changed and updated by them via their wallet app. 42 | 43 | ### Deploy Service 44 | 45 | ```bash 46 | # Install JS dependencies 47 | yarn --cwd web install 48 | 49 | # Build JS 50 | yarn --cwd web build 51 | 52 | # Build Docker image 53 | docker build . -t pr-game:latest 54 | 55 | # Run Docker container 56 | docker run -d -p 8081:8081 -p 9999:9999 -p 8545:8545 pr-game:latest 57 | ``` 58 | 59 | ### Run wallet 60 | 61 | Download and install [Mist Wallet](https://github.com/ethereum/mist/releases). Then run: 62 | 63 | ```bash 64 | mist --rpc http://node-ip:8545 65 | ``` 66 | -------------------------------------------------------------------------------- /web/build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const webpack = require('webpack') 4 | const config = require('../config') 5 | const merge = require('webpack-merge') 6 | const path = require('path') 7 | const baseWebpackConfig = require('./webpack.base.conf') 8 | const CopyWebpackPlugin = require('copy-webpack-plugin') 9 | const HtmlWebpackPlugin = require('html-webpack-plugin') 10 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 11 | const portfinder = require('portfinder') 12 | 13 | const HOST = process.env.HOST 14 | const PORT = process.env.PORT && Number(process.env.PORT) 15 | 16 | const devWebpackConfig = merge(baseWebpackConfig, { 17 | module: { 18 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: false }) 19 | }, 20 | // cheap-module-eval-source-map is faster for development 21 | devtool: config.dev.devtool, 22 | 23 | // these devServer options should be customized in /config/index.js 24 | devServer: { 25 | clientLogLevel: 'warning', 26 | historyApiFallback: { 27 | rewrites: [ 28 | { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') }, 29 | ], 30 | }, 31 | hot: true, 32 | contentBase: false, // since we use CopyWebpackPlugin. 33 | compress: true, 34 | host: HOST || config.dev.host, 35 | port: PORT || config.dev.port, 36 | open: config.dev.autoOpenBrowser, 37 | overlay: config.dev.errorOverlay 38 | ? { warnings: false, errors: true } 39 | : false, 40 | publicPath: config.dev.assetsPublicPath, 41 | proxy: config.dev.proxyTable, 42 | quiet: true, // necessary for FriendlyErrorsPlugin 43 | watchOptions: { 44 | poll: config.dev.poll, 45 | } 46 | }, 47 | plugins: [ 48 | new webpack.DefinePlugin({ 49 | 'process.env': require('../config/dev.env') 50 | }), 51 | new webpack.HotModuleReplacementPlugin(), 52 | new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update. 53 | new webpack.NoEmitOnErrorsPlugin(), 54 | // https://github.com/ampedandwired/html-webpack-plugin 55 | new HtmlWebpackPlugin({ 56 | filename: 'index.html', 57 | template: 'index.html', 58 | inject: true 59 | }), 60 | // copy custom static assets 61 | new CopyWebpackPlugin([ 62 | { 63 | from: path.resolve(__dirname, '../static'), 64 | to: config.dev.assetsSubDirectory, 65 | ignore: ['.*'] 66 | } 67 | ]) 68 | ] 69 | }) 70 | 71 | module.exports = new Promise((resolve, reject) => { 72 | portfinder.basePort = process.env.PORT || config.dev.port 73 | portfinder.getPort((err, port) => { 74 | if (err) { 75 | reject(err) 76 | } else { 77 | // publish the new Port, necessary for e2e tests 78 | process.env.PORT = port 79 | // add port to devServer config 80 | devWebpackConfig.devServer.port = port 81 | 82 | // Add FriendlyErrorsPlugin 83 | devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({ 84 | compilationSuccessInfo: { 85 | messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`], 86 | }, 87 | onErrors: config.dev.notifyOnErrors 88 | ? utils.createNotifierCallback() 89 | : undefined 90 | })) 91 | 92 | resolve(devWebpackConfig) 93 | } 94 | }) 95 | }) 96 | -------------------------------------------------------------------------------- /web/build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const utils = require('./utils') 4 | const webpack = require('webpack') 5 | const config = require('../config') 6 | const merge = require('webpack-merge') 7 | const baseWebpackConfig = require('./webpack.base.conf') 8 | const CopyWebpackPlugin = require('copy-webpack-plugin') 9 | const HtmlWebpackPlugin = require('html-webpack-plugin') 10 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 11 | const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') 12 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin') 13 | 14 | const env = require('../config/prod.env') 15 | 16 | const webpackConfig = merge(baseWebpackConfig, { 17 | module: { 18 | rules: utils.styleLoaders({ 19 | sourceMap: config.build.productionSourceMap, 20 | extract: true, 21 | usePostCSS: true 22 | }) 23 | }, 24 | devtool: config.build.productionSourceMap ? config.build.devtool : false, 25 | output: { 26 | path: config.build.assetsRoot, 27 | filename: utils.assetsPath('js/[name].[chunkhash].js'), 28 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') 29 | }, 30 | plugins: [ 31 | // http://vuejs.github.io/vue-loader/en/workflow/production.html 32 | new webpack.DefinePlugin({ 33 | 'process.env': env 34 | }), 35 | new UglifyJsPlugin({ 36 | uglifyOptions: { 37 | compress: { 38 | warnings: false 39 | } 40 | }, 41 | sourceMap: config.build.productionSourceMap, 42 | parallel: true 43 | }), 44 | // extract css into its own file 45 | new ExtractTextPlugin({ 46 | filename: utils.assetsPath('css/[name].[contenthash].css'), 47 | // Setting the following option to `false` will not extract CSS from codesplit chunks. 48 | // Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack. 49 | // It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`, 50 | // increasing file size: https://github.com/vuejs-templates/webpack/issues/1110 51 | allChunks: true, 52 | }), 53 | // Compress extracted CSS. We are using this plugin so that possible 54 | // duplicated CSS from different components can be deduped. 55 | new OptimizeCSSPlugin({ 56 | cssProcessorOptions: config.build.productionSourceMap 57 | ? { safe: true, map: { inline: false } } 58 | : { safe: true } 59 | }), 60 | // generate dist index.html with correct asset hash for caching. 61 | // you can customize output by editing /index.html 62 | // see https://github.com/ampedandwired/html-webpack-plugin 63 | new HtmlWebpackPlugin({ 64 | filename: config.build.index, 65 | template: 'index.html', 66 | inject: true, 67 | minify: { 68 | removeComments: true, 69 | collapseWhitespace: true, 70 | removeAttributeQuotes: true 71 | // more options: 72 | // https://github.com/kangax/html-minifier#options-quick-reference 73 | }, 74 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin 75 | chunksSortMode: 'dependency' 76 | }), 77 | // keep module.id stable when vendor modules does not change 78 | new webpack.HashedModuleIdsPlugin(), 79 | // enable scope hoisting 80 | new webpack.optimize.ModuleConcatenationPlugin(), 81 | // split vendor js into its own file 82 | new webpack.optimize.CommonsChunkPlugin({ 83 | name: 'vendor', 84 | minChunks (module) { 85 | // any required modules inside node_modules are extracted to vendor 86 | return ( 87 | module.resource && 88 | /\.js$/.test(module.resource) && 89 | module.resource.indexOf( 90 | path.join(__dirname, '../node_modules') 91 | ) === 0 92 | ) 93 | } 94 | }), 95 | // extract webpack runtime and module manifest to its own file in order to 96 | // prevent vendor hash from being updated whenever app bundle is updated 97 | new webpack.optimize.CommonsChunkPlugin({ 98 | name: 'manifest', 99 | minChunks: Infinity 100 | }), 101 | // This instance extracts shared chunks from code splitted chunks and bundles them 102 | // in a separate chunk, similar to the vendor chunk 103 | // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk 104 | new webpack.optimize.CommonsChunkPlugin({ 105 | name: 'app', 106 | async: 'vendor-async', 107 | children: true, 108 | minChunks: 3 109 | }), 110 | 111 | // copy custom static assets 112 | new CopyWebpackPlugin([ 113 | { 114 | from: path.resolve(__dirname, '../static'), 115 | to: config.build.assetsSubDirectory, 116 | ignore: ['.*'] 117 | } 118 | ]) 119 | ] 120 | }) 121 | 122 | if (config.build.productionGzip) { 123 | const CompressionWebpackPlugin = require('compression-webpack-plugin') 124 | 125 | webpackConfig.plugins.push( 126 | new CompressionWebpackPlugin({ 127 | asset: '[path].gz[query]', 128 | algorithm: 'gzip', 129 | test: new RegExp( 130 | '\\.(' + 131 | config.build.productionGzipExtensions.join('|') + 132 | ')$' 133 | ), 134 | threshold: 10240, 135 | minRatio: 0.8 136 | }) 137 | ) 138 | } 139 | 140 | if (config.build.bundleAnalyzerReport) { 141 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 142 | webpackConfig.plugins.push(new BundleAnalyzerPlugin()) 143 | } 144 | 145 | module.exports = webpackConfig 146 | -------------------------------------------------------------------------------- /pr_metrics.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import configparser 3 | import logging 4 | import math 5 | import pickle 6 | import sys 7 | 8 | import requests 9 | 10 | import aiohttp_cors 11 | from aiohttp import web 12 | from faker import Faker 13 | from github import BadCredentialsException, Github, RateLimitExceededException 14 | from retry import retry 15 | from web3 import IPCProvider, Web3 16 | 17 | LOGGER = logging.getLogger(__name__) 18 | 19 | 20 | class Auth(web.View): 21 | async def get(self): 22 | code = self.request.rel_url.query.get("code") 23 | response = requests.post( 24 | "https://github.com/login/oauth/access_token", 25 | json={ 26 | "client_id": self.request.app["github_client_id"], 27 | "client_secret": self.request.app["github_client_secret"], 28 | "code": code, 29 | }, 30 | ) 31 | access_token = response.json()["access_token"] 32 | r = web.HTTPFound(self.request.app["github_return_url"]) 33 | r.set_cookie("X-Access-Token", access_token) 34 | return r 35 | 36 | 37 | class Health(web.View): 38 | async def get(self): 39 | return web.Response(text="OK!") 40 | 41 | 42 | class Metrics(web.View): 43 | async def get(self): 44 | token = self.request.headers.get("X-Access-Token") 45 | w3 = Web3(IPCProvider(ipc_path=self.request.app["ipc"])) 46 | github_name = "" 47 | if token: 48 | try: 49 | github = Github(token) 50 | user = github.get_user() 51 | github_name = user.name or user.login 52 | except BadCredentialsException: 53 | return web.Response(status=401, text="401 Bad GitHub credentials") 54 | 55 | table_history = [] 56 | for name in sorted( 57 | self.request.app["scores"], key=self.request.app["scores"].get, reverse=True 58 | ): 59 | is_owner = False 60 | final_name = name 61 | try: 62 | address = self.request.app["accounts"][name] 63 | except KeyError: 64 | LOGGER.warning("No account found for name %s", name) 65 | address = None 66 | amount = 0.0 67 | if address: 68 | amount = w3.fromWei(w3.eth.getBalance(address), "ether") 69 | if (self.request.app["scores"].get(name) < 1.0) and github_name != name: 70 | final_name = self.request.app["fake"].name() 71 | elif github_name == name: 72 | final_name = name 73 | is_owner = True 74 | 75 | table_history.append( 76 | { 77 | "name": final_name, 78 | "ratio": f"{self.request.app['scores'].get(name):0.2f}", 79 | "address": address or "n/a", 80 | "coin": f"{amount:0.6f}", 81 | "is_owner": is_owner, 82 | } 83 | ) 84 | 85 | return web.json_response(table_history) 86 | 87 | 88 | def load_data(file): 89 | try: 90 | with open(file, "rb") as f: 91 | return pickle.load(f) 92 | except FileNotFoundError: 93 | return {} 94 | 95 | 96 | def update_accounts(app, accounts): 97 | with open(app["accounts_data_path"], "wb+") as f: 98 | pickle.dump(accounts, f) 99 | 100 | 101 | def update_scores(app, scores): 102 | with open(app["scores_data_path"], "wb+") as f: 103 | pickle.dump(scores, f) 104 | 105 | 106 | class App: 107 | ROUTES = [ 108 | web.get(r"/api/health", Health), 109 | web.get(r"/api/authenticate", Auth), 110 | web.get(r"/api/metrics", Metrics), 111 | ] 112 | 113 | def __init__(self, config): 114 | self.config = config 115 | 116 | def get_app(self) -> web.Application: 117 | app = web.Application() 118 | app.add_routes(self.ROUTES) 119 | 120 | app.cleanup_ctx.extend( 121 | [ 122 | self._init, 123 | self._init_config_ctx, 124 | self._init_github_ctx, 125 | self._init_scores_and_accounts_ctx, 126 | ] 127 | ) 128 | 129 | # Configure default CORS settings. 130 | cors = aiohttp_cors.setup( 131 | app, 132 | defaults={ 133 | "*": aiohttp_cors.ResourceOptions( 134 | allow_credentials=True, expose_headers="*", allow_headers="*", 135 | ) 136 | }, 137 | ) 138 | # Configure CORS on all routes. 139 | for route in list(app.router.routes()): 140 | cors.add(route) 141 | 142 | return app 143 | 144 | async def _init(self, app): 145 | # Connection to Geth 146 | app["ipc"] = "/root/geth.ipc" 147 | # Region for anonymous names 148 | app["fake"] = Faker("en_GB") 149 | yield 150 | 151 | async def _init_config_ctx(self, app): 152 | LOGGER.info("Initialisation of config") 153 | # Configuration parsing 154 | config = configparser.ConfigParser() 155 | app["cfg"] = config.read("./config.ini") 156 | 157 | github = config["Github"] 158 | oauth = config["OAuth"] 159 | ethereum = config["Ethereum"] 160 | 161 | app["organisation"] = github["Organisation"] 162 | app["token"] = github["AccessToken"] 163 | 164 | app["main"] = ethereum["PaymentWalletAddress"] 165 | app["default_wallet_password"] = ethereum["PaymentWalletPassword"] 166 | 167 | app["github_return_url"] = oauth["ReturnUrl"] 168 | app["github_client_secret"] = oauth["Secret"] 169 | app["github_client_id"] = oauth["ClientId"] 170 | 171 | yield 172 | config.clear() 173 | LOGGER.info("Cleanup of config") 174 | 175 | async def _init_github_ctx(self, app): 176 | LOGGER.info("Initialisation of GitHub connection") 177 | # Github API 178 | github = Github(app["token"]) 179 | app["github"] = github 180 | app["org"] = github.get_organization(app["organisation"]) 181 | yield 182 | 183 | async def _init_scores_and_accounts_ctx(self, app): 184 | LOGGER.info("Initialisation of scores and accounts") 185 | app["accounts_data_path"] = "data/accounts.data" 186 | app["scores_data_path"] = "data/scores.data" 187 | app["accounts"] = load_data(app["accounts_data_path"]) 188 | app["scores"] = load_data(app["scores_data_path"]) 189 | 190 | yield 191 | update_scores(app, app["scores"]) 192 | update_accounts(app, app["accounts"]) 193 | LOGGER.info("Flushing of data") 194 | 195 | 196 | def main(): 197 | logging.basicConfig( 198 | stream=sys.stdout, 199 | level=logging.WARNING, 200 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", 201 | ) 202 | logging.getLogger(__name__).setLevel(logging.DEBUG) 203 | 204 | app = App(configparser.ConfigParser()).get_app() 205 | app.on_startup.append(startup_tasks) 206 | web.run_app(app, port=9999) 207 | 208 | 209 | async def startup_tasks(app): 210 | asyncio.create_task(score_calculation_loop(app)) 211 | asyncio.create_task(pay_out_loop(app)) 212 | 213 | 214 | async def pay_out_loop(app): 215 | loop = asyncio.get_running_loop() 216 | while True: 217 | await loop.run_in_executor(None, pay_out, app) 218 | await asyncio.sleep(600) 219 | 220 | 221 | async def score_calculation_loop(app): 222 | loop = asyncio.get_running_loop() 223 | while True: 224 | await loop.run_in_executor(None, calculate_scores, app) 225 | 226 | 227 | def pay_out(app): 228 | w3 = Web3(IPCProvider(ipc_path=app["ipc"])) 229 | LOGGER.info("Paying out developers") 230 | for user in app["scores"]: 231 | address = app["accounts"].get(user) 232 | if address is None: 233 | address = w3.geth.personal.newAccount(app["default_wallet_password"]) 234 | LOGGER.info(f"Created a new account for {user} at {address}") 235 | app["accounts"][user] = address 236 | ratio = app["scores"].get(user) 237 | if ratio >= 1.0: 238 | amount = w3.toHex(int(math.pow(ratio, 15))) 239 | w3.geth.personal.unlockAccount(main, app["password"]) 240 | w3.eth.sendTransaction( 241 | transaction={"from": main, "to": address, "value": amount} 242 | ) 243 | update_accounts(app, app["accounts"]) 244 | 245 | 246 | def calculate_scores(app): 247 | members = app["org"].get_members() 248 | LOGGER.info(f"Pulling stats for {members.totalCount} developers") 249 | for member in members: 250 | username = str(member.login) 251 | score = get_score(app, username) 252 | if score > 0: 253 | name = member.name if member.name is not None else username 254 | app["scores"][name] = score 255 | update_scores(app, app["scores"]) 256 | 257 | 258 | @retry(RateLimitExceededException, delay=10) 259 | def get_score(app, username): 260 | github = app["github"] 261 | organisation = app["organisation"] 262 | authored = github.search_issues( 263 | f"org:{organisation} author:{username} is:closed" 264 | ).totalCount 265 | if authored > 0: 266 | reviewed = github.search_issues( 267 | f"org:{organisation} reviewed-by:{username} is:closed" 268 | ).totalCount 269 | authored_and_reviewed = github.search_issues( 270 | f"org:{organisation}" 271 | f" reviewed-by:{username} " 272 | f"author:{username} is:closed" 273 | ).totalCount 274 | review_ratio = float((reviewed - authored_and_reviewed)) / authored 275 | return review_ratio 276 | return 0 277 | 278 | 279 | if __name__ == "__main__": 280 | main() 281 | -------------------------------------------------------------------------------- /web/src/components/PrRatioTable.vue: -------------------------------------------------------------------------------- 1 | 147 | 148 | 149 | 169 | 170 | 181 | 182 | 338 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "aa6cb90e806f9874e01ffc57bddae90dbd90103c10648f037c5e8b7e99937b64" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "aiohttp": { 20 | "hashes": [ 21 | "sha256:1e984191d1ec186881ffaed4581092ba04f7c61582a177b187d3a2f07ed9719e", 22 | "sha256:259ab809ff0727d0e834ac5e8a283dc5e3e0ecc30c4d80b3cd17a4139ce1f326", 23 | "sha256:2f4d1a4fdce595c947162333353d4a44952a724fba9ca3205a3df99a33d1307a", 24 | "sha256:32e5f3b7e511aa850829fbe5aa32eb455e5534eaa4b1ce93231d00e2f76e5654", 25 | "sha256:344c780466b73095a72c616fac5ea9c4665add7fc129f285fbdbca3cccf4612a", 26 | "sha256:460bd4237d2dbecc3b5ed57e122992f60188afe46e7319116da5eb8a9dfedba4", 27 | "sha256:4c6efd824d44ae697814a2a85604d8e992b875462c6655da161ff18fd4f29f17", 28 | "sha256:50aaad128e6ac62e7bf7bd1f0c0a24bc968a0c0590a726d5a955af193544bcec", 29 | "sha256:6206a135d072f88da3e71cc501c59d5abffa9d0bb43269a6dcd28d66bfafdbdd", 30 | "sha256:65f31b622af739a802ca6fd1a3076fd0ae523f8485c52924a89561ba10c49b48", 31 | "sha256:ae55bac364c405caa23a4f2d6cfecc6a0daada500274ffca4a9230e7129eac59", 32 | "sha256:b778ce0c909a2653741cb4b1ac7015b5c130ab9c897611df43ae6a58523cb965" 33 | ], 34 | "index": "pypi", 35 | "version": "==3.6.2" 36 | }, 37 | "aiohttp-cors": { 38 | "hashes": [ 39 | "sha256:0451ba59fdf6909d0e2cd21e4c0a43752bc0703d33fc78ae94d9d9321710193e", 40 | "sha256:4d39c6d7100fd9764ed1caf8cebf0eb01bf5e3f24e2e073fda6234bc48b19f5d" 41 | ], 42 | "index": "pypi", 43 | "version": "==0.7.0" 44 | }, 45 | "appdirs": { 46 | "hashes": [ 47 | "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", 48 | "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" 49 | ], 50 | "version": "==1.4.3" 51 | }, 52 | "async-timeout": { 53 | "hashes": [ 54 | "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", 55 | "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" 56 | ], 57 | "version": "==3.0.1" 58 | }, 59 | "asyncio": { 60 | "hashes": [ 61 | "sha256:83360ff8bc97980e4ff25c964c7bd3923d333d177aa4f7fb736b019f26c7cb41", 62 | "sha256:b62c9157d36187eca799c378e572c969f0da87cd5fc42ca372d92cdb06e7e1de", 63 | "sha256:c46a87b48213d7464f22d9a497b9eef8c1928b68320a2fa94240f969f6fec08c", 64 | "sha256:c4d18b22701821de07bd6aea8b53d21449ec0ec5680645e5317062ea21817d2d" 65 | ], 66 | "index": "pypi", 67 | "version": "==3.4.3" 68 | }, 69 | "attrdict": { 70 | "hashes": [ 71 | "sha256:35c90698b55c683946091177177a9e9c0713a0860f0e049febd72649ccd77b70", 72 | "sha256:9432e3498c74ff7e1b20b3d93b45d766b71cbffa90923496f82c4ae38b92be34" 73 | ], 74 | "version": "==2.0.1" 75 | }, 76 | "attrs": { 77 | "hashes": [ 78 | "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", 79 | "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" 80 | ], 81 | "version": "==19.3.0" 82 | }, 83 | "base58": { 84 | "hashes": [ 85 | "sha256:1e42993c0628ed4f898c03b522b26af78fb05115732549b21a028bc4633d19ab", 86 | "sha256:6aa0553e477478993588303c54659d15e3c17ae062508c854a8b752d07c716bd", 87 | "sha256:9a793c599979c497800eb414c852b80866f28daaed5494703fc129592cc83e60" 88 | ], 89 | "version": "==1.0.3" 90 | }, 91 | "black": { 92 | "hashes": [ 93 | "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b", 94 | "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539" 95 | ], 96 | "index": "pypi", 97 | "version": "==19.10b0" 98 | }, 99 | "certifi": { 100 | "hashes": [ 101 | "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3", 102 | "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f" 103 | ], 104 | "version": "==2019.11.28" 105 | }, 106 | "chardet": { 107 | "hashes": [ 108 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 109 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 110 | ], 111 | "version": "==3.0.4" 112 | }, 113 | "click": { 114 | "hashes": [ 115 | "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", 116 | "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" 117 | ], 118 | "version": "==7.0" 119 | }, 120 | "cytoolz": { 121 | "hashes": [ 122 | "sha256:82f5bba81d73a5a6b06f2a3553ff9003d865952fcb32e1df192378dd944d8a5c" 123 | ], 124 | "markers": "implementation_name == 'cpython'", 125 | "version": "==0.10.1" 126 | }, 127 | "decorator": { 128 | "hashes": [ 129 | "sha256:54c38050039232e1db4ad7375cfce6748d7b41c29e95a081c8a6d2c30364a2ce", 130 | "sha256:5d19b92a3c8f7f101c8dd86afd86b0f061a8ce4540ab8cd401fa2542756bce6d" 131 | ], 132 | "version": "==4.4.1" 133 | }, 134 | "deprecated": { 135 | "hashes": [ 136 | "sha256:408038ab5fdeca67554e8f6742d1521cd3cd0ee0ff9d47f29318a4f4da31c308", 137 | "sha256:8b6a5aa50e482d8244a62e5582b96c372e87e3a28e8b49c316e46b95c76a611d" 138 | ], 139 | "version": "==1.2.7" 140 | }, 141 | "entrypoints": { 142 | "hashes": [ 143 | "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", 144 | "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451" 145 | ], 146 | "version": "==0.3" 147 | }, 148 | "eth-abi": { 149 | "hashes": [ 150 | "sha256:a8f3cc48a057dfcc77d4138920d482a9b0d3044e0ad68f0bc1bd8762720e0c13", 151 | "sha256:ca76f5e64bc1d7a89edd7ab88dbf1afc21956f91b7ac00e062c4db5d8cd6e0c5" 152 | ], 153 | "version": "==2.1.0" 154 | }, 155 | "eth-account": { 156 | "hashes": [ 157 | "sha256:bf857f800a3cb6a7d0535850dfc229fbfb9d04b124cdd0969881d6d5ec9cb645", 158 | "sha256:fa8308c1d280cfde28455d8c031c3a048c8811e502e750ec0d2cff76988dcd0b" 159 | ], 160 | "version": "==0.4.0" 161 | }, 162 | "eth-hash": { 163 | "extras": [ 164 | "pycryptodome" 165 | ], 166 | "hashes": [ 167 | "sha256:1b9cb34dd3cd99c85c2bd6a1420ceae39a2eee8bf080efd264bcda8be3edecc8", 168 | "sha256:499dc02d098f69856d1a6dd005529c16174157d4fb2a9fe20c41f69e39f8f176" 169 | ], 170 | "version": "==0.2.0" 171 | }, 172 | "eth-keyfile": { 173 | "hashes": [ 174 | "sha256:70d734af17efdf929a90bb95375f43522be4ed80c3b9e0a8bca575fb11cd1159", 175 | "sha256:939540efb503380bc30d926833e6a12b22c6750de80feef3720d79e5a79de47d" 176 | ], 177 | "version": "==0.5.1" 178 | }, 179 | "eth-keys": { 180 | "hashes": [ 181 | "sha256:d1cdcd6b2118edf5dcd112ba6efc4b187b028c5c7d6af6ca04d90b7af94a1c58", 182 | "sha256:e15a0140852552ec3eb07e9731e23d390aea4bae892022279af42ce32e9c2620" 183 | ], 184 | "version": "==0.2.4" 185 | }, 186 | "eth-rlp": { 187 | "hashes": [ 188 | "sha256:05d8456981d85e16a9afa57f2f2c3356af5d1c49499cc8512cfcdc034b90dde5", 189 | "sha256:a94744c207ea731a7266bd0894179dc6e51a6a8965316000c8e823b5d7e07694" 190 | ], 191 | "version": "==0.1.2" 192 | }, 193 | "eth-typing": { 194 | "hashes": [ 195 | "sha256:2f3e1f891226148898b219bd94674a9af06c2d75d8cdd8c6722227b472cbd4d4", 196 | "sha256:cf9e5e9fb62cfeb1027823328569315166851c65c5774604d801b6b926ff65bc" 197 | ], 198 | "version": "==2.2.1" 199 | }, 200 | "eth-utils": { 201 | "hashes": [ 202 | "sha256:8358318685e7a7666b148b07df3c4d409435b424dce18501e79920aa52bcaba7", 203 | "sha256:f398c649859cda5ef7c4ee2753468038d93be7d864de7631c06c3e73a7060649" 204 | ], 205 | "version": "==1.8.4" 206 | }, 207 | "faker": { 208 | "hashes": [ 209 | "sha256:202ad3b2ec16ae7c51c02904fb838831f8d2899e61bf18db1e91a5a582feab11", 210 | "sha256:92c84a10bec81217d9cb554ee12b3838c8986ce0b5d45f72f769da22e4bb5432" 211 | ], 212 | "index": "pypi", 213 | "version": "==3.0.0" 214 | }, 215 | "flake8": { 216 | "hashes": [ 217 | "sha256:45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb", 218 | "sha256:49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca" 219 | ], 220 | "index": "pypi", 221 | "version": "==3.7.9" 222 | }, 223 | "hexbytes": { 224 | "hashes": [ 225 | "sha256:438ba9a28dfcda2c2276954b4310f9af1604fb198bfe5ac44c6518feaf6d376a", 226 | "sha256:9e8b3e3dc4a7de23c0cf1bb3c3edfcc1f0df4b78927bad63816c27a027b8b7d1" 227 | ], 228 | "version": "==0.2.0" 229 | }, 230 | "idna": { 231 | "hashes": [ 232 | "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", 233 | "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" 234 | ], 235 | "version": "==2.8" 236 | }, 237 | "importlib-metadata": { 238 | "hashes": [ 239 | "sha256:073a852570f92da5f744a3472af1b61e28e9f78ccf0c9117658dc32b15de7b45", 240 | "sha256:d95141fbfa7ef2ec65cfd945e2af7e5a6ddbd7c8d9a25e66ff3be8e3daf9f60f" 241 | ], 242 | "markers": "python_version < '3.8'", 243 | "version": "==1.3.0" 244 | }, 245 | "ipfshttpclient": { 246 | "hashes": [ 247 | "sha256:0a199a1005fe44bff9da28b5af4785b0b09ca700baac9d1e26718fe23fe89bb7", 248 | "sha256:bee95c500edf669bb8a984d5588fc133fda9ec67845c5688bcbbea030a03f10f" 249 | ], 250 | "version": "==0.4.12" 251 | }, 252 | "jsonschema": { 253 | "hashes": [ 254 | "sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163", 255 | "sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a" 256 | ], 257 | "version": "==3.2.0" 258 | }, 259 | "lru-dict": { 260 | "hashes": [ 261 | "sha256:365457660e3d05b76f1aba3e0f7fedbfcd6528e97c5115a351ddd0db488354cc" 262 | ], 263 | "version": "==1.1.6" 264 | }, 265 | "mccabe": { 266 | "hashes": [ 267 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", 268 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" 269 | ], 270 | "version": "==0.6.1" 271 | }, 272 | "more-itertools": { 273 | "hashes": [ 274 | "sha256:b84b238cce0d9adad5ed87e745778d20a3f8487d0f0cb8b8a586816c7496458d", 275 | "sha256:c833ef592a0324bcc6a60e48440da07645063c453880c9477ceb22490aec1564" 276 | ], 277 | "version": "==8.0.2" 278 | }, 279 | "multiaddr": { 280 | "hashes": [ 281 | "sha256:2faec68b479945fe6b48dd2dc1f8bcccf939aa148836e3a1ab806d6c75db1238", 282 | "sha256:cb7f4091a2d1fa361fe2fd237efcd963abf650efe3af1414c4e9360a34947573" 283 | ], 284 | "version": "==0.0.8" 285 | }, 286 | "multidict": { 287 | "hashes": [ 288 | "sha256:09c19f642e055550c9319d5123221b7e07fc79bda58122aa93910e52f2ab2f29", 289 | "sha256:0c1a5d5f7aa7189f7b83c4411c2af8f1d38d69c4360d5de3eea129c65d8d7ce2", 290 | "sha256:12f22980e7ed0972a969520fb1e55682c9fca89a68b21b49ec43132e680be812", 291 | "sha256:258660e9d6b52de1a75097944e12718d3aa59adc611b703361e3577d69167aaf", 292 | "sha256:3374a23e707848f27b3438500db0c69eca82929337656fce556bd70031fbda74", 293 | "sha256:503b7fce0054c73aa631cc910a470052df33d599f3401f3b77e54d31182525d5", 294 | "sha256:6ce55f2c45ffc90239aab625bb1b4864eef33f73ea88487ef968291fbf09fb3f", 295 | "sha256:725496dde5730f4ad0a627e1a58e2620c1bde0ad1c8080aae15d583eb23344ce", 296 | "sha256:a3721078beff247d0cd4fb19d915c2c25f90907cf8d6cd49d0413a24915577c6", 297 | "sha256:ba566518550f81daca649eded8b5c7dd09210a854637c82351410aa15c49324a", 298 | "sha256:c42362750a51a15dc905cb891658f822ee5021bfbea898c03aa1ed833e2248a5", 299 | "sha256:cf14aaf2ab067ca10bca0b14d5cbd751dd249e65d371734bc0e47ddd8fafc175", 300 | "sha256:cf24e15986762f0e75a622eb19cfe39a042e952b8afba3e7408835b9af2be4fb", 301 | "sha256:d7b6da08538302c5245cd3103f333655ba7f274915f1f5121c4f4b5fbdb3febe", 302 | "sha256:e27e13b9ff0a914a6b8fb7e4947d4ac6be8e4f61ede17edffabd088817df9e26", 303 | "sha256:e53b205f8afd76fc6c942ef39e8ee7c519c775d336291d32874082a87802c67c", 304 | "sha256:ec804fc5f68695d91c24d716020278fcffd50890492690a7e1fef2e741f7172c" 305 | ], 306 | "version": "==4.7.1" 307 | }, 308 | "mypy-extensions": { 309 | "hashes": [ 310 | "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", 311 | "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" 312 | ], 313 | "version": "==0.4.3" 314 | }, 315 | "netaddr": { 316 | "hashes": [ 317 | "sha256:38aeec7cdd035081d3a4c306394b19d677623bf76fa0913f6695127c7753aefd", 318 | "sha256:56b3558bd71f3f6999e4c52e349f38660e54a7a8a9943335f73dfc96883e08ca" 319 | ], 320 | "version": "==0.7.19" 321 | }, 322 | "parsimonious": { 323 | "hashes": [ 324 | "sha256:3add338892d580e0cb3b1a39e4a1b427ff9f687858fdd61097053742391a9f6b" 325 | ], 326 | "version": "==0.8.1" 327 | }, 328 | "pathspec": { 329 | "hashes": [ 330 | "sha256:e285ccc8b0785beadd4c18e5708b12bb8fcf529a1e61215b3feff1d1e559ea5c" 331 | ], 332 | "version": "==0.6.0" 333 | }, 334 | "protobuf": { 335 | "hashes": [ 336 | "sha256:0265379852b9e1f76af6d3d3fe4b3c383a595cc937594bda8565cf69a96baabd", 337 | "sha256:29bd1ed46b2536ad8959401a2f02d2d7b5a309f8e97518e4f92ca6c5ba74dbed", 338 | "sha256:3175d45698edb9a07c1a78a1a4850e674ce8988f20596580158b1d0921d0f057", 339 | "sha256:34a7270940f86da7a28be466ac541c89b6dbf144a6348b9cf7ac6f56b71006ce", 340 | "sha256:38cbc830a4a5ba9956763b0f37090bfd14dd74e72762be6225de2ceac55f4d03", 341 | "sha256:665194f5ad386511ac8d8a0bd57b9ab37b8dd2cd71969458777318e774b9cd46", 342 | "sha256:839bad7d115c77cdff29b488fae6a3ab503ce9a4192bd4c42302a6ea8e5d0f33", 343 | "sha256:934a9869a7f3b0d84eca460e386fba1f7ba2a0c1a120a2648bc41fadf50efd1c", 344 | "sha256:aecdf12ef6dc7fd91713a6da93a86c2f2a8fe54840a3b1670853a2b7402e77c9", 345 | "sha256:c4e90bc27c0691c76e09b5dc506133451e52caee1472b8b3c741b7c912ce43ef", 346 | "sha256:c65d135ea2d85d40309e268106dab02d3bea723db2db21c23ecad4163ced210b", 347 | "sha256:c98dea04a1ff41a70aff2489610f280004831798cb36a068013eed04c698903d", 348 | "sha256:d9049aa194378a426f0b2c784e2054565bf6f754d20fcafdee7102a6250556e8", 349 | "sha256:e028fee51c96de4e81924484c77111dfdea14010ecfc906ea5b252209b0c4de6", 350 | "sha256:e84ad26fb50091b1ea676403c0dd2bd47663099454aa6d88000b1dafecab0941", 351 | "sha256:e88a924b591b06d0191620e9c8aa75297b3111066bb09d49a24bae1054a10c13" 352 | ], 353 | "version": "==3.11.1" 354 | }, 355 | "py": { 356 | "hashes": [ 357 | "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", 358 | "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53" 359 | ], 360 | "version": "==1.8.0" 361 | }, 362 | "pycodestyle": { 363 | "hashes": [ 364 | "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", 365 | "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c" 366 | ], 367 | "version": "==2.5.0" 368 | }, 369 | "pycryptodome": { 370 | "hashes": [ 371 | "sha256:042ae873baadd0c33b4d699a5c5b976ade3233a979d972f98ca82314632d868c", 372 | "sha256:0502876279772b1384b660ccc91563d04490d562799d8e2e06b411e2d81128a9", 373 | "sha256:2de33ed0a95855735d5a0fc0c39603314df9e78ee8bbf0baa9692fb46b3b8bbb", 374 | "sha256:319e568baf86620b419d53063b18c216abf924875966efdfe06891b987196a45", 375 | "sha256:4372ec7518727172e1605c0843cdc5375d4771e447b8148c787b860260aae151", 376 | "sha256:48821950ffb9c836858d8fa09d7840b6df52eadd387a3c5acece55cb387743f9", 377 | "sha256:4b9533d4166ca07abdd49ce9d516666b1df944997fe135d4b21ac376aa624aff", 378 | "sha256:54456cf85130e01674d21fb1ab89ffccacb138a8ade88d72fa2b0ac898d2798b", 379 | "sha256:56fdd0e425f1b8fd3a00b6d96351f86226674974814c50534864d0124d48871f", 380 | "sha256:57b1b707363490c495ad0eeb38bd1b0e1697c497af25fad78d3a1ebf0477fd5b", 381 | "sha256:5c485ed6e9718ebcaa81138fa70ace9c563d202b56a8cee119b4085b023931f5", 382 | "sha256:63c103a22cbe9752f6ea9f1a0de129995bad91c4d03a66c67cffcf6ee0c9f1e1", 383 | "sha256:68fab8455efcbfe87c5d75015476f9b606227ffe244d57bfd66269451706e899", 384 | "sha256:6c2720696b10ae356040e888bde1239b8957fe18885ccf5e7b4e8dec882f0856", 385 | "sha256:72166c2ac520a5dbd2d90208b9c279161ec0861662a621892bd52fb6ca13ab91", 386 | "sha256:7c52308ac5b834331b2f107a490b2c27de024a229b61df4cdc5c131d563dfe98", 387 | "sha256:87d8d85b4792ca5e730fb7a519fbc3ed976c59dcf79c5204589c59afd56b9926", 388 | "sha256:896e9b6fd0762aa07b203c993fbbee7a1f1a4674c6886afd7bfa86f3d1be98a8", 389 | "sha256:8a799bea3c6617736e914a2e77c409f52893d382f619f088f8a80e2e21f573c1", 390 | "sha256:9d9945ac8375d5d8e60bd2a2e1df5882eaa315522eedf3ca868b1546dfa34eba", 391 | "sha256:9ef966c727de942de3e41aa8462c4b7b4bca70f19af5a3f99e31376589c11aac", 392 | "sha256:a168e73879619b467072509a223282a02c8047d932a48b74fbd498f27224aa04", 393 | "sha256:a30f501bbb32e01a49ef9e09ca1260e5ab49bf33a257080ec553e08997acc487", 394 | "sha256:a8ca2450394d3699c9f15ef25e8de9a24b401933716a1e39d37fa01f5fe3c58b", 395 | "sha256:aec4d42deb836b8fb3ba32f2ba1ef0d33dd3dc9d430b1479ee7a914490d15b5e", 396 | "sha256:b4af098f2a50f8d048ab12cabb59456585c0acf43d90ee79782d2d6d0ed59dba", 397 | "sha256:b55c60c321ac91945c60a40ac9896ac7a3d432bb3e8c14006dfd82ad5871c331", 398 | "sha256:c53348358408d94869059e16fba5ff3bef8c52c25b18421472aba272b9bb450f", 399 | "sha256:cbfd97f9e060f0d30245cd29fa267a9a84de9da97559366fca0a3f7655acc63f", 400 | "sha256:d3fe3f33ad52bf0c19ee6344b695ba44ffbfa16f3c29ca61116b48d97bd970fb", 401 | "sha256:e3a79a30d15d9c7c284a7734036ee8abdb5ca3a6f5774d293cdc9e1358c1dc10", 402 | "sha256:eec0689509389f19875f66ae8dedd59f982240cdab31b9f78a8dc266011df93a" 403 | ], 404 | "version": "==3.9.4" 405 | }, 406 | "pyflakes": { 407 | "hashes": [ 408 | "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", 409 | "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2" 410 | ], 411 | "version": "==2.1.1" 412 | }, 413 | "pygithub": { 414 | "hashes": [ 415 | "sha256:453896a1c3d46eb6724598daa21cf7ae9a83c6012126e840e3f7c665142fb04f" 416 | ], 417 | "index": "pypi", 418 | "version": "==1.44.1" 419 | }, 420 | "pyjwt": { 421 | "hashes": [ 422 | "sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e", 423 | "sha256:8d59a976fb773f3e6a39c85636357c4f0e242707394cadadd9814f5cbaa20e96" 424 | ], 425 | "version": "==1.7.1" 426 | }, 427 | "pyrsistent": { 428 | "hashes": [ 429 | "sha256:f3b280d030afb652f79d67c5586157c5c1355c9a58dfc7940566e28d28f3df1b" 430 | ], 431 | "version": "==0.15.6" 432 | }, 433 | "python-dateutil": { 434 | "hashes": [ 435 | "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", 436 | "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" 437 | ], 438 | "version": "==2.8.1" 439 | }, 440 | "regex": { 441 | "hashes": [ 442 | "sha256:0472acc4b6319801c1bc681d838c88ba1446f9ae199e01f6e41091c701fb3d42", 443 | "sha256:16709434c4e2332ee8ba26ae339aceb8ab0b24b8398ebd0f52ebc943f45c4fc2", 444 | "sha256:223fb63ec8dcab20b3318e93dcec4aee89e98b062934090bf29ffc374d2000a2", 445 | "sha256:23c3ebf05d1cd3adb26723fd598e75724e0cdb7d6a35185ac0caf061cc6edb49", 446 | "sha256:2404a50fb48badaf214b700f08822b68d93d79200e0aefd9569d0332d21fbfcb", 447 | "sha256:2af3a7a16fed6eff85c25da106effa36f61cbbe801d00ade349b53ce7619eb15", 448 | "sha256:37e018d3746baf159aedfc9773c3cafacbd10d354ba15484f5cfc8ed9da5748b", 449 | "sha256:3c9c2988d02a9238a1975c70e87c6ce94e6f36dd8e372b66f468990cfe077434", 450 | "sha256:47298bc8b89d1c747f0f5974aa528fc0b6b17396f1694136a224d51461279d83", 451 | "sha256:4eeb0fe936797ae00a085f99802642bfc722b3b4ea557e9e7849cb621ea10c91", 452 | "sha256:6881be0218b47ed76db033f252bab3f912dfe7ed1fe7baa9daebf51de08546a0", 453 | "sha256:7ac08cee5055f548eed3889e9aaef15fd00172d037949496f1f0b34acb8a7c3e", 454 | "sha256:7c5e2efcf079c35ff266c3f3a6708834f88f9fd04a3c16b855e036b2b7b1b543", 455 | "sha256:8355eaa64724a0fdb010a1654b77cb3e375dc08b7f592cc4a1c05ac606aa481c", 456 | "sha256:999a885f7f5194464238ad5d74b05982acee54002f3aa775d8e0e8c5fb74c06c", 457 | "sha256:9fd2f4813eaa3e421e82819d38e5b634d900faff7ae5a80cd89ccff407175e69", 458 | "sha256:a2e1e53df7dd27943da2b512895125b33fb20f81862c9fed7b3bab2a1de684d1", 459 | "sha256:ab43bc0836820b7900dfffc025b996784aec26ec87dc1df4f95a40398760223f", 460 | "sha256:ba449b56fa419fb19bf2a2438adbd2433f27087a6fe115917eaf9cfca684d5b6", 461 | "sha256:d3f632cefad2cf247bd845794002585e3772288bfcb0dbac59fdecd32cd38b67", 462 | "sha256:d51311496061863caae2cfe120cf1ef37900019b86c89c2d75f0918e0b4b8bf3" 463 | ], 464 | "version": "==2019.12.19" 465 | }, 466 | "requests": { 467 | "hashes": [ 468 | "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", 469 | "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" 470 | ], 471 | "index": "pypi", 472 | "version": "==2.22.0" 473 | }, 474 | "retry": { 475 | "hashes": [ 476 | "sha256:ccddf89761fa2c726ab29391837d4327f819ea14d244c232a1d24c67a2f98606", 477 | "sha256:f8bfa8b99b69c4506d6f5bd3b0aabf77f98cdb17f3c9fc3f5ca820033336fba4" 478 | ], 479 | "index": "pypi", 480 | "version": "==0.9.2" 481 | }, 482 | "rlp": { 483 | "hashes": [ 484 | "sha256:27273fc2dbc3513c1e05ea6b8af28aac8745fb09c164e39e2ed2807bf7e1b342", 485 | "sha256:97b7e770f16442772311b33e6bc28b45318e7c8def69b9df16452304e224e9df" 486 | ], 487 | "version": "==1.2.0" 488 | }, 489 | "six": { 490 | "hashes": [ 491 | "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", 492 | "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66" 493 | ], 494 | "version": "==1.13.0" 495 | }, 496 | "text-unidecode": { 497 | "hashes": [ 498 | "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", 499 | "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93" 500 | ], 501 | "version": "==1.3" 502 | }, 503 | "toml": { 504 | "hashes": [ 505 | "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", 506 | "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" 507 | ], 508 | "version": "==0.10.0" 509 | }, 510 | "toolz": { 511 | "hashes": [ 512 | "sha256:08fdd5ef7c96480ad11c12d472de21acd32359996f69a5259299b540feba4560" 513 | ], 514 | "version": "==0.10.0" 515 | }, 516 | "typed-ast": { 517 | "hashes": [ 518 | "sha256:1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161", 519 | "sha256:18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e", 520 | "sha256:262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e", 521 | "sha256:2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0", 522 | "sha256:354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c", 523 | "sha256:48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47", 524 | "sha256:4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631", 525 | "sha256:630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4", 526 | "sha256:66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34", 527 | "sha256:71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b", 528 | "sha256:7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2", 529 | "sha256:838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e", 530 | "sha256:95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a", 531 | "sha256:bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233", 532 | "sha256:cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1", 533 | "sha256:d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36", 534 | "sha256:d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d", 535 | "sha256:d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a", 536 | "sha256:fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66", 537 | "sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12" 538 | ], 539 | "version": "==1.4.0" 540 | }, 541 | "typing-extensions": { 542 | "hashes": [ 543 | "sha256:091ecc894d5e908ac75209f10d5b4f118fbdb2eb1ede6a63544054bb1edb41f2", 544 | "sha256:910f4656f54de5993ad9304959ce9bb903f90aadc7c67a0bef07e678014e892d", 545 | "sha256:cf8b63fedea4d89bab840ecbb93e75578af28f76f66c35889bd7065f5af88575" 546 | ], 547 | "version": "==3.7.4.1" 548 | }, 549 | "urllib3": { 550 | "hashes": [ 551 | "sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293", 552 | "sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745" 553 | ], 554 | "version": "==1.25.7" 555 | }, 556 | "varint": { 557 | "hashes": [ 558 | "sha256:a6ecc02377ac5ee9d65a6a8ad45c9ff1dac8ccee19400a5950fb51d594214ca5" 559 | ], 560 | "version": "==1.0.2" 561 | }, 562 | "web3": { 563 | "hashes": [ 564 | "sha256:770dbbb86da23185df06bef1c2ed7c871f6f8714ac3d3cfe2e7f57f0bfb98086", 565 | "sha256:f4362d37137ab42423a38569fc0f6ff3b53a107925ac45345e798ed1d09da301" 566 | ], 567 | "index": "pypi", 568 | "version": "==5.4.0" 569 | }, 570 | "websockets": { 571 | "hashes": [ 572 | "sha256:0e4fb4de42701340bd2353bb2eee45314651caa6ccee80dbd5f5d5978888fed5", 573 | "sha256:1d3f1bf059d04a4e0eb4985a887d49195e15ebabc42364f4eb564b1d065793f5", 574 | "sha256:20891f0dddade307ffddf593c733a3fdb6b83e6f9eef85908113e628fa5a8308", 575 | "sha256:295359a2cc78736737dd88c343cd0747546b2174b5e1adc223824bcaf3e164cb", 576 | "sha256:2db62a9142e88535038a6bcfea70ef9447696ea77891aebb730a333a51ed559a", 577 | "sha256:3762791ab8b38948f0c4d281c8b2ddfa99b7e510e46bd8dfa942a5fff621068c", 578 | "sha256:3db87421956f1b0779a7564915875ba774295cc86e81bc671631379371af1170", 579 | "sha256:3ef56fcc7b1ff90de46ccd5a687bbd13a3180132268c4254fc0fa44ecf4fc422", 580 | "sha256:4f9f7d28ce1d8f1295717c2c25b732c2bc0645db3215cf757551c392177d7cb8", 581 | "sha256:5c01fd846263a75bc8a2b9542606927cfad57e7282965d96b93c387622487485", 582 | "sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f", 583 | "sha256:751a556205d8245ff94aeef23546a1113b1dd4f6e4d102ded66c39b99c2ce6c8", 584 | "sha256:7ff46d441db78241f4c6c27b3868c9ae71473fe03341340d2dfdbe8d79310acc", 585 | "sha256:965889d9f0e2a75edd81a07592d0ced54daa5b0785f57dc429c378edbcffe779", 586 | "sha256:9b248ba3dd8a03b1a10b19efe7d4f7fa41d158fdaa95e2cf65af5a7b95a4f989", 587 | "sha256:9bef37ee224e104a413f0780e29adb3e514a5b698aabe0d969a6ba426b8435d1", 588 | "sha256:c1ec8db4fac31850286b7cd3b9c0e1b944204668b8eb721674916d4e28744092", 589 | "sha256:c8a116feafdb1f84607cb3b14aa1418424ae71fee131642fc568d21423b51824", 590 | "sha256:ce85b06a10fc65e6143518b96d3dca27b081a740bae261c2fb20375801a9d56d", 591 | "sha256:d705f8aeecdf3262379644e4b55107a3b55860eb812b673b28d0fbc347a60c55", 592 | "sha256:e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36", 593 | "sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b" 594 | ], 595 | "version": "==8.1" 596 | }, 597 | "wrapt": { 598 | "hashes": [ 599 | "sha256:565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1" 600 | ], 601 | "version": "==1.11.2" 602 | }, 603 | "yarl": { 604 | "hashes": [ 605 | "sha256:0c2ab325d33f1b824734b3ef51d4d54a54e0e7a23d13b86974507602334c2cce", 606 | "sha256:0ca2f395591bbd85ddd50a82eb1fde9c1066fafe888c5c7cc1d810cf03fd3cc6", 607 | "sha256:2098a4b4b9d75ee352807a95cdf5f10180db903bc5b7270715c6bbe2551f64ce", 608 | "sha256:25e66e5e2007c7a39541ca13b559cd8ebc2ad8fe00ea94a2aad28a9b1e44e5ae", 609 | "sha256:26d7c90cb04dee1665282a5d1a998defc1a9e012fdca0f33396f81508f49696d", 610 | "sha256:308b98b0c8cd1dfef1a0311dc5e38ae8f9b58349226aa0533f15a16717ad702f", 611 | "sha256:3ce3d4f7c6b69c4e4f0704b32eca8123b9c58ae91af740481aa57d7857b5e41b", 612 | "sha256:58cd9c469eced558cd81aa3f484b2924e8897049e06889e8ff2510435b7ef74b", 613 | "sha256:5b10eb0e7f044cf0b035112446b26a3a2946bca9d7d7edb5e54a2ad2f6652abb", 614 | "sha256:6faa19d3824c21bcbfdfce5171e193c8b4ddafdf0ac3f129ccf0cdfcb083e462", 615 | "sha256:944494be42fa630134bf907714d40207e646fd5a94423c90d5b514f7b0713fea", 616 | "sha256:a161de7e50224e8e3de6e184707476b5a989037dcb24292b391a3d66ff158e70", 617 | "sha256:a4844ebb2be14768f7994f2017f70aca39d658a96c786211be5ddbe1c68794c1", 618 | "sha256:c2b509ac3d4b988ae8769901c66345425e361d518aecbe4acbfc2567e416626a", 619 | "sha256:c9959d49a77b0e07559e579f38b2f3711c2b8716b8410b320bf9713013215a1b", 620 | "sha256:d8cdee92bc930d8b09d8bd2043cedd544d9c8bd7436a77678dd602467a993080", 621 | "sha256:e15199cdb423316e15f108f51249e44eb156ae5dba232cb73be555324a1d49c2" 622 | ], 623 | "version": "==1.4.2" 624 | }, 625 | "zipp": { 626 | "hashes": [ 627 | "sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e", 628 | "sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335" 629 | ], 630 | "version": "==0.6.0" 631 | } 632 | }, 633 | "develop": {} 634 | } 635 | --------------------------------------------------------------------------------