├── .gitignore ├── Dockerfile ├── README.rst ├── VERSION ├── app.py ├── application.yaml ├── config.py ├── cron.yaml ├── cron ├── __init__.py ├── daily.py ├── mirrorlib.py ├── pypi_mirrors.py └── test_run.py ├── nginx.conf ├── notification.py ├── requirements.txt ├── services ├── __init__.py ├── errors.py ├── iploc.py └── metrics.py ├── static ├── 404.html ├── 500.html ├── 502.html ├── 503.html ├── 504.html ├── css │ └── bootstrap.min.css ├── favicon.ico ├── img │ ├── forkme.png │ ├── glyphicons-halflings-white.png │ ├── glyphicons-halflings.png │ ├── mailgun.png │ ├── mailgun_small.png │ └── powered-by-gce.svg └── js │ └── jquery.sparkline.min.js ├── templates └── index.html ├── utils.py └── wsgi.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | *.pyc 3 | environment.json 4 | environment.txt 5 | venv 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:2.7 2 | 3 | WORKDIR /src/ 4 | 5 | ADD ./requirements.txt . 6 | 7 | RUN pip install -r /src/requirements.txt 8 | 9 | ADD . . 10 | 11 | ENTRYPOINT ["/usr/local/bin/uwsgi"] 12 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | pypi-mirrors 2 | ============ 3 | 4 | Very simple tool that pings the PyPI mirrors and tells us when they were updated last. 5 | 6 | I threw this together very quickly as a proof of concept feel free to fork, and send pull requests. 7 | 8 | Docker Image 9 | ------------ 10 | 11 | https://hub.docker.com/r/ibigbug/pypi-mirrors/ 12 | 13 | Config 14 | ------ 15 | 16 | Redis 17 | ~~~~~ 18 | It requires redis in order to cache some of the data. For local development it is assuming it to be running 19 | at localhost:6379 db:1 and no password. see ``config.py`` for more info. 20 | 21 | GeoIP 22 | ~~~~~ 23 | In order to get the IP address geolocation lookup, you need to sign up for an account from http://ipinfodb.com/register.php . If you don't have the env variable set, you will not have access to the geo location information. set IPLOC_API_KEY with the API key they give you. 24 | 25 | Email & Twitter 26 | ~~~~~~~~~~~~~~~ 27 | Pass the corresponding environment variables to enable email & twitter notifications. 28 | 29 | 30 | Environment variables 31 | ~~~~~~~~~~~~~~~~~~~~~ 32 | 33 | env variables:: 34 | 35 | docker run \ 36 | -e 'CACHE_REDIS_HOST=localhost' \ 37 | -e 'CACHE_REDIS_PORT=6379' \ 38 | -e 'IPLOC_API_KEY=' \ 39 | -e 'TWITTER_CONSUMER_KEY=' \ 40 | -e 'TWITTER_CONSUMER_SECRET=' \ 41 | -e 'TWITTER_ACCESS_KEY=' \ 42 | -e 'TWITTER_ACCESS_SECRET=' \ 43 | -e 'EMAIL_HOST=' \ 44 | -e 'EMAIL_PORT=' \ 45 | -e 'EMAIL_USER=' \ 46 | -e 'EMAIL_PASSWORD=' \ 47 | -e 'EMAIL_FROM=' \ 48 | -e 'EMAIL_TO=' \ 49 | -e 'EMAIL_BCC=' \ 50 | -e 'EMAIL_TO_ADMIN='\ 51 | -e 'SENTRY_DSN='\ 52 | ibigbug/pypi-mirrors \ 53 | uwsgi -w wsgi --logto=/var/log/uwsgi/uwsgi.log --chdir=/src/ --chmod-socket=666 54 | 55 | 56 | How it works 57 | ------------ 58 | The ``pypi_mirrors.py`` script runs via a cron job and puts data into redis. There is one webpage that pull the data from redis and 59 | displays it. There is a daily cron job that runs and sends out notifications if the mirrors are out of date. 60 | 61 | Demo 62 | ---- 63 | http://www.pypi-mirrors.org 64 | 65 | How to help 66 | ----------- 67 | Pick one of the things on the TODO list and implement it and send a pull request. 68 | 69 | Running locally 70 | --------------- 71 | Make sure redis is running 72 | 73 | 1. Collecting Data:: 74 | 75 | $ python pypi_mirrors.py 76 | 77 | 2. Running web server:: 78 | 79 | $ python app.py 80 | # connect to http://localhost:5000 in browser 81 | 82 | 83 | TODO: 84 | ----- 85 | - [ ] Create a setup.py and add to PyPI 86 | - [ ] Add better documentation 87 | - [x] Make Docker Image 88 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.3.8 2 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from flask import g, request 4 | from flask import Flask, render_template, Response 5 | from utils import get_page_data, get_json_data 6 | 7 | from prometheus_client import make_wsgi_app 8 | from services.metrics import req_counter, rt_histogram 9 | 10 | 11 | app = Flask(__name__) 12 | 13 | 14 | @app.route('/') 15 | def index(): 16 | context = get_page_data() 17 | return render_template('index.html', **context) 18 | 19 | 20 | @app.route('/data.json') 21 | def json_data(): 22 | return Response(get_json_data(), mimetype='application/json') 23 | 24 | 25 | @app.route('/metrics') 26 | def metrics(): 27 | return make_wsgi_app() 28 | 29 | 30 | @app.route('/status') 31 | def healthcheck(): 32 | return 'ok' 33 | 34 | 35 | @app.before_request 36 | def before_request(): 37 | g.start_time = time.time() 38 | 39 | 40 | @app.after_request 41 | def after_request(res): 42 | meth = request.method 43 | endpoint = request.path 44 | status = res.status_code 45 | req_counter.labels(meth, endpoint, status).inc() 46 | 47 | delta = time.time() - g.start_time 48 | rt_histogram.labels(meth, endpoint, status).observe(delta) 49 | 50 | return res 51 | 52 | 53 | if __name__ == '__main__': 54 | params = {"debug": True, 55 | "host": "0.0.0.0", } 56 | 57 | app.run(**params) 58 | -------------------------------------------------------------------------------- /application.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: pypi-mirrors-front 6 | name: pypi-mirrors-front 7 | spec: 8 | minReadySeconds: 5 9 | progressDeadlineSeconds: 600 10 | replicas: 3 11 | revisionHistoryLimit: 2 12 | selector: 13 | matchLabels: 14 | app: pypi-mirrors-front 15 | strategy: 16 | rollingUpdate: 17 | maxSurge: 1 18 | maxUnavailable: 1 19 | type: RollingUpdate 20 | template: 21 | metadata: 22 | creationTimestamp: null 23 | labels: 24 | app: pypi-mirrors-front 25 | spec: 26 | containers: 27 | - args: 28 | - -w 29 | - wsgi 30 | - --chdir=/src/ 31 | - --chmod-socket=666 32 | - --http=0.0.0.0:80 33 | - --master 34 | env: 35 | - name: CACHE_REDIS_HOST 36 | valueFrom: 37 | secretKeyRef: 38 | key: CACHE_REDIS_HOST 39 | name: pypi-mirrors-config 40 | - name: CACHE_REDIS_PORT 41 | valueFrom: 42 | secretKeyRef: 43 | key: CACHE_REDIS_PORT 44 | name: pypi-mirrors-config 45 | - name: IPLOC_API_KEY 46 | valueFrom: 47 | secretKeyRef: 48 | key: IPLOC_API_KEY 49 | name: pypi-mirrors-config 50 | - name: TWITTER_CONSUMER_KEY 51 | valueFrom: 52 | secretKeyRef: 53 | key: TWITTER_CONSUMER_KEY 54 | name: pypi-mirrors-config 55 | - name: TWITTER_CONSUMER_SECRET 56 | valueFrom: 57 | secretKeyRef: 58 | key: TWITTER_CONSUMER_SECRET 59 | name: pypi-mirrors-config 60 | - name: TWITTER_ACCESS_KEY 61 | valueFrom: 62 | secretKeyRef: 63 | key: TWITTER_ACCESS_KEY 64 | name: pypi-mirrors-config 65 | - name: TWITTER_ACCESS_SECRET 66 | valueFrom: 67 | secretKeyRef: 68 | key: TWITTER_ACCESS_SECRET 69 | name: pypi-mirrors-config 70 | - name: EMAIL_HOST 71 | valueFrom: 72 | secretKeyRef: 73 | key: EMAIL_HOST 74 | name: pypi-mirrors-config 75 | - name: EMAIL_PORT 76 | valueFrom: 77 | secretKeyRef: 78 | key: EMAIL_PORT 79 | name: pypi-mirrors-config 80 | - name: EMAIL_USER 81 | valueFrom: 82 | secretKeyRef: 83 | key: EMAIL_USER 84 | name: pypi-mirrors-config 85 | - name: EMAIL_PASSWORD 86 | valueFrom: 87 | secretKeyRef: 88 | key: EMAIL_PASSWORD 89 | name: pypi-mirrors-config 90 | - name: EMAIL_FROM 91 | valueFrom: 92 | secretKeyRef: 93 | key: EMAIL_FROM 94 | name: pypi-mirrors-config 95 | - name: EMAIL_BCC 96 | valueFrom: 97 | secretKeyRef: 98 | key: EMAIL_BCC 99 | name: pypi-mirrors-config 100 | - name: EMAIL_TO_ADMIN 101 | valueFrom: 102 | secretKeyRef: 103 | key: EMAIL_TO_ADMIN 104 | name: pypi-mirrors-config 105 | image: wtfcontainerregistry.azurecr.io/pypi-mirrors:v1 106 | imagePullPolicy: IfNotPresent 107 | name: pypi-mirrors-front 108 | ports: 109 | - containerPort: 80 110 | protocol: TCP 111 | resources: 112 | limits: 113 | cpu: 500m 114 | requests: 115 | cpu: 250m 116 | terminationMessagePath: /dev/termination-log 117 | terminationMessagePolicy: File 118 | dnsPolicy: ClusterFirst 119 | restartPolicy: Always 120 | securityContext: {} 121 | terminationGracePeriodSeconds: 30 -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import json 4 | 5 | PY2 = sys.version_info < (3, 0) 6 | 7 | # mirrors listed here 8 | MIRRORS = [ 9 | ('https', 'pypi.shuosc.org'), 10 | ('http', 'mirrors.163.com/pypi'), 11 | ('https', 'mirror-ord.pypi.io'), 12 | ('https', 'pypi.doubanio.com'), 13 | ('https', 'pypi.fcio.net'), 14 | ('https', 'pypi.tuna.tsinghua.edu.cn'), 15 | ('http', 'mirror.picosecond.org/pypi'), 16 | ('http', 'mirrors.aliyun.com/pypi'), 17 | ('https', 'pypi.pubyun.com'), 18 | ('http', 'mirrors-uk.go-parts.com/python'), 19 | ('http', 'mirrors-ru.go-parts.com/python'), 20 | ('http', 'mirrors-au.go-parts.com/python'), 21 | ('https', 'pypi.mirrors.ustc.edu.cn'), 22 | ] 23 | 24 | EMAIL_OVERRIDE = None # None or "blah@example.com" 25 | 26 | 27 | def load_config(): 28 | envvars = '/etc/pypi-mirrors-environment.json' 29 | if os.path.exists(envvars): 30 | env = json.load(open(envvars)) 31 | env.update(os.environ) 32 | else: 33 | env = os.environ 34 | return { 35 | 'host': env.get('CACHE_REDIS_HOST', 'localhost'), 36 | 'port': env.get('CACHE_REDIS_PORT', 6379), 37 | 'password': env.get('CACHE_REDIS_PASSWORD'), 38 | 'db': 1, 39 | 'ip_api_key': env.get('IPLOC_API_KEY', None), 40 | 'twitter_consumer_key': env.get('TWITTER_CONSUMER_KEY', None), 41 | 'twitter_consumer_secret': env.get('TWITTER_CONSUMER_SECRET', None), 42 | 'twitter_access_key': env.get('TWITTER_ACCESS_KEY', None), 43 | 'twitter_access_secret': env.get('TWITTER_ACCESS_SECRET', None), 44 | 'email_host': env.get('EMAIL_HOST', None), 45 | 'email_port': env.get('EMAIL_PORT', None), 46 | 'email_user': env.get('EMAIL_USER', None), 47 | 'email_password': env.get('EMAIL_PASSWORD', None), 48 | 'email_from': env.get('EMAIL_FROM', None), 49 | 'email_to': env.get('EMAIL_TO', None), 50 | 'email_bcc': env.get('EMAIL_BCC', None), 51 | 'email_to_admin': env.get('EMAIL_TO_ADMIN', None), 52 | 'sentry_dsn': env.get('SENTRY_DSN', None), 53 | } 54 | -------------------------------------------------------------------------------- /cron.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1beta1 2 | kind: CronJob 3 | metadata: 4 | name: sync-mirror-status 5 | spec: 6 | schedule: "*/5 * * * *" 7 | jobTemplate: 8 | spec: 9 | template: 10 | spec: 11 | containers: 12 | - name: sync-mirror-status 13 | command: 14 | - python 15 | args: 16 | - /src/cron/pypi_mirrors.py 17 | 18 | env: 19 | - name: CACHE_REDIS_HOST 20 | valueFrom: 21 | secretKeyRef: 22 | key: CACHE_REDIS_HOST 23 | name: pypi-mirrors-config 24 | - name: CACHE_REDIS_PORT 25 | valueFrom: 26 | secretKeyRef: 27 | key: CACHE_REDIS_PORT 28 | name: pypi-mirrors-config 29 | - name: IPLOC_API_KEY 30 | valueFrom: 31 | secretKeyRef: 32 | key: IPLOC_API_KEY 33 | name: pypi-mirrors-config 34 | - name: TWITTER_CONSUMER_KEY 35 | valueFrom: 36 | secretKeyRef: 37 | key: TWITTER_CONSUMER_KEY 38 | name: pypi-mirrors-config 39 | - name: TWITTER_CONSUMER_SECRET 40 | valueFrom: 41 | secretKeyRef: 42 | key: TWITTER_CONSUMER_SECRET 43 | name: pypi-mirrors-config 44 | - name: TWITTER_ACCESS_KEY 45 | valueFrom: 46 | secretKeyRef: 47 | key: TWITTER_ACCESS_KEY 48 | name: pypi-mirrors-config 49 | - name: TWITTER_ACCESS_SECRET 50 | valueFrom: 51 | secretKeyRef: 52 | key: TWITTER_ACCESS_SECRET 53 | name: pypi-mirrors-config 54 | - name: EMAIL_HOST 55 | valueFrom: 56 | secretKeyRef: 57 | key: EMAIL_HOST 58 | name: pypi-mirrors-config 59 | - name: EMAIL_PORT 60 | valueFrom: 61 | secretKeyRef: 62 | key: EMAIL_PORT 63 | name: pypi-mirrors-config 64 | - name: EMAIL_USER 65 | valueFrom: 66 | secretKeyRef: 67 | key: EMAIL_USER 68 | name: pypi-mirrors-config 69 | - name: EMAIL_PASSWORD 70 | valueFrom: 71 | secretKeyRef: 72 | key: EMAIL_PASSWORD 73 | name: pypi-mirrors-config 74 | - name: EMAIL_FROM 75 | valueFrom: 76 | secretKeyRef: 77 | key: EMAIL_FROM 78 | name: pypi-mirrors-config 79 | - name: EMAIL_BCC 80 | valueFrom: 81 | secretKeyRef: 82 | key: EMAIL_BCC 83 | name: pypi-mirrors-config 84 | - name: EMAIL_TO_ADMIN 85 | valueFrom: 86 | secretKeyRef: 87 | key: EMAIL_TO_ADMIN 88 | name: pypi-mirrors-config 89 | image: wtfcontainerregistry.azurecr.io/pypi-mirrors:v1 90 | imagePullPolicy: Always 91 | restartPolicy: OnFailure -------------------------------------------------------------------------------- /cron/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import logging 3 | from datetime import datetime 4 | from apscheduler.schedulers.background import BackgroundScheduler 5 | 6 | from .daily import run as run_daily 7 | from .pypi_mirrors import run as run_sync 8 | 9 | logging.getLogger('apscheduler.executors.default').addHandler( 10 | logging.StreamHandler(stream=sys.stdout)) 11 | logging.getLogger('apscheduler.scheduler').addHandler( 12 | logging.StreamHandler(stream=sys.stdout)) 13 | 14 | 15 | def start(): 16 | print('cront started') 17 | scheduler = BackgroundScheduler() 18 | scheduler.add_job(run_daily, 'interval', days=1) 19 | scheduler.add_job(run_sync, 'interval', minutes=5, 20 | max_instances=1, next_run_time=datetime.now()) 21 | scheduler.start() 22 | -------------------------------------------------------------------------------- /cron/daily.py: -------------------------------------------------------------------------------- 1 | from mirrorlib import find_out_of_date_mirrors 2 | from config import MIRRORS 3 | from notification import ( 4 | update_twitter_status, send_warning_email, 5 | send_status_email) 6 | 7 | 8 | def __tweet_outofdate(mirror, last_update): 9 | """ Send a tweet saying we have a mirror out of date """ 10 | status = "{0} is out of date, it was last updated {1}".format(mirror, 11 | last_update) 12 | update_twitter_status(status) 13 | 14 | 15 | def daily_out_of_date_mirror_check(): 16 | """ run everything """ 17 | results = find_out_of_date_mirrors(mirrors=MIRRORS) 18 | 19 | if results: 20 | email_message = "" 21 | for res in results: 22 | email_message += "{0} was last updated {1}\n".format( 23 | res.get('mirror'), 24 | res.get('time_diff_human')) 25 | 26 | print("{0} is out of date. {1}".format( 27 | res.get('mirror'), res.get('time_diff_human'))) 28 | 29 | # one tweet for each out of date mirror 30 | __tweet_outofdate(res.get('mirror'), res.get('time_diff_human')) 31 | 32 | # one email for all out of date mirrors 33 | send_warning_email(email_message) 34 | else: 35 | print("All is good, sending Good message!") 36 | send_status_email("[All Mirrors are up to date]") 37 | 38 | 39 | def run(): 40 | """ run all of the daily cron jobs.""" 41 | daily_out_of_date_mirror_check() 42 | 43 | 44 | if __name__ == '__main__': 45 | run() 46 | -------------------------------------------------------------------------------- /cron/mirrorlib.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # -*- coding: utf-8 -*- 4 | # Open Source Initiative OSI - The MIT License (MIT):Licensing 5 | # 6 | # The MIT License (MIT) 7 | # Copyright (c) 2012 Ken Cochrane (KenCochrane@gmail.com) 8 | # 9 | # Permission is hereby granted, free of charge, to any person obtaining a copy of 10 | # this software and associated documentation files (the "Software"), to deal in 11 | # the Software without restriction, including without limitation the rights to 12 | # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 13 | # of the Software, and to permit persons to whom the Software is furnished to do 14 | # so, subject to the following conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be included in all 17 | # copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | # SOFTWARE. 26 | 27 | """ This is a simple library that you can use to find out the status of 28 | PyPI mirrors. It is based on the information that is found at 29 | http://www.python.org/dev/peps/pep-0381/ and 30 | http://pypi.python.org/mirrors 31 | 32 | Updated for PEP 449 33 | http://www.python.org/dev/peps/pep-0449/ 34 | 35 | """ 36 | try: 37 | import gevent 38 | import gevent.monkey 39 | gevent.monkey.patch_socket() 40 | gevent.monkey.patch_ssl() 41 | from gevent.pool import Pool 42 | pool = Pool(10) 43 | print('using gevent pool') 44 | except ImportError: 45 | from multiprocessing.dummy import Pool 46 | pool = Pool(10) 47 | print('using multiprocessing pool') 48 | 49 | 50 | import datetime 51 | import time 52 | import operator 53 | 54 | from utils import cmp_to_key 55 | 56 | from config import PY2 57 | if PY2: 58 | from urllib2 import urlopen 59 | else: 60 | from urllib.request import urlopen 61 | 62 | 63 | MIRROR_URL_FORMAT = "{0}://{1}/last-modified" 64 | MASTER_URL_FORMAT = "https://{0}/daytime" 65 | MASTER_SERVER = "pypi.python.org" 66 | 67 | STATUSES = {'GREEN': 'Green', 68 | 'YELLOW': 'Yellow', 69 | 'RED': 'Red'} 70 | 71 | 72 | def sort_results_by_age(results): 73 | def compare(x, y): 74 | if x[1] is None and y[1] is None: 75 | return 0 76 | if x[1] is None: 77 | return 1 78 | if y[1] is None: 79 | return -1 80 | return (y[1] > x[1]) - (y[1] < x[1]) 81 | return sorted(results, key=cmp_to_key(compare)) 82 | 83 | 84 | def ping_mirror(mirror_url): 85 | """ Given a mirror_url it will ping it and return the contents and 86 | the response time """ 87 | try: 88 | start = time.time() 89 | res = urlopen(mirror_url) 90 | stop = time.time() 91 | response_time = round((stop - start) * 1000, 2) 92 | return res.read().strip(), response_time 93 | except Exception: 94 | return None, None 95 | 96 | 97 | def parse_date(date_str): 98 | """ parse the date the get back from the mirror """ 99 | 100 | if len(date_str) == 17: 101 | # Used on official mirrors 102 | date_fmt = '%Y%m%dT%H:%M:%S' 103 | else: 104 | # Canonical ISO-8601 format (compliant with PEP 381) 105 | date_fmt = '%Y-%m-%dT%H:%M:%S' 106 | try: 107 | return datetime.datetime.strptime(date_str, date_fmt) 108 | except: 109 | return datetime.datetime.strptime(date_str.decode('utf-8'), date_fmt) 110 | 111 | 112 | def humanize_date_difference(now, other_date=None, offset=None, sign="ago"): 113 | """ This function prints the difference between two python datetime objects 114 | in a more human readable form 115 | """ 116 | 117 | if other_date: 118 | dt = abs(now - other_date) 119 | delta_d, offset = dt.days, dt.seconds 120 | if now < other_date: 121 | sign = "ahead" 122 | elif offset: 123 | delta_d, offset = divmod(offset, 60 * 60 * 24) 124 | else: 125 | raise ValueError("Must supply other_date or offset (from now)") 126 | 127 | offset, delta_s = divmod(offset, 60) 128 | delta_h, delta_m = divmod(offset, 60) 129 | 130 | if delta_d: 131 | fmt = "{d:d} days, {h:d} hours, {m:d} minutes {ago}" 132 | elif delta_h: 133 | fmt = "{h:d} hours, {m:d} minutes {ago}" 134 | elif delta_m: 135 | fmt = "{m:d} minutes, {s:d} seconds {ago}" 136 | else: 137 | fmt = "{s:d} seconds {ago}" 138 | return fmt.format(d=delta_d, h=delta_h, m=delta_m, s=delta_s, ago=sign) 139 | 140 | 141 | def mirror_status_desc(how_old): 142 | """ Get the status description of the mirror """ 143 | 144 | if how_old < datetime.timedelta(minutes=60): 145 | return STATUSES.get('GREEN') 146 | elif how_old < datetime.timedelta(days=1): 147 | return STATUSES.get('YELLOW') 148 | else: 149 | return STATUSES.get('RED') 150 | 151 | 152 | def ping_master_pypi_server(master_url_format=MASTER_URL_FORMAT): 153 | """ Ping the master Pypi server, it is a little different 154 | then the other servers. """ 155 | # a.pypi.python.org is the master server treat it differently 156 | m_url = master_url_format.format(MASTER_SERVER) 157 | res, res_time = ping_mirror(m_url) 158 | return MASTER_SERVER, res, res_time 159 | 160 | 161 | def mirror_statuses(mirror_url_format=MIRROR_URL_FORMAT, 162 | mirrors=None, 163 | ping_master_mirror=True, 164 | sort_by_age=True): 165 | """ get the data we need from the mirrors and return a list of 166 | dictionaries with information about each mirror 167 | 168 | ``mirror_url_format`` - Change the url format from the standard one 169 | 170 | ``mirrors`` - provided the list if mirrors to check. 171 | The list needs to contain a tuple, (protocal, domain) for example: 172 | [('http, 'pypi.example.com'), ('https', 'pypi2.example.com')] 173 | 174 | ``ping_master_mirror`` - Do you want to include the status of the master 175 | server in the results. Defaults to True. 176 | 177 | ``sort_by_age`` - Do you want to sort the results by age, default to True. 178 | 179 | """ 180 | if not mirrors: 181 | return [] 182 | 183 | # scan the mirrors and collect data 184 | ping_results = [] 185 | 186 | p = {m[1]: m[0] for m in mirrors} 187 | 188 | urls = [mirror_url_format.format(protocol, ml) for protocol, ml in mirrors] 189 | results = pool.map(ping_mirror, urls) 190 | if hasattr(pool, 'close'): 191 | pool.close() 192 | pool.join() 193 | ping_results = [(mirrors[i][1], results[i][0], results[i][1]) 194 | for i in range(len(urls))] 195 | 196 | if sort_by_age: 197 | ping_results = sort_results_by_age(ping_results) 198 | 199 | if ping_master_mirror: 200 | # pypi.python.org is the master server treat it differently 201 | master_results = ping_master_pypi_server() 202 | ping_results.insert(0, master_results) 203 | 204 | now = datetime.datetime.utcnow() 205 | results = [] 206 | for ml, res, res_time in ping_results: 207 | if res: 208 | last_update = parse_date(res) 209 | time_diff = abs(now - last_update) 210 | status = mirror_status_desc(time_diff) 211 | time_diff_human = humanize_date_difference(now, last_update) 212 | results.append({ 213 | 'mirror': ml, 214 | 'scheme': p.get(ml, 'https'), 215 | 'last_update': last_update, 216 | 'time_now': now, 217 | 'time_diff': time_diff, 218 | 'time_diff_human': time_diff_human, 219 | 'response_time': res_time, 220 | 'status': status} 221 | ) 222 | else: 223 | results.append({ 224 | 'mirror': ml, 225 | 'scheme': p.get(ml, 'https'), 226 | 'last_update': "Unavailable", 227 | 'time_now': now, 228 | 'time_diff_human': "Unavailable", 229 | 'time_diff': 'Unavailable', 230 | 'response_time': "Unavailable", 231 | 'status': 'Unavailable'} 232 | ) 233 | 234 | return results 235 | 236 | 237 | def is_master_alive(): 238 | """ Check if the Master server is alive """ 239 | server, response, res_time = ping_master_pypi_server() 240 | if response is None: 241 | return False 242 | return True 243 | 244 | 245 | def find_out_of_date_mirrors(mirrors=None): 246 | """ Find the mirrors that are out of date """ 247 | results = mirror_statuses(mirrors=mirrors) 248 | bad_mirrors = [] 249 | for r in results: 250 | if r.get('status') == STATUSES.get('RED'): 251 | bad_mirrors.append(r) 252 | return bad_mirrors 253 | 254 | 255 | def __find_mirror_sort(sort_field, mirrors=None, reverse=False): 256 | """ Find the first mirror that is sorted by sort_field """ 257 | results = mirror_statuses(mirrors=mirrors, ping_master_mirror=False) 258 | new_list = sorted(results, key=operator.itemgetter( 259 | sort_field), reverse=reverse) 260 | return new_list[0] 261 | 262 | 263 | def find_fastest_mirror(mirrors=None): 264 | """ Find the fastest mirror (via response time), might not be up to date """ 265 | return __find_mirror_sort('response_time', mirrors=mirrors) 266 | 267 | 268 | def find_freshest_mirror(mirrors=None): 269 | """ Find the freshest mirror (via last updated) """ 270 | return __find_mirror_sort('time_diff', mirrors=mirrors) 271 | -------------------------------------------------------------------------------- /cron/pypi_mirrors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import json 3 | from mirrorlib import mirror_statuses 4 | 5 | from utils import (cache_key, location_name, get_total_seconds, 6 | get_connection, store_page_data, find_number_of_packages, 7 | get_location_for_mirror, store_json_data) 8 | 9 | from config import MIRRORS 10 | 11 | 12 | def process_results(results): 13 | """ process the results and gather data """ 14 | 15 | conn = get_connection() 16 | new_results = [] 17 | for d in results: 18 | mirror = d.get('mirror') 19 | status = d.get('status') 20 | location = get_location_for_mirror(mirror) 21 | d['location'] = location_name(location) 22 | if status != 'Unavailable': 23 | resp_time = d.get('response_time') 24 | age = get_total_seconds(d.get('time_diff')) 25 | conn.rpush(cache_key('RESPTIME', mirror), resp_time) 26 | conn.rpush(cache_key('AGE', mirror), age) 27 | resp_list = conn.lrange(cache_key('RESPTIME', mirror), -60, -1) 28 | age_list = conn.lrange(cache_key('AGE', mirror), -60, -1) 29 | d['num_packages'] = find_number_of_packages(mirror, d['scheme']) 30 | d['resp_list'] = ",".join(resp_list) 31 | d['age_list'] = ",".join(age_list) 32 | new_results.append(d) 33 | return new_results 34 | 35 | 36 | def json_results(data): 37 | results = {} 38 | for mirror in data: 39 | results[mirror.get('mirror')] = { 40 | 'status': mirror.get('status', 'n/a'), 41 | 'location': mirror.get('location', 'n/a'), 42 | 'num_packages': mirror.get('num_packages', 'n/a'), 43 | 'last_updated': mirror.get('time_diff_human', 'n/a'), 44 | } 45 | return json.dumps(results) 46 | 47 | 48 | def run(): 49 | """ run everything """ 50 | results = mirror_statuses(mirrors=MIRRORS) 51 | if results: 52 | time_now = results[0].get('time_now', None) 53 | data = process_results(results) 54 | json_data = json_results(data) 55 | 56 | store_json_data(json_data) 57 | store_page_data(data, time_now) 58 | 59 | 60 | if __name__ == '__main__': 61 | run() -------------------------------------------------------------------------------- /cron/test_run.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from jinja2 import Environment, FileSystemLoader 4 | from mirrorlib import mirror_statuses 5 | 6 | from utils import (find_number_of_packages) 7 | 8 | from config import MIRRORS 9 | 10 | 11 | def process_results(results): 12 | """ process the results and gather data """ 13 | 14 | new_results = [] 15 | for d in results: 16 | mirror = d.get('mirror') 17 | status = d.get('status') 18 | d['location'] = "n/a" 19 | if status != 'Unavailable': 20 | resp_list = ["1", "2", "3", "4", "5", "6", 21 | "7", "8", "9", "10"] # faked out for test 22 | age_list = ["1", "2", "3", "4", "5", "6", 23 | "7", "8", "9", "10"] # faked out for test 24 | d['num_packages'] = find_number_of_packages(mirror, d['scheme']) 25 | d['resp_list'] = ",".join(resp_list) 26 | d['age_list'] = ",".join(age_list) 27 | new_results.append(d) 28 | return new_results 29 | 30 | 31 | def url_for(something): 32 | return something 33 | 34 | 35 | def run(): 36 | """ run everything """ 37 | results = mirror_statuses(mirrors=MIRRORS) 38 | if results: 39 | time_now = results[0].get('time_now', None) 40 | data = process_results(results) 41 | 42 | env = Environment(loader=FileSystemLoader('templates')) 43 | # add the dummy url_for so it doesn't throw error. 44 | env.globals.update(url_for=url_for) 45 | template = env.get_template('index.html') 46 | context = {'data': data, 'date_now': time_now} 47 | print template.render(**context) 48 | 49 | 50 | if __name__ == '__main__': 51 | run() 52 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | location /static/ { root /home/dotcloud/current; } -------------------------------------------------------------------------------- /notification.py: -------------------------------------------------------------------------------- 1 | import tweepy 2 | import smtplib 3 | 4 | from config import load_config, EMAIL_OVERRIDE 5 | 6 | CONFIG = load_config() 7 | 8 | 9 | def prepare_twitter_message(status): 10 | """ shrink to the right size and add link to site. """ 11 | link = "https://www.pypi-mirrors.org" 12 | link_len = len(link) + 4 13 | message_len = 140 - link_len 14 | status_new = status[:message_len] 15 | if len(status) > message_len: 16 | status_new += "..." 17 | status_new += " {0}".format(link) 18 | return status_new 19 | 20 | 21 | def update_twitter_status(status): 22 | """ update the twitter account's status """ 23 | 24 | consumer_key = CONFIG.get('twitter_consumer_key') 25 | consumer_secret = CONFIG.get('twitter_consumer_secret') 26 | 27 | access_token = CONFIG.get('twitter_access_key') 28 | access_token_secret = CONFIG.get('twitter_access_secret') 29 | 30 | message = prepare_twitter_message(status) 31 | 32 | auth = tweepy.OAuthHandler(consumer_key, consumer_secret) 33 | auth.set_access_token(access_token, access_token_secret) 34 | api = tweepy.API(auth) 35 | api.update_status(message) 36 | 37 | 38 | def send_warning_email(message): 39 | """ send a message saying a mirror(s) is out of date. """ 40 | email_to = CONFIG.get('email_to') 41 | email_from = CONFIG.get('email_from') 42 | email_template = '''Subject: [pypi-mirrors] Mirror is out of Date Notice 43 | 44 | This is an automated email from https://www.pypi-mirrors.org to let you 45 | know that the following mirrors are out of date. 46 | 47 | {message} 48 | 49 | -- 50 | This automated message is sent to you by https://www.pypi-mirrors.org If you no 51 | longer want to receive these emails, please contact Yuwei Ba(@ibigbug) on twitter 52 | or reply to this email. 53 | ''' 54 | email_body = email_template.format(message=message) 55 | 56 | send_email(email_body, email_to, email_from) 57 | 58 | 59 | def send_status_email(message): 60 | """ send a daily status message """ 61 | email_to = CONFIG.get('email_to_admin') 62 | email_from = CONFIG.get('email_from') 63 | email_template = '''Subject: [pypi-mirrors] Mirrors are all up to date 64 | 65 | This is an automated email from https://www.pypi-mirrors.org to let you 66 | know that the following mirrors are all up to date. 67 | 68 | {message} 69 | -- 70 | This automated message is sent to you by https://www.pypi-mirrors.org If you no 71 | longer want to receive these emails, please contact Yuwei Ba(@ibigbug) on twitter 72 | or reply to this email. 73 | ''' 74 | 75 | email_body = email_template.format(message=message) 76 | 77 | send_email(email_body, email_to, email_from) 78 | 79 | 80 | def send_email(email_body, email_to, email_from): 81 | """ Send an email using the configuration provided """ 82 | email_host = CONFIG.get('email_host') 83 | email_port = CONFIG.get('email_port') 84 | email_user = CONFIG.get('email_user') 85 | email_password = CONFIG.get('email_password') 86 | email_bcc = CONFIG.get('email_bcc') 87 | 88 | if EMAIL_OVERRIDE: 89 | print 'Over-riding email with {0}.'.format(EMAIL_OVERRIDE) 90 | email = EMAIL_OVERRIDE 91 | else: 92 | email = email_to 93 | 94 | print("email to {0} , bcc: {1}; from {2}".format( 95 | email, email_bcc, email_from)) 96 | smtp = smtplib.SMTP(email_host, email_port) 97 | smtp.starttls() 98 | smtp.login(email_user, email_password) 99 | smtp.sendmail(email_from, [email, email_bcc], email_body) 100 | smtp.quit() 101 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | redis 2 | hiredis 3 | requests 4 | lxml 5 | flask 6 | tweepy 7 | uwsgi 8 | raven==6.1.0 9 | gevent 10 | prometheus-client==0.0.20 11 | apscheduler==3.3.1 12 | -------------------------------------------------------------------------------- /services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibigbug/pypi-mirrors/0c7f4414b0bc4291b36f40c790241281d164053a/services/__init__.py -------------------------------------------------------------------------------- /services/errors.py: -------------------------------------------------------------------------------- 1 | import os 2 | from raven import Client 3 | 4 | sentry = Client(os.environ.get('SENTRY_DSN')) 5 | 6 | 7 | if __name__ == '__main__': 8 | try: 9 | 1 / 0 10 | except: 11 | sentry.captureException() 12 | -------------------------------------------------------------------------------- /services/iploc.py: -------------------------------------------------------------------------------- 1 | from config import PY2 2 | 3 | import json 4 | import urllib 5 | 6 | if PY2: 7 | from urllib2 import urlopen 8 | else: 9 | from urllib.request import urlopen 10 | 11 | 12 | def get_city(apikey, ip): 13 | """ get city location for an ip """ 14 | base_url = "http://api.ipinfodb.com/v3/ip-city/" 15 | variables = {"format": "json", 16 | "key": apikey, 17 | "ip": ip, } 18 | 19 | urldata = urllib.urlencode(variables) 20 | url = "{0}?{1}".format(base_url, urldata) 21 | urlobj = urlopen(url) 22 | data = urlobj.read() 23 | urlobj.close() 24 | return json.loads(data) 25 | -------------------------------------------------------------------------------- /services/metrics.py: -------------------------------------------------------------------------------- 1 | from prometheus_client import Counter, Histogram 2 | 3 | 4 | req_counter = Counter( 5 | 'http_requests_total', 6 | 'HTTP requests total', 7 | ['method', 'endpoint', 'status'] 8 | ) 9 | rt_histogram = Histogram( 10 | 'http_response_time_seconds', 11 | 'HTTP response time seconds', 12 | ['method', 'endpoint', 'status'] 13 | ) 14 | -------------------------------------------------------------------------------- /static/404.html: -------------------------------------------------------------------------------- 1 | 404 - Page not found -------------------------------------------------------------------------------- /static/500.html: -------------------------------------------------------------------------------- 1 | 500 - Server error -------------------------------------------------------------------------------- /static/502.html: -------------------------------------------------------------------------------- 1 | 502 error -------------------------------------------------------------------------------- /static/503.html: -------------------------------------------------------------------------------- 1 | 503 error -------------------------------------------------------------------------------- /static/504.html: -------------------------------------------------------------------------------- 1 | 504 error -------------------------------------------------------------------------------- /static/css/bootstrap.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v2.0.2 3 | * 4 | * Copyright 2012 Twitter, Inc 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Designed and built with all the love in the world @twitter by @mdo and @fat. 9 | */ 10 | .clearfix{*zoom:1;}.clearfix:before,.clearfix:after{display:table;content:"";} 11 | .clearfix:after{clear:both;} 12 | .hide-text{overflow:hidden;text-indent:100%;white-space:nowrap;} 13 | .input-block-level{display:block;width:100%;min-height:28px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;} 14 | article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block;} 15 | audio,canvas,video{display:inline-block;*display:inline;*zoom:1;} 16 | audio:not([controls]){display:none;} 17 | html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;} 18 | a:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px;} 19 | a:hover,a:active{outline:0;} 20 | sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline;} 21 | sup{top:-0.5em;} 22 | sub{bottom:-0.25em;} 23 | img{height:auto;border:0;-ms-interpolation-mode:bicubic;vertical-align:middle;} 24 | button,input,select,textarea{margin:0;font-size:100%;vertical-align:middle;} 25 | button,input{*overflow:visible;line-height:normal;} 26 | button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0;} 27 | button,input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button;} 28 | input[type="search"]{-webkit-appearance:textfield;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;} 29 | input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button{-webkit-appearance:none;} 30 | textarea{overflow:auto;vertical-align:top;} 31 | body{margin:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;line-height:18px;color:#333333;background-color:#ffffff;} 32 | a{color:#0088cc;text-decoration:none;} 33 | a:hover{color:#005580;text-decoration:underline;} 34 | .row{margin-left:-20px;*zoom:1;}.row:before,.row:after{display:table;content:"";} 35 | .row:after{clear:both;} 36 | [class*="span"]{float:left;margin-left:20px;} 37 | .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px;} 38 | .span12{width:940px;} 39 | .span11{width:860px;} 40 | .span10{width:780px;} 41 | .span9{width:700px;} 42 | .span8{width:620px;} 43 | .span7{width:540px;} 44 | .span6{width:460px;} 45 | .span5{width:380px;} 46 | .span4{width:300px;} 47 | .span3{width:220px;} 48 | .span2{width:140px;} 49 | .span1{width:60px;} 50 | .offset12{margin-left:980px;} 51 | .offset11{margin-left:900px;} 52 | .offset10{margin-left:820px;} 53 | .offset9{margin-left:740px;} 54 | .offset8{margin-left:660px;} 55 | .offset7{margin-left:580px;} 56 | .offset6{margin-left:500px;} 57 | .offset5{margin-left:420px;} 58 | .offset4{margin-left:340px;} 59 | .offset3{margin-left:260px;} 60 | .offset2{margin-left:180px;} 61 | .offset1{margin-left:100px;} 62 | .row-fluid{width:100%;*zoom:1;}.row-fluid:before,.row-fluid:after{display:table;content:"";} 63 | .row-fluid:after{clear:both;} 64 | .row-fluid>[class*="span"]{float:left;margin-left:2.127659574%;} 65 | .row-fluid>[class*="span"]:first-child{margin-left:0;} 66 | .row-fluid > .span12{width:99.99999998999999%;} 67 | .row-fluid > .span11{width:91.489361693%;} 68 | .row-fluid > .span10{width:82.97872339599999%;} 69 | .row-fluid > .span9{width:74.468085099%;} 70 | .row-fluid > .span8{width:65.95744680199999%;} 71 | .row-fluid > .span7{width:57.446808505%;} 72 | .row-fluid > .span6{width:48.93617020799999%;} 73 | .row-fluid > .span5{width:40.425531911%;} 74 | .row-fluid > .span4{width:31.914893614%;} 75 | .row-fluid > .span3{width:23.404255317%;} 76 | .row-fluid > .span2{width:14.89361702%;} 77 | .row-fluid > .span1{width:6.382978723%;} 78 | .container{margin-left:auto;margin-right:auto;*zoom:1;}.container:before,.container:after{display:table;content:"";} 79 | .container:after{clear:both;} 80 | .container-fluid{padding-left:20px;padding-right:20px;*zoom:1;}.container-fluid:before,.container-fluid:after{display:table;content:"";} 81 | .container-fluid:after{clear:both;} 82 | p{margin:0 0 9px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;line-height:18px;}p small{font-size:11px;color:#999999;} 83 | .lead{margin-bottom:18px;font-size:20px;font-weight:200;line-height:27px;} 84 | h1,h2,h3,h4,h5,h6{margin:0;font-family:inherit;font-weight:bold;color:inherit;text-rendering:optimizelegibility;}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{font-weight:normal;color:#999999;} 85 | h1{font-size:30px;line-height:36px;}h1 small{font-size:18px;} 86 | h2{font-size:24px;line-height:36px;}h2 small{font-size:18px;} 87 | h3{line-height:27px;font-size:18px;}h3 small{font-size:14px;} 88 | h4,h5,h6{line-height:18px;} 89 | h4{font-size:14px;}h4 small{font-size:12px;} 90 | h5{font-size:12px;} 91 | h6{font-size:11px;color:#999999;text-transform:uppercase;} 92 | .page-header{padding-bottom:17px;margin:18px 0;border-bottom:1px solid #eeeeee;} 93 | .page-header h1{line-height:1;} 94 | ul,ol{padding:0;margin:0 0 9px 25px;} 95 | ul ul,ul ol,ol ol,ol ul{margin-bottom:0;} 96 | ul{list-style:disc;} 97 | ol{list-style:decimal;} 98 | li{line-height:18px;} 99 | ul.unstyled,ol.unstyled{margin-left:0;list-style:none;} 100 | dl{margin-bottom:18px;} 101 | dt,dd{line-height:18px;} 102 | dt{font-weight:bold;line-height:17px;} 103 | dd{margin-left:9px;} 104 | .dl-horizontal dt{float:left;clear:left;width:120px;text-align:right;} 105 | .dl-horizontal dd{margin-left:130px;} 106 | hr{margin:18px 0;border:0;border-top:1px solid #eeeeee;border-bottom:1px solid #ffffff;} 107 | strong{font-weight:bold;} 108 | em{font-style:italic;} 109 | .muted{color:#999999;} 110 | abbr[title]{border-bottom:1px dotted #ddd;cursor:help;} 111 | abbr.initialism{font-size:90%;text-transform:uppercase;} 112 | blockquote{padding:0 0 0 15px;margin:0 0 18px;border-left:5px solid #eeeeee;}blockquote p{margin-bottom:0;font-size:16px;font-weight:300;line-height:22.5px;} 113 | blockquote small{display:block;line-height:18px;color:#999999;}blockquote small:before{content:'\2014 \00A0';} 114 | blockquote.pull-right{float:right;padding-left:0;padding-right:15px;border-left:0;border-right:5px solid #eeeeee;}blockquote.pull-right p,blockquote.pull-right small{text-align:right;} 115 | q:before,q:after,blockquote:before,blockquote:after{content:"";} 116 | address{display:block;margin-bottom:18px;line-height:18px;font-style:normal;} 117 | small{font-size:100%;} 118 | cite{font-style:normal;} 119 | code,pre{padding:0 3px 2px;font-family:Menlo,Monaco,"Courier New",monospace;font-size:12px;color:#333333;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;} 120 | code{padding:2px 4px;color:#d14;background-color:#f7f7f9;border:1px solid #e1e1e8;} 121 | pre{display:block;padding:8.5px;margin:0 0 9px;font-size:12.025px;line-height:18px;background-color:#f5f5f5;border:1px solid #ccc;border:1px solid rgba(0, 0, 0, 0.15);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;white-space:pre;white-space:pre-wrap;word-break:break-all;word-wrap:break-word;}pre.prettyprint{margin-bottom:18px;} 122 | pre code{padding:0;color:inherit;background-color:transparent;border:0;} 123 | .pre-scrollable{max-height:340px;overflow-y:scroll;} 124 | .label{padding:1px 4px 2px;font-size:10.998px;font-weight:bold;line-height:13px;color:#ffffff;vertical-align:middle;white-space:nowrap;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#999999;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;} 125 | .label:hover{color:#ffffff;text-decoration:none;} 126 | .label-important{background-color:#b94a48;} 127 | .label-important:hover{background-color:#953b39;} 128 | .label-warning{background-color:#f89406;} 129 | .label-warning:hover{background-color:#c67605;} 130 | .label-success{background-color:#468847;} 131 | .label-success:hover{background-color:#356635;} 132 | .label-info{background-color:#3a87ad;} 133 | .label-info:hover{background-color:#2d6987;} 134 | .label-inverse{background-color:#333333;} 135 | .label-inverse:hover{background-color:#1a1a1a;} 136 | .badge{padding:1px 9px 2px;font-size:12.025px;font-weight:bold;white-space:nowrap;color:#ffffff;background-color:#999999;-webkit-border-radius:9px;-moz-border-radius:9px;border-radius:9px;} 137 | .badge:hover{color:#ffffff;text-decoration:none;cursor:pointer;} 138 | .badge-error{background-color:#b94a48;} 139 | .badge-error:hover{background-color:#953b39;} 140 | .badge-warning{background-color:#f89406;} 141 | .badge-warning:hover{background-color:#c67605;} 142 | .badge-success{background-color:#468847;} 143 | .badge-success:hover{background-color:#356635;} 144 | .badge-info{background-color:#3a87ad;} 145 | .badge-info:hover{background-color:#2d6987;} 146 | .badge-inverse{background-color:#333333;} 147 | .badge-inverse:hover{background-color:#1a1a1a;} 148 | table{max-width:100%;border-collapse:collapse;border-spacing:0;background-color:transparent;} 149 | .table{width:100%;margin-bottom:18px;}.table th,.table td{padding:8px;line-height:18px;text-align:left;vertical-align:top;border-top:1px solid #dddddd;} 150 | .table th{font-weight:bold;} 151 | .table thead th{vertical-align:bottom;} 152 | .table colgroup+thead tr:first-child th,.table colgroup+thead tr:first-child td,.table thead:first-child tr:first-child th,.table thead:first-child tr:first-child td{border-top:0;} 153 | .table tbody+tbody{border-top:2px solid #dddddd;} 154 | .table-condensed th,.table-condensed td{padding:4px 5px;} 155 | .table-bordered{border:1px solid #dddddd;border-left:0;border-collapse:separate;*border-collapse:collapsed;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}.table-bordered th,.table-bordered td{border-left:1px solid #dddddd;} 156 | .table-bordered thead:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child td{border-top:0;} 157 | .table-bordered thead:first-child tr:first-child th:first-child,.table-bordered tbody:first-child tr:first-child td:first-child{-webkit-border-radius:4px 0 0 0;-moz-border-radius:4px 0 0 0;border-radius:4px 0 0 0;} 158 | .table-bordered thead:first-child tr:first-child th:last-child,.table-bordered tbody:first-child tr:first-child td:last-child{-webkit-border-radius:0 4px 0 0;-moz-border-radius:0 4px 0 0;border-radius:0 4px 0 0;} 159 | .table-bordered thead:last-child tr:last-child th:first-child,.table-bordered tbody:last-child tr:last-child td:first-child{-webkit-border-radius:0 0 0 4px;-moz-border-radius:0 0 0 4px;border-radius:0 0 0 4px;} 160 | .table-bordered thead:last-child tr:last-child th:last-child,.table-bordered tbody:last-child tr:last-child td:last-child{-webkit-border-radius:0 0 4px 0;-moz-border-radius:0 0 4px 0;border-radius:0 0 4px 0;} 161 | .table-striped tbody tr:nth-child(odd) td,.table-striped tbody tr:nth-child(odd) th{background-color:#f9f9f9;} 162 | .table tbody tr:hover td,.table tbody tr:hover th{background-color:#f5f5f5;} 163 | table .span1{float:none;width:44px;margin-left:0;} 164 | table .span2{float:none;width:124px;margin-left:0;} 165 | table .span3{float:none;width:204px;margin-left:0;} 166 | table .span4{float:none;width:284px;margin-left:0;} 167 | table .span5{float:none;width:364px;margin-left:0;} 168 | table .span6{float:none;width:444px;margin-left:0;} 169 | table .span7{float:none;width:524px;margin-left:0;} 170 | table .span8{float:none;width:604px;margin-left:0;} 171 | table .span9{float:none;width:684px;margin-left:0;} 172 | table .span10{float:none;width:764px;margin-left:0;} 173 | table .span11{float:none;width:844px;margin-left:0;} 174 | table .span12{float:none;width:924px;margin-left:0;} 175 | table .span13{float:none;width:1004px;margin-left:0;} 176 | table .span14{float:none;width:1084px;margin-left:0;} 177 | table .span15{float:none;width:1164px;margin-left:0;} 178 | table .span16{float:none;width:1244px;margin-left:0;} 179 | table .span17{float:none;width:1324px;margin-left:0;} 180 | table .span18{float:none;width:1404px;margin-left:0;} 181 | table .span19{float:none;width:1484px;margin-left:0;} 182 | table .span20{float:none;width:1564px;margin-left:0;} 183 | table .span21{float:none;width:1644px;margin-left:0;} 184 | table .span22{float:none;width:1724px;margin-left:0;} 185 | table .span23{float:none;width:1804px;margin-left:0;} 186 | table .span24{float:none;width:1884px;margin-left:0;} 187 | [class^="icon-"],[class*=" icon-"]{display:inline-block;width:14px;height:14px;line-height:14px;vertical-align:text-top;background-image:url("../img/glyphicons-halflings.png");background-position:14px 14px;background-repeat:no-repeat;*margin-right:.3em;}[class^="icon-"]:last-child,[class*=" icon-"]:last-child{*margin-left:0;} 188 | .icon-white{background-image:url("../img/glyphicons-halflings-white.png");} 189 | .icon-glass{background-position:0 0;} 190 | .icon-music{background-position:-24px 0;} 191 | .icon-search{background-position:-48px 0;} 192 | .icon-envelope{background-position:-72px 0;} 193 | .icon-heart{background-position:-96px 0;} 194 | .icon-star{background-position:-120px 0;} 195 | .icon-star-empty{background-position:-144px 0;} 196 | .icon-user{background-position:-168px 0;} 197 | .icon-film{background-position:-192px 0;} 198 | .icon-th-large{background-position:-216px 0;} 199 | .icon-th{background-position:-240px 0;} 200 | .icon-th-list{background-position:-264px 0;} 201 | .icon-ok{background-position:-288px 0;} 202 | .icon-remove{background-position:-312px 0;} 203 | .icon-zoom-in{background-position:-336px 0;} 204 | .icon-zoom-out{background-position:-360px 0;} 205 | .icon-off{background-position:-384px 0;} 206 | .icon-signal{background-position:-408px 0;} 207 | .icon-cog{background-position:-432px 0;} 208 | .icon-trash{background-position:-456px 0;} 209 | .icon-home{background-position:0 -24px;} 210 | .icon-file{background-position:-24px -24px;} 211 | .icon-time{background-position:-48px -24px;} 212 | .icon-road{background-position:-72px -24px;} 213 | .icon-download-alt{background-position:-96px -24px;} 214 | .icon-download{background-position:-120px -24px;} 215 | .icon-upload{background-position:-144px -24px;} 216 | .icon-inbox{background-position:-168px -24px;} 217 | .icon-play-circle{background-position:-192px -24px;} 218 | .icon-repeat{background-position:-216px -24px;} 219 | .icon-refresh{background-position:-240px -24px;} 220 | .icon-list-alt{background-position:-264px -24px;} 221 | .icon-lock{background-position:-287px -24px;} 222 | .icon-flag{background-position:-312px -24px;} 223 | .icon-headphones{background-position:-336px -24px;} 224 | .icon-volume-off{background-position:-360px -24px;} 225 | .icon-volume-down{background-position:-384px -24px;} 226 | .icon-volume-up{background-position:-408px -24px;} 227 | .icon-qrcode{background-position:-432px -24px;} 228 | .icon-barcode{background-position:-456px -24px;} 229 | .icon-tag{background-position:0 -48px;} 230 | .icon-tags{background-position:-25px -48px;} 231 | .icon-book{background-position:-48px -48px;} 232 | .icon-bookmark{background-position:-72px -48px;} 233 | .icon-print{background-position:-96px -48px;} 234 | .icon-camera{background-position:-120px -48px;} 235 | .icon-font{background-position:-144px -48px;} 236 | .icon-bold{background-position:-167px -48px;} 237 | .icon-italic{background-position:-192px -48px;} 238 | .icon-text-height{background-position:-216px -48px;} 239 | .icon-text-width{background-position:-240px -48px;} 240 | .icon-align-left{background-position:-264px -48px;} 241 | .icon-align-center{background-position:-288px -48px;} 242 | .icon-align-right{background-position:-312px -48px;} 243 | .icon-align-justify{background-position:-336px -48px;} 244 | .icon-list{background-position:-360px -48px;} 245 | .icon-indent-left{background-position:-384px -48px;} 246 | .icon-indent-right{background-position:-408px -48px;} 247 | .icon-facetime-video{background-position:-432px -48px;} 248 | .icon-picture{background-position:-456px -48px;} 249 | .icon-pencil{background-position:0 -72px;} 250 | .icon-map-marker{background-position:-24px -72px;} 251 | .icon-adjust{background-position:-48px -72px;} 252 | .icon-tint{background-position:-72px -72px;} 253 | .icon-edit{background-position:-96px -72px;} 254 | .icon-share{background-position:-120px -72px;} 255 | .icon-check{background-position:-144px -72px;} 256 | .icon-move{background-position:-168px -72px;} 257 | .icon-step-backward{background-position:-192px -72px;} 258 | .icon-fast-backward{background-position:-216px -72px;} 259 | .icon-backward{background-position:-240px -72px;} 260 | .icon-play{background-position:-264px -72px;} 261 | .icon-pause{background-position:-288px -72px;} 262 | .icon-stop{background-position:-312px -72px;} 263 | .icon-forward{background-position:-336px -72px;} 264 | .icon-fast-forward{background-position:-360px -72px;} 265 | .icon-step-forward{background-position:-384px -72px;} 266 | .icon-eject{background-position:-408px -72px;} 267 | .icon-chevron-left{background-position:-432px -72px;} 268 | .icon-chevron-right{background-position:-456px -72px;} 269 | .icon-plus-sign{background-position:0 -96px;} 270 | .icon-minus-sign{background-position:-24px -96px;} 271 | .icon-remove-sign{background-position:-48px -96px;} 272 | .icon-ok-sign{background-position:-72px -96px;} 273 | .icon-question-sign{background-position:-96px -96px;} 274 | .icon-info-sign{background-position:-120px -96px;} 275 | .icon-screenshot{background-position:-144px -96px;} 276 | .icon-remove-circle{background-position:-168px -96px;} 277 | .icon-ok-circle{background-position:-192px -96px;} 278 | .icon-ban-circle{background-position:-216px -96px;} 279 | .icon-arrow-left{background-position:-240px -96px;} 280 | .icon-arrow-right{background-position:-264px -96px;} 281 | .icon-arrow-up{background-position:-289px -96px;} 282 | .icon-arrow-down{background-position:-312px -96px;} 283 | .icon-share-alt{background-position:-336px -96px;} 284 | .icon-resize-full{background-position:-360px -96px;} 285 | .icon-resize-small{background-position:-384px -96px;} 286 | .icon-plus{background-position:-408px -96px;} 287 | .icon-minus{background-position:-433px -96px;} 288 | .icon-asterisk{background-position:-456px -96px;} 289 | .icon-exclamation-sign{background-position:0 -120px;} 290 | .icon-gift{background-position:-24px -120px;} 291 | .icon-leaf{background-position:-48px -120px;} 292 | .icon-fire{background-position:-72px -120px;} 293 | .icon-eye-open{background-position:-96px -120px;} 294 | .icon-eye-close{background-position:-120px -120px;} 295 | .icon-warning-sign{background-position:-144px -120px;} 296 | .icon-plane{background-position:-168px -120px;} 297 | .icon-calendar{background-position:-192px -120px;} 298 | .icon-random{background-position:-216px -120px;} 299 | .icon-comment{background-position:-240px -120px;} 300 | .icon-magnet{background-position:-264px -120px;} 301 | .icon-chevron-up{background-position:-288px -120px;} 302 | .icon-chevron-down{background-position:-313px -119px;} 303 | .icon-retweet{background-position:-336px -120px;} 304 | .icon-shopping-cart{background-position:-360px -120px;} 305 | .icon-folder-close{background-position:-384px -120px;} 306 | .icon-folder-open{background-position:-408px -120px;} 307 | .icon-resize-vertical{background-position:-432px -119px;} 308 | .icon-resize-horizontal{background-position:-456px -118px;} 309 | .btn-group{position:relative;*zoom:1;*margin-left:.3em;}.btn-group:before,.btn-group:after{display:table;content:"";} 310 | .btn-group:after{clear:both;} 311 | .btn-group:first-child{*margin-left:0;} 312 | .btn-group+.btn-group{margin-left:5px;} 313 | .btn-toolbar{margin-top:9px;margin-bottom:9px;}.btn-toolbar .btn-group{display:inline-block;*display:inline;*zoom:1;} 314 | .btn-group .btn{position:relative;float:left;margin-left:-1px;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;} 315 | .btn-group .btn:first-child{margin-left:0;-webkit-border-top-left-radius:4px;-moz-border-radius-topleft:4px;border-top-left-radius:4px;-webkit-border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px;border-bottom-left-radius:4px;} 316 | .btn-group .btn:last-child,.btn-group .dropdown-toggle{-webkit-border-top-right-radius:4px;-moz-border-radius-topright:4px;border-top-right-radius:4px;-webkit-border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px;border-bottom-right-radius:4px;} 317 | .btn-group .btn.large:first-child{margin-left:0;-webkit-border-top-left-radius:6px;-moz-border-radius-topleft:6px;border-top-left-radius:6px;-webkit-border-bottom-left-radius:6px;-moz-border-radius-bottomleft:6px;border-bottom-left-radius:6px;} 318 | .btn-group .btn.large:last-child,.btn-group .large.dropdown-toggle{-webkit-border-top-right-radius:6px;-moz-border-radius-topright:6px;border-top-right-radius:6px;-webkit-border-bottom-right-radius:6px;-moz-border-radius-bottomright:6px;border-bottom-right-radius:6px;} 319 | .btn-group .btn:hover,.btn-group .btn:focus,.btn-group .btn:active,.btn-group .btn.active{z-index:2;} 320 | .btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0;} 321 | .btn-group .dropdown-toggle{padding-left:8px;padding-right:8px;-webkit-box-shadow:inset 1px 0 0 rgba(255, 255, 255, 0.125),inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 1px 0 0 rgba(255, 255, 255, 0.125),inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 1px 0 0 rgba(255, 255, 255, 0.125),inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);*padding-top:3px;*padding-bottom:3px;} 322 | .btn-group .btn-mini.dropdown-toggle{padding-left:5px;padding-right:5px;*padding-top:1px;*padding-bottom:1px;} 323 | .btn-group .btn-small.dropdown-toggle{*padding-top:4px;*padding-bottom:4px;} 324 | .btn-group .btn-large.dropdown-toggle{padding-left:12px;padding-right:12px;} 325 | .btn-group.open{*z-index:1000;}.btn-group.open .dropdown-menu{display:block;margin-top:1px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;} 326 | .btn-group.open .dropdown-toggle{background-image:none;-webkit-box-shadow:inset 0 1px 6px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 1px 6px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 0 1px 6px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);} 327 | .btn .caret{margin-top:7px;margin-left:0;} 328 | .btn:hover .caret,.open.btn-group .caret{opacity:1;filter:alpha(opacity=100);} 329 | .btn-mini .caret{margin-top:5px;} 330 | .btn-small .caret{margin-top:6px;} 331 | .btn-large .caret{margin-top:6px;border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid #000000;} 332 | .btn-primary .caret,.btn-warning .caret,.btn-danger .caret,.btn-info .caret,.btn-success .caret,.btn-inverse .caret{border-top-color:#ffffff;border-bottom-color:#ffffff;opacity:0.75;filter:alpha(opacity=75);} 333 | .nav{margin-left:0;margin-bottom:18px;list-style:none;} 334 | .nav>li>a{display:block;} 335 | .nav>li>a:hover{text-decoration:none;background-color:#eeeeee;} 336 | .nav .nav-header{display:block;padding:3px 15px;font-size:11px;font-weight:bold;line-height:18px;color:#999999;text-shadow:0 1px 0 rgba(255, 255, 255, 0.5);text-transform:uppercase;} 337 | .nav li+.nav-header{margin-top:9px;} 338 | .nav-list{padding-left:15px;padding-right:15px;margin-bottom:0;} 339 | .nav-list>li>a,.nav-list .nav-header{margin-left:-15px;margin-right:-15px;text-shadow:0 1px 0 rgba(255, 255, 255, 0.5);} 340 | .nav-list>li>a{padding:3px 15px;} 341 | .nav-list>.active>a,.nav-list>.active>a:hover{color:#ffffff;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.2);background-color:#0088cc;} 342 | .nav-list [class^="icon-"]{margin-right:2px;} 343 | .nav-list .divider{height:1px;margin:8px 1px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #ffffff;*width:100%;*margin:-5px 0 5px;} 344 | .nav-tabs,.nav-pills{*zoom:1;}.nav-tabs:before,.nav-pills:before,.nav-tabs:after,.nav-pills:after{display:table;content:"";} 345 | .nav-tabs:after,.nav-pills:after{clear:both;} 346 | .nav-tabs>li,.nav-pills>li{float:left;} 347 | .nav-tabs>li>a,.nav-pills>li>a{padding-right:12px;padding-left:12px;margin-right:2px;line-height:14px;} 348 | .nav-tabs{border-bottom:1px solid #ddd;} 349 | .nav-tabs>li{margin-bottom:-1px;} 350 | .nav-tabs>li>a{padding-top:8px;padding-bottom:8px;line-height:18px;border:1px solid transparent;-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0;}.nav-tabs>li>a:hover{border-color:#eeeeee #eeeeee #dddddd;} 351 | .nav-tabs>.active>a,.nav-tabs>.active>a:hover{color:#555555;background-color:#ffffff;border:1px solid #ddd;border-bottom-color:transparent;cursor:default;} 352 | .nav-pills>li>a{padding-top:8px;padding-bottom:8px;margin-top:2px;margin-bottom:2px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;} 353 | .nav-pills>.active>a,.nav-pills>.active>a:hover{color:#ffffff;background-color:#0088cc;} 354 | .nav-stacked>li{float:none;} 355 | .nav-stacked>li>a{margin-right:0;} 356 | .nav-tabs.nav-stacked{border-bottom:0;} 357 | .nav-tabs.nav-stacked>li>a{border:1px solid #ddd;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;} 358 | .nav-tabs.nav-stacked>li:first-child>a{-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0;} 359 | .nav-tabs.nav-stacked>li:last-child>a{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px;} 360 | .nav-tabs.nav-stacked>li>a:hover{border-color:#ddd;z-index:2;} 361 | .nav-pills.nav-stacked>li>a{margin-bottom:3px;} 362 | .nav-pills.nav-stacked>li:last-child>a{margin-bottom:1px;} 363 | .nav-tabs .dropdown-menu,.nav-pills .dropdown-menu{margin-top:1px;border-width:1px;} 364 | .nav-pills .dropdown-menu{-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;} 365 | .nav-tabs .dropdown-toggle .caret,.nav-pills .dropdown-toggle .caret{border-top-color:#0088cc;border-bottom-color:#0088cc;margin-top:6px;} 366 | .nav-tabs .dropdown-toggle:hover .caret,.nav-pills .dropdown-toggle:hover .caret{border-top-color:#005580;border-bottom-color:#005580;} 367 | .nav-tabs .active .dropdown-toggle .caret,.nav-pills .active .dropdown-toggle .caret{border-top-color:#333333;border-bottom-color:#333333;} 368 | .nav>.dropdown.active>a:hover{color:#000000;cursor:pointer;} 369 | .nav-tabs .open .dropdown-toggle,.nav-pills .open .dropdown-toggle,.nav>.open.active>a:hover{color:#ffffff;background-color:#999999;border-color:#999999;} 370 | .nav .open .caret,.nav .open.active .caret,.nav .open a:hover .caret{border-top-color:#ffffff;border-bottom-color:#ffffff;opacity:1;filter:alpha(opacity=100);} 371 | .tabs-stacked .open>a:hover{border-color:#999999;} 372 | .tabbable{*zoom:1;}.tabbable:before,.tabbable:after{display:table;content:"";} 373 | .tabbable:after{clear:both;} 374 | .tab-content{display:table;width:100%;} 375 | .tabs-below .nav-tabs,.tabs-right .nav-tabs,.tabs-left .nav-tabs{border-bottom:0;} 376 | .tab-content>.tab-pane,.pill-content>.pill-pane{display:none;} 377 | .tab-content>.active,.pill-content>.active{display:block;} 378 | .tabs-below .nav-tabs{border-top:1px solid #ddd;} 379 | .tabs-below .nav-tabs>li{margin-top:-1px;margin-bottom:0;} 380 | .tabs-below .nav-tabs>li>a{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px;}.tabs-below .nav-tabs>li>a:hover{border-bottom-color:transparent;border-top-color:#ddd;} 381 | .tabs-below .nav-tabs .active>a,.tabs-below .nav-tabs .active>a:hover{border-color:transparent #ddd #ddd #ddd;} 382 | .tabs-left .nav-tabs>li,.tabs-right .nav-tabs>li{float:none;} 383 | .tabs-left .nav-tabs>li>a,.tabs-right .nav-tabs>li>a{min-width:74px;margin-right:0;margin-bottom:3px;} 384 | .tabs-left .nav-tabs{float:left;margin-right:19px;border-right:1px solid #ddd;} 385 | .tabs-left .nav-tabs>li>a{margin-right:-1px;-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px;} 386 | .tabs-left .nav-tabs>li>a:hover{border-color:#eeeeee #dddddd #eeeeee #eeeeee;} 387 | .tabs-left .nav-tabs .active>a,.tabs-left .nav-tabs .active>a:hover{border-color:#ddd transparent #ddd #ddd;*border-right-color:#ffffff;} 388 | .tabs-right .nav-tabs{float:right;margin-left:19px;border-left:1px solid #ddd;} 389 | .tabs-right .nav-tabs>li>a{margin-left:-1px;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0;} 390 | .tabs-right .nav-tabs>li>a:hover{border-color:#eeeeee #eeeeee #eeeeee #dddddd;} 391 | .tabs-right .nav-tabs .active>a,.tabs-right .nav-tabs .active>a:hover{border-color:#ddd #ddd #ddd transparent;*border-left-color:#ffffff;} 392 | .navbar{*position:relative;*z-index:2;overflow:visible;margin-bottom:18px;} 393 | .navbar-inner{padding-left:20px;padding-right:20px;background-color:#2c2c2c;background-image:-moz-linear-gradient(top, #333333, #222222);background-image:-ms-linear-gradient(top, #333333, #222222);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#333333), to(#222222));background-image:-webkit-linear-gradient(top, #333333, #222222);background-image:-o-linear-gradient(top, #333333, #222222);background-image:linear-gradient(top, #333333, #222222);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 1px 3px rgba(0, 0, 0, 0.25),inset 0 -1px 0 rgba(0, 0, 0, 0.1);-moz-box-shadow:0 1px 3px rgba(0, 0, 0, 0.25),inset 0 -1px 0 rgba(0, 0, 0, 0.1);box-shadow:0 1px 3px rgba(0, 0, 0, 0.25),inset 0 -1px 0 rgba(0, 0, 0, 0.1);} 394 | .navbar .container{width:auto;} 395 | .btn-navbar{display:none;float:right;padding:7px 10px;margin-left:5px;margin-right:5px;background-color:#2c2c2c;background-image:-moz-linear-gradient(top, #333333, #222222);background-image:-ms-linear-gradient(top, #333333, #222222);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#333333), to(#222222));background-image:-webkit-linear-gradient(top, #333333, #222222);background-image:-o-linear-gradient(top, #333333, #222222);background-image:linear-gradient(top, #333333, #222222);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0);border-color:#222222 #222222 #000000;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:dximagetransform.microsoft.gradient(enabled=false);-webkit-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.075);-moz-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.075);box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.075);}.btn-navbar:hover,.btn-navbar:active,.btn-navbar.active,.btn-navbar.disabled,.btn-navbar[disabled]{background-color:#222222;} 396 | .btn-navbar:active,.btn-navbar.active{background-color:#080808 \9;} 397 | .btn-navbar .icon-bar{display:block;width:18px;height:2px;background-color:#f5f5f5;-webkit-border-radius:1px;-moz-border-radius:1px;border-radius:1px;-webkit-box-shadow:0 1px 0 rgba(0, 0, 0, 0.25);-moz-box-shadow:0 1px 0 rgba(0, 0, 0, 0.25);box-shadow:0 1px 0 rgba(0, 0, 0, 0.25);} 398 | .btn-navbar .icon-bar+.icon-bar{margin-top:3px;} 399 | .nav-collapse.collapse{height:auto;} 400 | .navbar{color:#999999;}.navbar .brand:hover{text-decoration:none;} 401 | .navbar .brand{float:left;display:block;padding:8px 20px 12px;margin-left:-20px;font-size:20px;font-weight:200;line-height:1;color:#ffffff;} 402 | .navbar .navbar-text{margin-bottom:0;line-height:40px;} 403 | .navbar .btn,.navbar .btn-group{margin-top:5px;} 404 | .navbar .btn-group .btn{margin-top:0;} 405 | .navbar-form{margin-bottom:0;*zoom:1;}.navbar-form:before,.navbar-form:after{display:table;content:"";} 406 | .navbar-form:after{clear:both;} 407 | .navbar-form input,.navbar-form select,.navbar-form .radio,.navbar-form .checkbox{margin-top:5px;} 408 | .navbar-form input,.navbar-form select{display:inline-block;margin-bottom:0;} 409 | .navbar-form input[type="image"],.navbar-form input[type="checkbox"],.navbar-form input[type="radio"]{margin-top:3px;} 410 | .navbar-form .input-append,.navbar-form .input-prepend{margin-top:6px;white-space:nowrap;}.navbar-form .input-append input,.navbar-form .input-prepend input{margin-top:0;} 411 | .navbar-search{position:relative;float:left;margin-top:6px;margin-bottom:0;}.navbar-search .search-query{padding:4px 9px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;font-weight:normal;line-height:1;color:#ffffff;background-color:#626262;border:1px solid #151515;-webkit-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1),0 1px 0px rgba(255, 255, 255, 0.15);-moz-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1),0 1px 0px rgba(255, 255, 255, 0.15);box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1),0 1px 0px rgba(255, 255, 255, 0.15);-webkit-transition:none;-moz-transition:none;-ms-transition:none;-o-transition:none;transition:none;}.navbar-search .search-query:-moz-placeholder{color:#cccccc;} 412 | .navbar-search .search-query::-webkit-input-placeholder{color:#cccccc;} 413 | .navbar-search .search-query:focus,.navbar-search .search-query.focused{padding:5px 10px;color:#333333;text-shadow:0 1px 0 #ffffff;background-color:#ffffff;border:0;-webkit-box-shadow:0 0 3px rgba(0, 0, 0, 0.15);-moz-box-shadow:0 0 3px rgba(0, 0, 0, 0.15);box-shadow:0 0 3px rgba(0, 0, 0, 0.15);outline:0;} 414 | .navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030;margin-bottom:0;} 415 | .navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding-left:0;padding-right:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;} 416 | .navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px;} 417 | .navbar-fixed-top{top:0;} 418 | .navbar-fixed-bottom{bottom:0;} 419 | .navbar .nav{position:relative;left:0;display:block;float:left;margin:0 10px 0 0;} 420 | .navbar .nav.pull-right{float:right;} 421 | .navbar .nav>li{display:block;float:left;} 422 | .navbar .nav>li>a{float:none;padding:10px 10px 11px;line-height:19px;color:#999999;text-decoration:none;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);} 423 | .navbar .nav>li>a:hover{background-color:transparent;color:#ffffff;text-decoration:none;} 424 | .navbar .nav .active>a,.navbar .nav .active>a:hover{color:#ffffff;text-decoration:none;background-color:#222222;} 425 | .navbar .divider-vertical{height:40px;width:1px;margin:0 9px;overflow:hidden;background-color:#222222;border-right:1px solid #333333;} 426 | .navbar .nav.pull-right{margin-left:10px;margin-right:0;} 427 | .navbar .dropdown-menu{margin-top:1px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}.navbar .dropdown-menu:before{content:'';display:inline-block;border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:7px solid #ccc;border-bottom-color:rgba(0, 0, 0, 0.2);position:absolute;top:-7px;left:9px;} 428 | .navbar .dropdown-menu:after{content:'';display:inline-block;border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #ffffff;position:absolute;top:-6px;left:10px;} 429 | .navbar-fixed-bottom .dropdown-menu:before{border-top:7px solid #ccc;border-top-color:rgba(0, 0, 0, 0.2);border-bottom:0;bottom:-7px;top:auto;} 430 | .navbar-fixed-bottom .dropdown-menu:after{border-top:6px solid #ffffff;border-bottom:0;bottom:-6px;top:auto;} 431 | .navbar .nav .dropdown-toggle .caret,.navbar .nav .open.dropdown .caret{border-top-color:#ffffff;border-bottom-color:#ffffff;} 432 | .navbar .nav .active .caret{opacity:1;filter:alpha(opacity=100);} 433 | .navbar .nav .open>.dropdown-toggle,.navbar .nav .active>.dropdown-toggle,.navbar .nav .open.active>.dropdown-toggle{background-color:transparent;} 434 | .navbar .nav .active>.dropdown-toggle:hover{color:#ffffff;} 435 | .navbar .nav.pull-right .dropdown-menu,.navbar .nav .dropdown-menu.pull-right{left:auto;right:0;}.navbar .nav.pull-right .dropdown-menu:before,.navbar .nav .dropdown-menu.pull-right:before{left:auto;right:12px;} 436 | .navbar .nav.pull-right .dropdown-menu:after,.navbar .nav .dropdown-menu.pull-right:after{left:auto;right:13px;} 437 | .alert{padding:8px 35px 8px 14px;margin-bottom:18px;text-shadow:0 1px 0 rgba(255, 255, 255, 0.5);background-color:#fcf8e3;border:1px solid #fbeed5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;color:#c09853;} 438 | .alert-heading{color:inherit;} 439 | .alert .close{position:relative;top:-2px;right:-21px;line-height:18px;} 440 | .alert-success{background-color:#dff0d8;border-color:#d6e9c6;color:#468847;} 441 | .alert-danger,.alert-error{background-color:#f2dede;border-color:#eed3d7;color:#b94a48;} 442 | .alert-info{background-color:#d9edf7;border-color:#bce8f1;color:#3a87ad;} 443 | .alert-block{padding-top:14px;padding-bottom:14px;} 444 | .alert-block>p,.alert-block>ul{margin-bottom:0;} 445 | .alert-block p+p{margin-top:5px;} 446 | .hero-unit{padding:60px;margin-bottom:30px;background-color:#eeeeee;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;}.hero-unit h1{margin-bottom:0;font-size:60px;line-height:1;color:inherit;letter-spacing:-1px;} 447 | .hero-unit p{font-size:18px;font-weight:200;line-height:27px;color:inherit;} 448 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibigbug/pypi-mirrors/0c7f4414b0bc4291b36f40c790241281d164053a/static/favicon.ico -------------------------------------------------------------------------------- /static/img/forkme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibigbug/pypi-mirrors/0c7f4414b0bc4291b36f40c790241281d164053a/static/img/forkme.png -------------------------------------------------------------------------------- /static/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibigbug/pypi-mirrors/0c7f4414b0bc4291b36f40c790241281d164053a/static/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /static/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibigbug/pypi-mirrors/0c7f4414b0bc4291b36f40c790241281d164053a/static/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /static/img/mailgun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibigbug/pypi-mirrors/0c7f4414b0bc4291b36f40c790241281d164053a/static/img/mailgun.png -------------------------------------------------------------------------------- /static/img/mailgun_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibigbug/pypi-mirrors/0c7f4414b0bc4291b36f40c790241281d164053a/static/img/mailgun_small.png -------------------------------------------------------------------------------- /static/img/powered-by-gce.svg: -------------------------------------------------------------------------------- 1 | 2 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | -------------------------------------------------------------------------------- /static/js/jquery.sparkline.min.js: -------------------------------------------------------------------------------- 1 | /* jquery.sparkline 1.6 - http://omnipotent.net/jquery.sparkline/ 2 | ** Licensed under the New BSD License - see above site for details */ 3 | 4 | (function($){var defaults={common:{type:'line',lineColor:'#00f',fillColor:'#cdf',defaultPixelsPerValue:3,width:'auto',height:'auto',composite:false,tagValuesAttribute:'values',tagOptionsPrefix:'spark',enableTagOptions:false},line:{spotColor:'#f80',spotRadius:1.5,minSpotColor:'#f80',maxSpotColor:'#f80',lineWidth:1,normalRangeMin:undefined,normalRangeMax:undefined,normalRangeColor:'#ccc',drawNormalOnTop:false,chartRangeMin:undefined,chartRangeMax:undefined,chartRangeMinX:undefined,chartRangeMaxX:undefined},bar:{barColor:'#00f',negBarColor:'#f44',zeroColor:undefined,nullColor:undefined,zeroAxis:undefined,barWidth:4,barSpacing:1,chartRangeMax:undefined,chartRangeMin:undefined,chartRangeClip:false,colorMap:undefined},tristate:{barWidth:4,barSpacing:1,posBarColor:'#6f6',negBarColor:'#f44',zeroBarColor:'#999',colorMap:{}},discrete:{lineHeight:'auto',thresholdColor:undefined,thresholdValue:0,chartRangeMax:undefined,chartRangeMin:undefined,chartRangeClip:false},bullet:{targetColor:'red',targetWidth:3,performanceColor:'blue',rangeColors:['#D3DAFE','#A8B6FF','#7F94FF'],base:undefined},pie:{sliceColors:['#f00','#0f0','#00f']},box:{raw:false,boxLineColor:'black',boxFillColor:'#cdf',whiskerColor:'black',outlierLineColor:'#333',outlierFillColor:'white',medianColor:'red',showOutliers:true,outlierIQR:1.5,spotRadius:1.5,target:undefined,targetColor:'#4a2',chartRangeMax:undefined,chartRangeMin:undefined}};var VCanvas_base,VCanvas_canvas,VCanvas_vml;$.fn.simpledraw=function(width,height,use_existing){if(use_existing&&this[0].VCanvas){return this[0].VCanvas;} 5 | if(width===undefined){width=$(this).innerWidth();} 6 | if(height===undefined){height=$(this).innerHeight();} 7 | if($.browser.hasCanvas){return new VCanvas_canvas(width,height,this);}else if($.browser.msie){return new VCanvas_vml(width,height,this);}else{return false;}};var pending=[];$.fn.sparkline=function(uservalues,userOptions){return this.each(function(){var options=new $.fn.sparkline.options(this,userOptions);var render=function(){var values,width,height;if(uservalues==='html'||uservalues===undefined){var vals=this.getAttribute(options.get('tagValuesAttribute'));if(vals===undefined||vals===null){vals=$(this).html();} 8 | values=vals.replace(/(^\s*\s*$)|\s+/g,'').split(',');}else{values=uservalues;} 9 | width=options.get('width')=='auto'?values.length*options.get('defaultPixelsPerValue'):options.get('width');if(options.get('height')=='auto'){if(!options.get('composite')||!this.VCanvas){var tmp=document.createElement('span');tmp.innerHTML='a';$(this).html(tmp);height=$(tmp).innerHeight();$(tmp).remove();}}else{height=options.get('height');} 10 | $.fn.sparkline[options.get('type')].call(this,values,options,width,height);};if(($(this).html()&&$(this).is(':hidden'))||($.fn.jquery<"1.3.0"&&$(this).parents().is(':hidden'))||!$(this).parents('body').length){pending.push([this,render]);}else{render.call(this);}});};$.fn.sparkline.defaults=defaults;$.sparkline_display_visible=function(){for(var i=pending.length-1;i>=0;i--){var el=pending[i][0];if($(el).is(':visible')&&!$(el).parents().is(':hidden')){pending[i][1].call(el);pending.splice(i,1);}}};var UNSET_OPTION={};var normalizeValue=function(val){switch(val){case'undefined':val=undefined;break;case'null':val=null;break;case'true':val=true;break;case'false':val=false;break;default:var nf=parseFloat(val);if(val==nf){val=nf;}} 11 | return val;};$.fn.sparkline.options=function(tag,userOptions){var extendedOptions;this.userOptions=userOptions=userOptions||{};this.tag=tag;this.tagValCache={};var defaults=$.fn.sparkline.defaults;var base=defaults.common;this.tagOptionsPrefix=userOptions.enableTagOptions&&(userOptions.tagOptionsPrefix||base.tagOptionsPrefix);var tagOptionType=this.getTagSetting('type');if(tagOptionType===UNSET_OPTION){extendedOptions=defaults[userOptions.type||base.type];}else{extendedOptions=defaults[tagOptionType];} 12 | this.mergedOptions=$.extend({},base,extendedOptions,userOptions);};$.fn.sparkline.options.prototype.getTagSetting=function(key){var val,i,prefix=this.tagOptionsPrefix;if(prefix===false||prefix===undefined){return UNSET_OPTION;} 13 | if(this.tagValCache.hasOwnProperty(key)){val=this.tagValCache.key;}else{val=this.tag.getAttribute(prefix+key);if(val===undefined||val===null){val=UNSET_OPTION;}else if(val.substr(0,1)=='['){val=val.substr(1,val.length-2).split(',');for(i=val.length;i--;){val[i]=normalizeValue(val[i].replace(/(^\s*)|(\s*$)/g,''));}}else if(val.substr(0,1)=='{'){var pairs=val.substr(1,val.length-2).split(',');val={};for(i=pairs.length;i--;){var keyval=pairs[i].split(':',2);val[keyval[0].replace(/(^\s*)|(\s*$)/g,'')]=normalizeValue(keyval[1].replace(/(^\s*)|(\s*$)/g,''));}}else{val=normalizeValue(val);} 14 | this.tagValCache.key=val;} 15 | return val;};$.fn.sparkline.options.prototype.get=function(key){var tagOption=this.getTagSetting(key);if(tagOption!==UNSET_OPTION){return tagOption;} 16 | return this.mergedOptions[key];};$.fn.sparkline.line=function(values,options,width,height){var xvalues=[],yvalues=[],yminmax=[];for(var i=0;imaxy){maxy=normalRangeMax;}} 20 | if(options.get('chartRangeMin')!==undefined&&(options.get('chartRangeClip')||options.get('chartRangeMin')maxy)){maxy=options.get('chartRangeMax');} 22 | if(options.get('chartRangeMinX')!==undefined&&(options.get('chartRangeClipX')||options.get('chartRangeMinX')maxx)){maxx=options.get('chartRangeMaxX');} 24 | var rangex=maxx-minx===0?1:maxx-minx;var rangey=maxy-miny===0?1:maxy-miny;var vl=yvalues.length-1;if(vl<1){this.innerHTML='';return;} 25 | var target=$(this).simpledraw(width,height,options.get('composite'));if(target){var canvas_width=target.pixel_width;var canvas_height=target.pixel_height;var canvas_top=0;var canvas_left=0;var spotRadius=options.get('spotRadius');if(spotRadius&&(canvas_width<(spotRadius*4)||canvas_height<(spotRadius*4))){spotRadius=0;} 26 | if(spotRadius){if(options.get('minSpotColor')||(options.get('spotColor')&&yvalues[vl]==miny)){canvas_height-=Math.ceil(spotRadius);} 27 | if(options.get('maxSpotColor')||(options.get('spotColor')&&yvalues[vl]==maxy)){canvas_height-=Math.ceil(spotRadius);canvas_top+=Math.ceil(spotRadius);} 28 | if(options.get('minSpotColor')||options.get('maxSpotColor')&&(yvalues[0]==miny||yvalues[0]==maxy)){canvas_left+=Math.ceil(spotRadius);canvas_width-=Math.ceil(spotRadius);} 29 | if(options.get('spotColor')||(options.get('minSpotColor')||options.get('maxSpotColor')&&(yvalues[vl]==miny||yvalues[vl]==maxy))){canvas_width-=Math.ceil(spotRadius);}} 30 | canvas_height--;var drawNormalRange=function(){if(normalRangeMin!==undefined){var ytop=canvas_top+Math.round(canvas_height-(canvas_height*((normalRangeMax-miny)/rangey)));var height=Math.round((canvas_height*(normalRangeMax-normalRangeMin))/rangey);target.drawRect(canvas_left,ytop,canvas_width,height,undefined,options.get('normalRangeColor'));}};if(!options.get('drawNormalOnTop')){drawNormalRange();} 31 | var path=[];var paths=[path];var x,y,vlen=yvalues.length;for(i=0;imaxy){y=maxy;} 33 | if(!path.length){path.push([canvas_left+Math.round((x-minx)*(canvas_width/rangex)),canvas_top+canvas_height]);} 34 | path.push([canvas_left+Math.round((x-minx)*(canvas_width/rangex)),canvas_top+Math.round(canvas_height-(canvas_height*((y-miny)/rangey)))]);}} 35 | var lineshapes=[];var fillshapes=[];var plen=paths.length;for(i=0;i2){path[0]=[path[0][0],path[1][1]];} 38 | lineshapes.push(path);} 39 | plen=fillshapes.length;for(i=0;imax)){max=options.get('chartRangeMax');} 47 | var zeroAxis=options.get('zeroAxis');if(zeroAxis===undefined){zeroAxis=min<0;} 48 | var range=max-min===0?1:max-min;var colorMapByIndex,colorMapByValue;if($.isArray(options.get('colorMap'))){colorMapByIndex=options.get('colorMap');colorMapByValue=null;}else{colorMapByIndex=null;colorMapByValue=options.get('colorMap');} 49 | var target=$(this).simpledraw(width,height,options.get('composite'));if(target){var color,canvas_height=target.pixel_height,yzero=min<0&&zeroAxis?canvas_height-Math.round(canvas_height*(Math.abs(min)/range))-1:canvas_height-1;for(i=values.length;i--;){var x=i*(options.get('barWidth')+options.get('barSpacing')),y,val=values[i];if(val===null){if(options.get('nullColor')){color=options.get('nullColor');val=(zeroAxis&&min<0)?0:min;height=1;y=(zeroAxis&&min<0)?yzero:canvas_height-height;}else{continue;}}else{if(valmax){val=max;} 51 | color=(val<0)?options.get('negBarColor'):options.get('barColor');if(zeroAxis&&min<0){height=Math.round(canvas_height*((Math.abs(val)/range)))+1;y=(val<0)?yzero:yzero-height;}else{height=Math.round(canvas_height*((val-min)/range))+1;y=canvas_height-height;} 52 | if(val===0&&options.get('zeroColor')!==undefined){color=options.get('zeroColor');} 53 | if(colorMapByValue&&colorMapByValue[val]){color=colorMapByValue[val];}else if(colorMapByIndex&&colorMapByIndex.length>i){color=colorMapByIndex[i];} 54 | if(color===null){continue;}} 55 | target.drawRect(x,y,options.get('barWidth')-1,height-1,color,color);}}else{this.innerHTML='';}};$.fn.sparkline.tristate=function(values,options,width,height){values=$.map(values,Number);width=(values.length*options.get('barWidth'))+((values.length-1)*options.get('barSpacing'));var colorMapByIndex,colorMapByValue;if($.isArray(options.get('colorMap'))){colorMapByIndex=options.get('colorMap');colorMapByValue=null;}else{colorMapByIndex=null;colorMapByValue=options.get('colorMap');} 56 | var target=$(this).simpledraw(width,height,options.get('composite'));if(target){var canvas_height=target.pixel_height,half_height=Math.round(canvas_height/2);for(var i=values.length;i--;){var x=i*(options.get('barWidth')+options.get('barSpacing')),y,color;if(values[i]<0){y=half_height;height=half_height-1;color=options.get('negBarColor');}else if(values[i]>0){y=0;height=half_height-1;color=options.get('posBarColor');}else{y=half_height-1;height=2;color=options.get('zeroBarColor');} 57 | if(colorMapByValue&&colorMapByValue[values[i]]){color=colorMapByValue[values[i]];}else if(colorMapByIndex&&colorMapByIndex.length>i){color=colorMapByIndex[i];} 58 | if(color===null){continue;} 59 | target.drawRect(x,y,options.get('barWidth')-1,height-1,color,color);}}else{this.innerHTML='';}};$.fn.sparkline.discrete=function(values,options,width,height){values=$.map(values,Number);width=options.get('width')=='auto'?values.length*2:width;var interval=Math.floor(width/values.length);var target=$(this).simpledraw(width,height,options.get('composite'));if(target){var canvas_height=target.pixel_height,line_height=options.get('lineHeight')=='auto'?Math.round(canvas_height*0.3):options.get('lineHeight'),pheight=canvas_height-line_height,min=Math.min.apply(Math,values),max=Math.max.apply(Math,values);if(options.get('chartRangeMin')!==undefined&&(options.get('chartRangeClip')||options.get('chartRangeMin')max)){max=options.get('chartRangeMax');} 61 | var range=max-min;for(var i=values.length;i--;){var val=values[i];if(valmax){val=max;} 63 | var x=(i*interval),ytop=Math.round(pheight-pheight*((val-min)/range));target.drawLine(x,ytop,x,ytop+line_height,(options.get('thresholdColor')&&val1){var canvas_width=target.pixel_width-Math.ceil(options.get('targetWidth')/2),canvas_height=target.pixel_height,min=Math.min.apply(Math,values),max=Math.max.apply(Math,values);if(options.get('base')===undefined){min=min<0?min:0;}else{min=options.get('base');} 64 | var range=max-min;for(var i=2,vlen=values.length;i1){var canvas_width=target.pixel_width,canvas_height=target.pixel_height,radius=Math.floor(Math.min(canvas_width,canvas_height)/2),total=0,next=0,circle=2*Math.PI;for(var i=values.length;i--;){total+=values[i];} 66 | if(options.get('offset')){next+=(2*Math.PI)*(options.get('offset')/360);} 67 | var vlen=values.length;for(i=0;i0){end=next+(circle*(values[i]/total));} 68 | target.drawPieSlice(radius,radius,radius,start,end,undefined,options.get('sliceColors')[i%options.get('sliceColors').length]);next=end;}}};var quartile=function(values,q){if(q==2){var vl2=Math.floor(values.length/2);return values.length%2?values[vl2]:(values[vl2]+values[vl2+1])/2;}else{var vl4=Math.floor(values.length/4);return values.length%2?(values[vl4*q]+values[vl4*q+1])/2:values[vl4*q];}};$.fn.sparkline.box=function(values,options,width,height){values=$.map(values,Number);width=options.get('width')=='auto'?'4.0em':width;var minvalue=options.get('chartRangeMin')===undefined?Math.min.apply(Math,values):options.get('chartRangeMin'),maxvalue=options.get('chartRangeMax')===undefined?Math.max.apply(Math,values):options.get('chartRangeMax'),target=$(this).simpledraw(width,height,options.get('composite')),vlen=values.length,lwhisker,loutlier,q1,q2,q3,rwhisker,routlier;if(target&&values.length>1){var canvas_width=target.pixel_width,canvas_height=target.pixel_height;if(options.get('raw')){if(options.get('showOutliers')&&values.length>5){loutlier=values[0];lwhisker=values[1];q1=values[2];q2=values[3];q3=values[4];rwhisker=values[5];routlier=values[6];}else{lwhisker=values[0];q1=values[1];q2=values[2];q3=values[3];rwhisker=values[4];}}else{values.sort(function(a,b){return a-b;});q1=quartile(values,1);q2=quartile(values,2);q3=quartile(values,3);var iqr=q3-q1;if(options.get('showOutliers')){lwhisker=undefined;rwhisker=undefined;for(var i=0;iq1-(iqr*options.get('outlierIQR'))){lwhisker=values[i];} 69 | if(values[i]rwhisker){target.drawCircle((routlier-minvalue)*unitsize+canvas_left,canvas_height/2,options.get('spotRadius'),options.get('outlierLineColor'),options.get('outlierFillColor'));}} 73 | target.drawRect(Math.round((q1-minvalue)*unitsize+canvas_left),Math.round(canvas_height*0.1),Math.round((q3-q1)*unitsize),Math.round(canvas_height*0.8),options.get('boxLineColor'),options.get('boxFillColor'));target.drawLine(Math.round((lwhisker-minvalue)*unitsize+canvas_left),Math.round(canvas_height/2),Math.round((q1-minvalue)*unitsize+canvas_left),Math.round(canvas_height/2),options.get('lineColor'));target.drawLine(Math.round((lwhisker-minvalue)*unitsize+canvas_left),Math.round(canvas_height/4),Math.round((lwhisker-minvalue)*unitsize+canvas_left),Math.round(canvas_height-canvas_height/4),options.get('whiskerColor'));target.drawLine(Math.round((rwhisker-minvalue)*unitsize+canvas_left),Math.round(canvas_height/2),Math.round((q3-minvalue)*unitsize+canvas_left),Math.round(canvas_height/2),options.get('lineColor'));target.drawLine(Math.round((rwhisker-minvalue)*unitsize+canvas_left),Math.round(canvas_height/4),Math.round((rwhisker-minvalue)*unitsize+canvas_left),Math.round(canvas_height-canvas_height/4),options.get('whiskerColor'));target.drawLine(Math.round((q2-minvalue)*unitsize+canvas_left),Math.round(canvas_height*0.1),Math.round((q2-minvalue)*unitsize+canvas_left),Math.round(canvas_height*0.9),options.get('medianColor'));if(options.get('target')){var size=Math.ceil(options.get('spotRadius'));target.drawLine(Math.round((options.get('target')-minvalue)*unitsize+canvas_left),Math.round((canvas_height/2)-size),Math.round((options.get('target')-minvalue)*unitsize+canvas_left),Math.round((canvas_height/2)+size),options.get('targetColor'));target.drawLine(Math.round((options.get('target')-minvalue)*unitsize+canvas_left-size),Math.round(canvas_height/2),Math.round((options.get('target')-minvalue)*unitsize+canvas_left+size),Math.round(canvas_height/2),options.get('targetColor'));}}else{this.innerHTML='';}};if($.browser.msie&&!document.namespaces.v){document.namespaces.add('v','urn:schemas-microsoft-com:vml','#default#VML');} 74 | if($.browser.hasCanvas===undefined){var t=document.createElement('canvas');$.browser.hasCanvas=t.getContext!==undefined;} 75 | VCanvas_base=function(width,height,target){};VCanvas_base.prototype={init:function(width,height,target){this.width=width;this.height=height;this.target=target;if(target[0]){target=target[0];} 76 | target.VCanvas=this;},drawShape:function(path,lineColor,fillColor,lineWidth){alert('drawShape not implemented');},drawLine:function(x1,y1,x2,y2,lineColor,lineWidth){return this.drawShape([[x1,y1],[x2,y2]],lineColor,lineWidth);},drawCircle:function(x,y,radius,lineColor,fillColor){alert('drawCircle not implemented');},drawPieSlice:function(x,y,radius,startAngle,endAngle,lineColor,fillColor){alert('drawPieSlice not implemented');},drawRect:function(x,y,width,height,lineColor,fillColor){alert('drawRect not implemented');},getElement:function(){return this.canvas;},_insert:function(el,target){$(target).html(el);}};VCanvas_canvas=function(width,height,target){return this.init(width,height,target);};VCanvas_canvas.prototype=$.extend(new VCanvas_base(),{_super:VCanvas_base.prototype,init:function(width,height,target){this._super.init(width,height,target);this.canvas=document.createElement('canvas');if(target[0]){target=target[0];} 77 | target.VCanvas=this;$(this.canvas).css({display:'inline-block',width:width,height:height,verticalAlign:'top'});this._insert(this.canvas,target);this.pixel_height=$(this.canvas).height();this.pixel_width=$(this.canvas).width();this.canvas.width=this.pixel_width;this.canvas.height=this.pixel_height;$(this.canvas).css({width:this.pixel_width,height:this.pixel_height});},_getContext:function(lineColor,fillColor,lineWidth){var context=this.canvas.getContext('2d');if(lineColor!==undefined){context.strokeStyle=lineColor;} 78 | context.lineWidth=lineWidth===undefined?1:lineWidth;if(fillColor!==undefined){context.fillStyle=fillColor;} 79 | return context;},drawShape:function(path,lineColor,fillColor,lineWidth){var context=this._getContext(lineColor,fillColor,lineWidth);context.beginPath();context.moveTo(path[0][0]+0.5,path[0][1]+0.5);for(var i=1,plen=path.length;i';this.canvas.insertAdjacentHTML('beforeEnd',groupel);this.group=$(this.canvas).children()[0];},drawShape:function(path,lineColor,fillColor,lineWidth){var vpath=[];for(var i=0,plen=path.length;i'+' ';this.group.insertAdjacentHTML('beforeEnd',vel);},drawCircle:function(x,y,radius,lineColor,fillColor){x-=radius+1;y-=radius+1;var stroke=lineColor===undefined?' stroked="false" ':' strokeWeight="1" strokeColor="'+lineColor+'" ';var fill=fillColor===undefined?' filled="false"':' fillColor="'+fillColor+'" filled="true" ';var vel='';this.group.insertAdjacentHTML('beforeEnd',vel);},drawPieSlice:function(x,y,radius,startAngle,endAngle,lineColor,fillColor){if(startAngle==endAngle){return;} 90 | if((endAngle-startAngle)==(2*Math.PI)){startAngle=0.0;endAngle=(2*Math.PI);} 91 | var startx=x+Math.round(Math.cos(startAngle)*radius);var starty=y+Math.round(Math.sin(startAngle)*radius);var endx=x+Math.round(Math.cos(endAngle)*radius);var endy=y+Math.round(Math.sin(endAngle)*radius);if(startx==endx&&starty==endy&&(endAngle-startAngle)'+' ';this.group.insertAdjacentHTML('beforeEnd',vel);},drawRect:function(x,y,width,height,lineColor,fillColor){return this.drawShape([[x,y],[x,y+height],[x+width,y+height],[x+width,y],[x,y]],lineColor,fillColor);}});})(jQuery); -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | PyPI Mirror Status 7 | 8 | 9 | 10 | 11 | 12 | 13 | 21 | 22 | 23 | 24 | 25 | 26 | 37 | 38 | 39 | Fork me on GitHub 40 | 41 |
42 |
43 |
44 | 48 |
49 | 50 | 53 |
54 | 55 |
56 |
57 | 58 | 59 | 60 | 61 | {% for item in data %} 62 | 63 | 64 | 65 | 66 | 69 | 72 | 82 | {% endfor %} 83 | 84 | 85 | 86 |
MirrorLocation# of PackagesLast updateAgeResponse Time (ms)*Status
{{item.mirror}}{{item.location}}{{item.num_packages}}{{item.last_update}}{{item.time_diff_human}} 67 | {% if item.age_list %}{% endif %} 68 | {{item.response_time}} 70 | {% if item.resp_list %} {% endif %} 71 | {% if item.status == 'Green' %} 73 | Fresh 74 | {% elif item.status == "Yellow" %} 75 | Aging 76 | {% elif item.status == "Red" %} 77 | Old 78 | {% else %} 79 | N/A 80 | {% endif %} 81 |
* Response time from Virginia, US
87 |
88 |
89 |
90 |
91 |

