setup views, templates, plots

This commit is contained in:
crflynn
2018-04-05 15:57:08 -04:00
parent 741e385e1e
commit 03995ebf5a
20 changed files with 881 additions and 32 deletions

View File

@@ -15,6 +15,7 @@ github-flask = "*"
flask-sqlalchemy = "*"
flask-migrate = "*"
flask-login = "*"
flask-wtf = "*"
[dev-packages]

21
Pipfile.lock generated
View File

@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "36b3e674443d732f498a3aad93d143a2036297b5a24fbb282a7d48cd4dd83ad2"
"sha256": "f6427c3191e0004b972c855a3c74155aae118b1f0228e77b839ec4aa2c5a1141"
},
"host-environment-markers": {
"implementation_name": "cpython",
@@ -88,6 +88,13 @@
],
"version": "==2.3.2"
},
"flask-wtf": {
"hashes": [
"sha256:d9a9e366b32dcbb98ef17228e76be15702cd2600675668bca23f63a7947fd5ac",
"sha256:5d14d55cfd35f613d99ee7cba0fc3fbbe63ba02f544d349158c14ca15561cc36"
],
"version": "==0.14.2"
},
"github-flask": {
"hashes": [
"sha256:24600b720f698bac10667b76b136995ba7821d884e58b27e2a18ca0e4760c786"
@@ -110,10 +117,10 @@
},
"google-cloud-bigquery": {
"hashes": [
"sha256:dfb9b2819d5731a42e7e5e003938be7ceda66b40c8ffb67a44073d45aca94b7a",
"sha256:6374a68ef232ae93b6bc364e62c37c9e2bc1fffdd017ea10ffe6a65393f40acb"
"sha256:0681c20dbc663ba382397fd4fc45bd6dba92339408ff399365e47303753f3084",
"sha256:f1c274342a364904de0656eeee519ba6c2cf165204b824ccb39370b72f242894"
],
"version": "==0.31.0"
"version": "==0.32.0"
},
"google-cloud-core": {
"hashes": [
@@ -373,6 +380,12 @@
"sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c"
],
"version": "==0.14.1"
},
"wtforms": {
"hashes": [
"sha256:ffdf10bd1fa565b8233380cb77a304cd36fd55c73023e91d4b803c96bc11d46f"
],
"version": "==2.1"
}
},
"develop": {}

View File

@@ -43,7 +43,7 @@ class Model(CRUDMixin, db.Model):
class SurrogatePK(object):
"""A mixin that adds a surrogate integer 'primary key' column.
Adds a surrogate integer 'primary key' columnnamed ``id`` to any
Adds a surrogate integer 'primary key' column named ``id`` to any
declarative-mapped class.
"""

View File

