├── 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 |
2 |
3 |
12 |
13 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
31 |
32 |
36 |
37 |
38 |
42 | web
43 |
44 |
48 | remove
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
60 | Picnic Technologies
61 |
62 |
68 |
69 | fab fa-github
70 |
71 | Sign-in through Github
72 |
73 |
74 |
75 |
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 | 
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 |
2 |
3 |
4 |
5 |
6 |
11 |
15 |
16 |
20 | {{ description.header }}
21 |
22 |
26 |
27 |
28 | {{ item.icon }}
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
47 |
55 |
59 |
60 | {{ props.item.rank }}
61 | |
62 | {{ props.item.name }} |
63 |
64 | {{ props.item.ratio }}
65 | |
66 |
67 |
68 |
74 | {{ props.item.address.substring(0, 5) }}
75 |
76 | {{ props.item.address }}
77 |
78 | |
79 |
80 | {{ props.item.coin }}
81 | |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
94 |
95 |
96 |
97 |
98 | Shop
99 |
100 |
101 |
102 |
103 |
107 |
108 | {{ item.icon }}
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
129 |
130 |
131 | {{ clickedWallet }}
132 |
133 |
134 |
139 | Close
140 |
141 |
142 |
143 |
144 |
145 |
146 |
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 |
--------------------------------------------------------------------------------