Mirror Statuses

92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 |
AgeStatus
age < 1 hourFresh
1 hour < age < 1 dayAging
age > 1 day Old
100 |
101 | 102 |
103 |

Using a mirror

104 |

Single Usage:
105 |

pip install -i https://<mirror>/simple <package>
106 |

Global settings:
107 | Add ~/.pip/pip.conf that includes: 108 |

109 | [global]
110 | index-url = https://<mirror>/simple
111 |

112 |

Use pmm:
113 |

114 | pip install pmm
115 | pmm
116 |

117 |
118 |
119 |
120 |
121 |
122 | Data also available in JSON format
123 | Page last updated at {{date_now}}
124 | Originally built by: @KenCochrane
125 | Maintained by: @ibigbug
126 | Built with: 127 | pypi-mirrors, Bootstrap, jQuery Sparklines, Redis and Flask 128 |
129 |
130 |
131 |
132 | 133 |
134 | 135 | 136 | 141 | 142 | 143 | 144 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import redis 2 | import socket 3 | import requests 4 | import lxml.html 5 | 6 | from config import PY2 7 | from config import load_config 8 | from services.iploc import get_city 9 | 10 | from services.errors import sentry 11 | 12 | 13 | if PY2: 14 | from urlparse import urlparse 15 | else: 16 | from urllib.parse import urlparse 17 | 18 | try: 19 | import cPickle as pickle 20 | except ImportError: 21 | import pickle 22 | 23 | 24 | CONFIG = load_config() 25 | 26 | 27 | def get_connection(): 28 | """ Get the connection to Redis""" 29 | return redis.StrictRedis(host=CONFIG.get('host'), 30 | port=int(CONFIG.get('port')), 31 | db=CONFIG.get('db'), 32 | password=CONFIG.get('password')) 33 | 34 | 35 | def find_number_of_packages(mirror, scheme='http'): 36 | """ Find the number of packages in a mirror """ 37 | html = lxml.html.fromstring(requests.get( 38 | "{0}://{1}/simple/".format(scheme, mirror)).content) 39 | return len(html.xpath("//a")) 40 | 41 | 42 | def ping_ip2loc(ip): 43 | """ get the location info for the ip 44 | you need to register for an API key here. http://ipinfodb.com/register.php 45 | 46 | and set it as an envirornment variable called 47 | PYPI_MIRRORS_API_KEY 48 | 49 | """ 50 | api_key = CONFIG.get('ip_api_key') 51 | if not api_key: 52 | return None 53 | return get_city(api_key, ip) 54 | 55 | 56 | def get_location_for_mirror(mirror): 57 | """ get the location for the mirror """ 58 | conn = get_connection() 59 | loc_key = cache_key('IPLOC', mirror) 60 | value = conn.get(loc_key) 61 | if value: 62 | return pickle.loads(value) 63 | 64 | # if we have a mirror name like mirror.domain.suffix/blah it won't work 65 | try: 66 | hostname = urlparse("http://{0}".format(mirror)).netloc 67 | except Exception as exc: 68 | # if error, just default to mirror that works most of the time 69 | print("Error getting location for {0} \n {1}".format(mirror, exc)) 70 | sentry.captureException() 71 | hostname = mirror 72 | 73 | ip = socket.gethostbyname(hostname) 74 | location = ping_ip2loc(ip) 75 | if location: 76 | conn.setex(loc_key, 86400, pickle.dumps(location)) # 1 day cache 77 | return location 78 | # if we get here, no good, return None 79 | return None 80 | 81 | 82 | def store_page_data(data, time_now): 83 | """ Store the data in the cache for later use.""" 84 | conn = get_connection() 85 | context = {'data': data, 'date_now': time_now} 86 | conn.set('PAGE_DATA', pickle.dumps(context)) 87 | 88 | 89 | def get_page_data(): 90 | """ Get the page data from the cache """ 91 | conn = get_connection() 92 | data = conn.get('PAGE_DATA') 93 | if data: 94 | return pickle.loads(data) 95 | return {} 96 | 97 | 98 | def store_json_data(data): 99 | """ Store the data in the cache for later use.""" 100 | conn = get_connection() 101 | conn.set('JSON_DATA', data) 102 | 103 | 104 | def get_json_data(): 105 | """ Get the json data from the cache """ 106 | conn = get_connection() 107 | data = conn.get('JSON_DATA') 108 | if not data: 109 | return {} 110 | return data 111 | 112 | 113 | def get_total_seconds(delta): 114 | """ need this since timedelta.total_seconds() 115 | isn't available in python 2.6.x""" 116 | if delta: 117 | return delta.seconds + (delta.days * 24 * 3600) 118 | return 0 119 | 120 | 121 | def cache_key(token, value): 122 | """ build a cache key """ 123 | return "{0}_{1}".format(token, value) 124 | 125 | 126 | def location_name(location): 127 | """ build out the location name given the location data """ 128 | if not location: 129 | return "N/A" 130 | city = location.get('cityName', None) 131 | region = location.get('regionName', None) 132 | country = location.get('countryName', None) 133 | country_code = location.get('countryCode', None) 134 | 135 | # clear out the -'s 136 | if city and city == '-': 137 | city = None 138 | if region and region == '-': 139 | region = None 140 | 141 | # If we have everything return everything but only use country_code 142 | if city and region and country_code: 143 | return "{0}, {1} {2}".format(city, region, country_code) 144 | 145 | # if we just have country, then only return country 146 | if not city and not region and country: 147 | return country 148 | 149 | # whatever else we have build it out by dynamically 150 | name = "" 151 | if city: 152 | name += city 153 | if city and region: 154 | name += ", " 155 | if region: 156 | name += region + " " 157 | if country: 158 | name += country 159 | return name 160 | 161 | 162 | def cmp_to_key(mycmp): 163 | 'Convert a cmp= function into a key= function' 164 | class K: 165 | def __init__(self, obj, *args): 166 | self.obj = obj 167 | 168 | def __lt__(self, other): 169 | return mycmp(self.obj, other.obj) < 0 170 | 171 | def __gt__(self, other): 172 | return mycmp(self.obj, other.obj) > 0 173 | 174 | def __eq__(self, other): 175 | return mycmp(self.obj, other.obj) == 0 176 | 177 | def __le__(self, other): 178 | return mycmp(self.obj, other.obj) <= 0 179 | 180 | def __ge__(self, other): 181 | return mycmp(self.obj, other.obj) >= 0 182 | 183 | def __ne__(self, other): 184 | return mycmp(self.obj, other.obj) != 0 185 | return K 186 | -------------------------------------------------------------------------------- /wsgi.py: -------------------------------------------------------------------------------- 1 | from app import app 2 | 3 | application = app 4 | --------------------------------------------------------------------------------