@@ -55,6 +55,9 @@ class PythonMinorDownloadCount(Model):
)
RECENT_CATEGORIES = ["day", "week", "month"]
class RecentDownloadCount(Model):
"""Recent day/week/month download counts."""
@@ -66,7 +69,7 @@ class RecentDownloadCount(Model):
downloads = Column(db.BigInteger(), nullable=False)
def __repr__(self):
return "<RecentDownloadCount {}".format(
return "<RecentDownloadCount {}>".format(
f"{str(self.package)} - {str(self.category)}"
)

View File

@@ -0,0 +1,33 @@
{"data": [
{
"x": [
"2017-05-01",
"2017-05-02",
"2017-05-03"
],
"y": [
"345234",
"123123",
"456344"
],
"name": "Retention",
"type": "scatter",
"mode": "lines+markers",
"connectgaps": true,
"marker": {
"color": "rgba(61,133,198,1)",
"symbol": "circle",
"line": {
"color": "#444",
"width": 1
}
},
"line": {
"color": "rgba(61,133,198,1)",
"shape": "linear",
"smoothing": 1,
"width": 2
}
}
]
}

View File

@@ -0,0 +1,78 @@
{
"layout": {
"autosize": true,
"height": 400,
"margin": {
"r": 100,
"t": 40,
"autoexpand": true,
"b": 80,
"l": 100,
"pad": 0
},
"paper_bgcolor": "#fff",
"plot_bgcolor": "rgba(175, 175, 175, 0.2)",
"showlegend": true,
"legend": {
"orientation": "v",
"bgcolor": "#e7e7e7",
"xanchor": "left",
"yanchor": "middle",
"x": 0,
"y": 0.5
},
"title": "Downloads",
"yaxis": {
},
"xaxis": {
"tickformat": "%m-%d",
"dtick": 604800000,
"tick0": "2017-08-07",
"gridcolor": "#FFF",
"gridwidth": 2,
"anchor": "y",
"domain": [
0,
1
],
"title": "Date",
"titlefont": {
"family": "'Geneva', Verdana, Geneva, sans-serif",
"size": 16,
"color": "#7f7f7f"
},
"showline": true,
"linecolor": "rgba(148, 148, 148, 1)",
"linewidth": 2,
"tickangle": -45
},
"yaxis": {
"hoverformat": ",.0",
"tickformat": ",.0",
"gridcolor": "#FFF",
"gridwidth": 2,
"autotick": true,
"rangemode": "tozero",
"showline": true,
"title": "Downloads",
"ticksuffix": "",
"tickmode": "auto",
"linecolor": "rgba(148, 148, 148, 1)",
"linewidth": 2
}
},
"config": {
"displaylogo": false,
"modeBarButtonsToRemove": [
"toImage",
"sendDataToCloud",
"zoom2d",
"pan2d",
"select2d",
"lasso2d",
"zoomIn2d",
"zoomOut2d",
"toggleSpikelines"
]
}
}

View File

@@ -5,4 +5,5 @@ from pypistats.settings import ProdConfig
from pypistats.settings import TestConfig
# change this for migrations
app = create_app(DevConfig)

301
pypistats/static/style.css Normal file
View File

@@ -0,0 +1,301 @@
body {
background-color: #fff;
padding:0.4in;
font: 16px/1.5 "Noto Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
color:#727272;
font-weight:400;
overflow:auto;
}
h1, h2, h3, h4, h5, h6 {
color:#222;
margin:0 0 20px;
}
p, ul, ol, table, pre, dl {
margin:0 0 20px;
}
h1, h2, h3 {
line-height:1.1;
}
h1 {
font-size:28px;
}
h2 {
color:#393939;
}
h3, h4, h5, h6 {
color:#494949;
}
a {
color:#39c;
text-decoration:none;
}
a:hover {
color:#069;
}
a small {
font-size:11px;
color:#777;
margin-top:-0.3em;
display:block;
}
a:hover small {
color:#777;
}
.wrapper {
/*width:860px;*/
/* margin:0 auto; */
}
blockquote {
border-left:1px solid #e5e5e5;
margin:0;
padding:0 0 0 20px;
font-style:italic;
}
code, pre {
font-family:Monaco, Bitstream Vera Sans Mono, Lucida Console, Terminal, Consolas, Liberation Mono, DejaVu Sans Mono, Courier New, monospace;
color:#333;
font-size:12px;
}
pre {
padding:5px 5px;
background: #f8f8f8;
border-radius:5px;
border:1px solid #e5e5e5;
overflow-x: auto;
}
table {
/* width:100%; */
border-collapse:collapse;
}
th, td {
text-align:left;
padding:2px 10px;
border-bottom:1px solid #e5e5e5;
}
dt {
color:#444;
font-weight:700;
}
th {
color:#444;
}
img {
max-width:100%;
}
header {
width:200px;
float:left;
position:fixed;
-webkit-font-smoothing:subpixel-antialiased;
}
header ul {
list-style:none;
height:40px;
padding:0;
background: #f4f4f4;
border-radius:5px;
border:1px solid #e0e0e0;
width:270px;
}
header li {
width:89px;
float:left;
border-right:1px solid #e0e0e0;
height:40px;
}
header li:first-child a {
border-radius:5px 0 0 5px;
}
header li:last-child a {
border-radius:0 5px 5px 0;
}
header ul a {
line-height:1;
font-size:11px;
color:#999;
display:block;
text-align:center;
padding-top:6px;
height:34px;
}
header ul a:hover {
color:#999;
}
header ul a:active {
background-color:#f0f0f0;
}
strong {
color:#222;
font-weight:700;
}
header ul li + li + li {
border-right:none;
width:89px;
}
header ul a strong {
font-size:14px;
display:block;
color:#222;
}
section {
/*float:left;
width:100%;*/
padding-left: 225px;
padding-bottom:20px;
}
small {
font-size:11px;
}
hr {
border:0;
background:#e5e5e5;
height:1px;
margin:0 0 20px;
}
footer {
width:200px;
float:left;
position:fixed;
bottom:20px;
-webkit-font-smoothing:subpixel-antialiased;
}
.btn {
background: #f5c56c;
background-image: -webkit-linear-gradient(top, #f5c56c, #b8842b);
background-image: -moz-linear-gradient(top, #f5c56c, #b8842b);
background-image: -ms-linear-gradient(top, #f5c56c, #b8842b);
background-image: -o-linear-gradient(top, #f5c56c, #b8842b);
background-image: linear-gradient(to bottom, #f5c56c, #b8842b);
-webkit-border-radius: 0;
-moz-border-radius: 0;
border-radius: 0px;
text-shadow: 1px 1px 3px #666666;
font-family: Arial;
color: #ffffff;
font-size: 14px;
padding: 5px 10px 5px 10px;
text-decoration: none;
}
.btn:hover {
background: #e08c43;
background-image: -webkit-linear-gradient(top, #e08c43, #de9347);
background-image: -moz-linear-gradient(top, #e08c43, #de9347);
background-image: -ms-linear-gradient(top, #e08c43, #de9347);
background-image: -o-linear-gradient(top, #e08c43, #de9347);
background-image: linear-gradient(to bottom, #e08c43, #de9347);
text-decoration: none;
}
@media print, screen and (max-width: 960px) {
body {
padding:0.0in;
}
div.wrapper {
width:auto;
margin:0;
}
header, section, footer {
float:none;
position:static;
width:auto;
}
header {
padding-right:320px;
}
section {
border:1px solid #e5e5e5;
border-width:1px 0;
padding:20px 0;
margin:0 0 20px;
}
header a small {
display:inline;
}
header ul {
position:absolute;
right:50px;
top:52px;
}
}
@media print, screen and (max-width: 720px) {
body {
word-wrap:break-word;
}
header {
padding:0;
}
header ul, header p.view {
position:static;
}
pre, code {
word-wrap:normal;
}
}
@media print, screen and (max-width: 480px) {
body {
padding:0.0in;
}
header ul {
width:99%;
}
header li, header ul li + li + li {
width:33%;
}
}
@media print {
body {
font-size:12pt;
color:#444;
}
}

View File

@@ -247,9 +247,9 @@ def get_query(date):
SELECT
package,
'python_major' AS category_label,
SPLIT(python_version, '.')[
cast(SPLIT(python_version, '.')[
OFFSET
(0)] AS category,
(0)] as string) AS category,
COUNT(*) AS downloads
FROM
dls
@@ -262,11 +262,11 @@ def get_query(date):
SELECT
package,
'python_minor' AS category_label,
CONCAT(SPLIT(python_version, '.')[
cast(CONCAT(SPLIT(python_version, '.')[
OFFSET
(0)],'.',SPLIT(python_version, '.')[
OFFSET
(1)]) AS category,
(1)]) as string) AS category,
COUNT(*) AS downloads
FROM
dls

View File

@@ -0,0 +1,64 @@
{% extends "layout.html" %}
{% block title %}PyPI Stats{% endblock %}
{% block body %}
<h1>About PyPI Stats</h1>
<hr>
<h3>Goal</h3>
<p>PyPI Stats aims to provide aggregate download information on python packages available from the Python Package Index.</p>
<h3>Data</h3>
<p>Download stats are sourced from the Python Software Foundation's publicly available
<a href="https://bigquery.cloud.google.com/table/the-psf:pypi.downloads">download stats</a>
on Google BigQuery. All aggregate download stats ignore known PyPI mirrors (such as
<a href="{{url_for('general.package', package='bandersnatch')}}">bandersnatch</a>) unless noted otherwise.</p>
<p>PyPI Stats attempts to operate within the free tier in AWS. For this reason, aggregate data is only retained for 30 days.</p>
<h3>Tech</h3>
<p>PyPI Stats is a project developed using Python 3.6.
<ul>
<li>Framework:
<a href="{{url_for('general.package', package='flask')}}">Flask</a>
</li>
<li>
Host:
<a href="{{url_for('general.package', package='awscli')}}">AWS</a>
</li>
<li>
Deployment:
<a href="{{url_for('general.package', package='zappa')}}">Zappa</a>
</li>
<li>Authentication:
<a href="{{url_for('general.package', package='github-flask')}}">GitHub OAuth</a>
</li>
<li>
ORM:
<a href="{{url_for('general.package', package='sqlalchemy')}}">SQLAlchemy</a>
</li>
<li>
DBAPI:
<a href="{{url_for('general.package', package='psycopg2')}}">psycopg2</a>
</li>
<li>
RDBMS:
<a href="{{url_for('general.package', package='alembic')}}">alembic</a>
</li>
<li>
Templating:
<a href="{{url_for('general.package', package='jinja2')}}">jinja2</a>
</li>
<li>Charts:
<a href="{{url_for('general.package', package='plotly')}}">plotly.js</a>
</li>
<li>Data:
<a href="{{url_for('general.package', package='google-cloud-bigquery')}}">Google Cloud's BigQuery</a>
</li>
<li>
<a href="{{url_for('general.package', package='__all__')}}">And many more open source software packages</a>
</li>
</ul>
</p>
<h3>Who</h3>
<p>PyPI Stats was created by
<a href="https://flynn.gg">Christopher Flynn</a>.
</p>
{% endblock %}

View File

@@ -0,0 +1,16 @@
{% extends "layout.html" %}
{% block title %}PyPI Stats{% endblock %}
{% block body %}
<h1>Analytics for PyPI packages</h1>
<hr>
<p>Search for a python package on PyPI.</p>
<form method="POST" action="/">
{{ form.csrf_token }}
{{ form.name.label }}
{{ form.name(size=24) }}
<input type="submit" value="Search">
</form>
{% if search %}
{% include "results.html" %}
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,84 @@
<!doctype html>
<html>
<head>
<!-- <script type="text/javascript"> // var host = "example.com"; // if ((host == window.location.host) && (window.location.protocol != "https:")) // window.location.protocol = "https"; </script> -->
<link rel="stylesheet" href="/static/style.css">
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="chrome=1">
<title>
{% block title %}{% endblock %}
</title>
<meta name="viewport" content="width=device-width">
<!--[if lt IE 9]> <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script> <![endif]-->
<!-- ****** faviconit.com favicons ****** -->
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
<!-- <link rel="shortcut icon" href="/favicon.ico"> -->
<link rel="icon" sizes="16x16 32x32 64x64" href="{{ url_for('static', filename='favicon.ico') }}">
<link rel="icon" type="image/png" sizes="196x196" href="{{ url_for('static', filename='favicon-192.png') }}">
<link rel="icon" type="image/png" sizes="160x160" href="{{ url_for('static', filename='favicon-160.png') }}">
<link rel="icon" type="image/png" sizes="96x96" href="{{ url_for('static', filename='favicon-96.png') }}">
<link rel="icon" type="image/png" sizes="64x64" href="{{ url_for('static', filename='favicon-64.png') }}">
<link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', filename='favicon-32.png') }}">
<link rel="icon" type="image/png" sizes="16x16" href="{{ url_for('static', filename='favicon-16.png') }}">
<link rel="apple-touch-icon" href="{{ url_for('static', filename='favicon-57.png') }}">
<link rel="apple-touch-icon" sizes="114x114" href="{{ url_for('static', filename='favicon-114.png') }}">
<link rel="apple-touch-icon" sizes="72x72" href="{{ url_for('static', filename='favicon-72.png') }}">
<link rel="apple-touch-icon" sizes="144x144" href="{{ url_for('static', filename='favicon-144.png') }}">
<link rel="apple-touch-icon" sizes="60x60" href="{{ url_for('static', filename='favicon-60.png') }}">
<link rel="apple-touch-icon" sizes="120x120" href="{{ url_for('static', filename='favicon-120.png') }}">
<link rel="apple-touch-icon" sizes="76x76" href="{{ url_for('static', filename='favicon-76.png') }}">
<link rel="apple-touch-icon" sizes="152x152" href="{{ url_for('static', filename='favicon-152.png') }}">
<link rel="apple-touch-icon" sizes="180x180" href="{{ url_for('static', filename='favicon-180.png') }}">
<meta name="msapplication-TileColor" content="#FFFFFF">
<meta name="msapplication-TileImage" content="{{ url_for('static', filename='favicon-144.png') }}">
<meta name="msapplication-config" content="{{ url_for('static', filename='browserconfig.xml') }}">
<!-- ****** faviconit.com favicons ****** -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML" type="text/javascript"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/anchor-js/4.0.0/anchor.min.js" type="text/javascript"></script>
<!-- Place this tag in your head or just before your close body tag. -->
{% block plot %}{% endblock %}
{% block auth %}{% endblock %}
</head>
<body>
<div class="wrapper">
<header>
<h1>PyPI Stats</h1>
<p>
<a href="{{ url_for('general.index') }}">Home</a>
<br>
<br>
<a href="{{ url_for('general.package', package='__all__') }}">__all__</a>
<br>
<a href="{{ url_for('general.top') }}">Top</a>
<br>
<br>
<a href="{{ url_for('user.login')}}">Personal</a>
<br>
<a href="{{ url_for('user.logout') }}">Logout</a>
</p>
</header>
<section>
{% block body %}{% endblock %}
</section>
<footer>
<p>
<a href="{{ url_for('general.about') }}">About</a>
<br>
<small>PyPI Stats.<br></small>
</p>
</p>
</footer>
</div>
<script>
anchors.add();
</script>
</body>
</html>

View File

@@ -0,0 +1,67 @@
{% extends "layout.html" %}
{% block title %}PyPI Stats{% endblock %}
{% block plot%}
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
{% endblock %}
{% block body %}
<h1>Downloads for package:
{{package}}</h1>
<hr>
{% if package == "__all__" %}
<p>Download stats for __all__ indicate downloads across all packages on PyPI.</p>
<br>
{% else %}
<p>
<a href="{{ metadata['info']['package_url']}}">PyPI page</a>
<br>
<a href="{{ metadata['info']['home_page']}}">Home page</a>
<br>
Author:
{{metadata['info']['author']}}
<br>
License:
{{metadata['info']['license']}}
<br>
Summary:
{{metadata['info']['summary']}}
<br>
Latest version:
{{metadata['info']['version']}}
<br><br>
Downloads last day:
{{"{:,.0f}".format(recent['day'])}}
<br>
Downloads last week:
{{"{:,.0f}".format(recent['week'])}}
<br>
Downloads last month:
{{"{:,.0f}".format(recent['month'])}}
{% endif %}
<script>
(function () {
var WIDTH_IN_PERCENT_OF_PARENT = 100
// var HEIGHT = '300px'
var divelems = []
var data = {{ plots|tojson }}
for (plt in data) {
var gd3 = Plotly.d3.select('section').append('div').style({
width: WIDTH_IN_PERCENT_OF_PARENT + '%',
'margin-left': (100 - WIDTH_IN_PERCENT_OF_PARENT) / 2 + '%',
// height: HEIGHT,
})
var gd = gd3.node()
divelems.push(gd)
Plotly.newPlot(divelems[divelems.length - 1], data[plt].data, data[plt].layout, data[plt].config);
}
window.onresize = function () {
for (chart in divelems) {
Plotly.Plots.resize(divelems[chart]);
}
};
})();
</script>
{% endblock %}

View File

@@ -0,0 +1,12 @@
<br>
{% if packages %}
<ul>
{% for package in packages %}
<li>
<a href="{{ url_for('general.package', package=package) }}">{{ package }}</a>
</li>
{% endfor %}
</ul>
{% else %}
No results.
{% endif %}

View File

@@ -0,0 +1 @@
{% include "index.html" %}

View File

@@ -0,0 +1,37 @@
{% extends "layout.html" %}
{% block title %}PyPI Stats{% endblock %}
{% block body %}
<h1>Most downloaded PyPI packages</h1>
<hr>
<table>
<tr>
{% for best in top %}
<td>
Most downloaded past
<b>{{ best['category'].lower() }}</b>.
</td>
{% endfor %}
</tr>
<tr>
{% for best in top %}
<td>
<table>
{% for package in best['packages'] %}
<tr>
<td>
{{ loop.index }}
</td>
<td>
<a href="{{ url_for('general.package', package=package['package']) }}">{{ package['package'] }}</a>
</td>
<td>
{{ "{:,.0f}".format(package['downloads']) }}
</td>
</tr>
{% endfor %}
</table>
</td>
{% endfor %}
</tr>
</table>
{% endblock %}

View File

@@ -7,6 +7,7 @@ from flask import request
from pypistats.models.download import OverallDownloadCount
from pypistats.models.download import PythonMajorDownloadCount
from pypistats.models.download import PythonMinorDownloadCount
from pypistats.models.download import RECENT_CATEGORIES
from pypistats.models.download import RecentDownloadCount
from pypistats.models.download import SystemDownloadCount
@@ -19,17 +20,22 @@ def api_downloads_recent(package):
"""Get the recent downloads of a package."""
category = request.args.get('period')
if category is None:
downloads = RecentDownloadCount.query.filter_by(package=package).all()
elif category in ("day", "week", "month"):
downloads = RecentDownloadCount.query.filter_by(package=package, category=category).first()
downloads = RecentDownloadCount.query.\
filter_by(package=package).all()
elif category in RECENT_CATEGORIES:
downloads = RecentDownloadCount.query.\
filter_by(package=package, category=category).all()
else:
abort(404)
response = {"package": package, "type": "recent_downloads"}
if len(downloads) > 0:
response["data"] = {
r.category: r.downloads for r in downloads
}
if category is None:
response["data"] = {"last_" + rc: 0 for rc in RECENT_CATEGORIES}
else:
response["data"] = {"last_" + category: 0}
for r in downloads:
response["data"]["last_" + r.category] = r.downloads
else:
abort(404)
@@ -51,7 +57,8 @@ def api_downloads_overall(package):
else:
downloads = OverallDownloadCount.query.\
filter_by(package=package).\
order_by(OverallDownloadCount.category, OverallDownloadCount.date).all()
order_by(OverallDownloadCount.category,
OverallDownloadCount.date).all()
response = {"package": package, "type": "overall_downloads"}
if len(downloads) > 0:
@@ -69,19 +76,22 @@ def api_downloads_overall(package):
@blueprint.route("/<package>/python_major")
def api_downloads_python_major(package):
"""Get the python major download time series of a package."""
return generic_downloads(PythonMajorDownloadCount, package, "version", "python_major")
return generic_downloads(
PythonMajorDownloadCount, package, "version", "python_major")
@blueprint.route("/<package>/python_minor")
def api_downloads_python_minor(package):
"""Get the python minor download time series of a package."""
return generic_downloads(PythonMinorDownloadCount, package, "version", "python_minor")
return generic_downloads(
PythonMinorDownloadCount, package, "version", "python_minor")
@blueprint.route("/<package>/system")
def api_downloads_system(package):
"""Get the system download time series of a package."""
return generic_downloads(SystemDownloadCount, package, "os", "system")
return generic_downloads(
SystemDownloadCount, package, "os", "system")
def generic_downloads(model, package, arg, name):

View File

@@ -7,22 +7,22 @@ blueprint = Blueprint('error', __name__, template_folder='templates')
@blueprint.app_errorhandler(400)
def handle_400(err):
"""Return 400."""
return "400"
return "400", 400
@blueprint.app_errorhandler(401)
def handle_401(err):
"""Return 401."""
return "401"
return "401", 401
@blueprint.app_errorhandler(404)
def handle_404(err):
"""Return 404."""
return "404"
return "404", 404
@blueprint.app_errorhandler(500)
def handle_500(err):
"""Return 500."""
return "500"
return "500", 500

View File

@@ -1,37 +1,165 @@
"""General pages."""
from copy import deepcopy
import os
from flask import Blueprint
from flask import current_app
from flask import json
from flask import redirect
from flask import render_template
from flask_wtf import FlaskForm
import requests
from wtforms import StringField
from wtforms.validators import DataRequired
from pypistats.models.download import OverallDownloadCount
from pypistats.models.download import PythonMajorDownloadCount
from pypistats.models.download import PythonMinorDownloadCount
from pypistats.models.download import RECENT_CATEGORIES
from pypistats.models.download import RecentDownloadCount
from pypistats.models.download import SystemDownloadCount
blueprint = Blueprint('general', __name__, template_folder='templates')
blueprint = Blueprint("general", __name__, template_folder="templates")
@blueprint.route("/")
MODELS = [
OverallDownloadCount,
PythonMajorDownloadCount,
PythonMinorDownloadCount,
SystemDownloadCount,
]
class MyForm(FlaskForm):
"""Search form."""
name = StringField("Package: ", validators=[DataRequired()])
@blueprint.route("/", methods=("GET", "POST"))
def index():
"""Render the home page."""
return "PYPISTATS!"
form = MyForm()
if form.validate_on_submit():
package = form.name.data
return redirect(f"/search/{package}")
return render_template("index.html", form=form)
@blueprint.route("/search/<package>", methods=("GET", "POST"))
def search(package):
"""Render the home page."""
form = MyForm()
if form.validate_on_submit():
package = form.name.data
return redirect(f"/search/{package}")
results = RecentDownloadCount.query.filter(
RecentDownloadCount.package.like(f"{package}%"),
RecentDownloadCount.category == "month").\
order_by(RecentDownloadCount.package).\
limit(20).all()
packages = [r.package for r in results]
return render_template(
"search.html", search=True, form=form, packages=packages
)
@blueprint.route("/about")
def about():
"""Render the about page."""
return "About this website."
return render_template("about.html")
@blueprint.route("/<package>")
@blueprint.route("/package/<package>")
def package(package):
"""Render the package page."""
return package + ' main page'
# PyPI metadata
try:
metadata = requests.get(
f"https://pypi.python.org/pypi/{package}/json").json()
except Exception:
metadata = None
# Get data from db
model_data = []
for model in MODELS:
model_data.append({
"name": model.__tablename__,
"data": get_download_data(package, model),
})
# Plotly chart definitions
plot_base = json.load(
open(os.path.join(current_app.root_path, 'plots', 'plot_base.json'))
)
data_base = json.load(
open(os.path.join(current_app.root_path, 'plots', 'data_base.json'))
)
# Build the plots
plots = []
for model in model_data:
plot = deepcopy(plot_base)
data = []
for category, values in model["data"].items():
base = deepcopy(data_base)
base["x"] = values["x"]
base["y"] = values["y"]
base["name"] = category.title()
data.append(base)
plot["data"] = data
plot["layout"]["title"] = \
f"Downloads by {model['name'].title().replace('_', ' ')}"
plots.append(plot)
# Recent download stats
recent_downloads = RecentDownloadCount.query.\
filter_by(package=package).all()
recent = {r: 0 for r in RECENT_CATEGORIES}
for r in recent_downloads:
recent[r.category] = r.downloads
return render_template(
"package.html",
package=package,
plots=plots,
metadata=metadata,
recent=recent
)
def get_download_data(package, model):
"""Get the download data for a package - model."""
records = model.query.filter_by(package=package).\
order_by(model.category,
model.date).all()
data = {}
for record in records:
category = record.category
if category not in data:
data[category] = {"x": [], "y": []}
data[category]["x"].append(str(record.date))
data[category]["y"].append(record.downloads)
return data
@blueprint.route("/top")
def top():
"""Render the top packages page."""
return 'top stats'
top = []
for category in ("day", "week", "month"):
downloads = RecentDownloadCount.query.filter_by(category=category).\
filter(RecentDownloadCount.package != "__all__").\
order_by(RecentDownloadCount.downloads.desc()).limit(20).all()
top.append({
"category": category,
"packages": [{
"package": d.package,
"downloads": d.downloads,
} for d in downloads]
})
return render_template("top.html", top=top)
@blueprint.route("/status")

View File

@@ -36,7 +36,7 @@ def login():
def logout():
"""Logout."""
session.pop('user_id', None)
return redirect(url_for('index'))
return redirect(url_for('general.index'))
@blueprint.route('/github-callback')