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-sqlalchemy = "*"
flask-migrate = "*" flask-migrate = "*"
flask-login = "*" flask-login = "*"
flask-wtf = "*"
[dev-packages] [dev-packages]

21
Pipfile.lock generated
View File

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

View File

@@ -43,7 +43,7 @@ class Model(CRUDMixin, db.Model):
class SurrogatePK(object): class SurrogatePK(object):
"""A mixin that adds a surrogate integer 'primary key' column. """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. declarative-mapped class.
""" """

View File

@@ -55,6 +55,9 @@ class PythonMinorDownloadCount(Model):
) )
RECENT_CATEGORIES = ["day", "week", "month"]
class RecentDownloadCount(Model): class RecentDownloadCount(Model):
"""Recent day/week/month download counts.""" """Recent day/week/month download counts."""
@@ -66,7 +69,7 @@ class RecentDownloadCount(Model):
downloads = Column(db.BigInteger(), nullable=False) downloads = Column(db.BigInteger(), nullable=False)
def __repr__(self): def __repr__(self):
return "<RecentDownloadCount {}".format( return "<RecentDownloadCount {}>".format(
f"{str(self.package)} - {str(self.category)}" 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 from pypistats.settings import TestConfig
# change this for migrations
app = create_app(DevConfig) 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 SELECT
package, package,
'python_major' AS category_label, 'python_major' AS category_label,
SPLIT(python_version, '.')[ cast(SPLIT(python_version, '.')[
OFFSET OFFSET
(0)] AS category, (0)] as string) AS category,
COUNT(*) AS downloads COUNT(*) AS downloads
FROM FROM
dls dls
@@ -262,11 +262,11 @@ def get_query(date):
SELECT SELECT
package, package,
'python_minor' AS category_label, 'python_minor' AS category_label,
CONCAT(SPLIT(python_version, '.')[ cast(CONCAT(SPLIT(python_version, '.')[
OFFSET OFFSET
(0)],'.',SPLIT(python_version, '.')[ (0)],'.',SPLIT(python_version, '.')[
OFFSET OFFSET
(1)]) AS category, (1)]) as string) AS category,
COUNT(*) AS downloads COUNT(*) AS downloads
FROM FROM
dls 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 OverallDownloadCount
from pypistats.models.download import PythonMajorDownloadCount from pypistats.models.download import PythonMajorDownloadCount
from pypistats.models.download import PythonMinorDownloadCount from pypistats.models.download import PythonMinorDownloadCount
from pypistats.models.download import RECENT_CATEGORIES
from pypistats.models.download import RecentDownloadCount from pypistats.models.download import RecentDownloadCount
from pypistats.models.download import SystemDownloadCount from pypistats.models.download import SystemDownloadCount
@@ -19,17 +20,22 @@ def api_downloads_recent(package):
"""Get the recent downloads of a package.""" """Get the recent downloads of a package."""
category = request.args.get('period') category = request.args.get('period')
if category is None: if category is None:
downloads = RecentDownloadCount.query.filter_by(package=package).all() downloads = RecentDownloadCount.query.\
elif category in ("day", "week", "month"): filter_by(package=package).all()
downloads = RecentDownloadCount.query.filter_by(package=package, category=category).first() elif category in RECENT_CATEGORIES:
downloads = RecentDownloadCount.query.\
filter_by(package=package, category=category).all()
else: else:
abort(404) abort(404)
response = {"package": package, "type": "recent_downloads"} response = {"package": package, "type": "recent_downloads"}
if len(downloads) > 0: if len(downloads) > 0:
response["data"] = { if category is None:
r.category: r.downloads for r in downloads 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: else:
abort(404) abort(404)
@@ -51,7 +57,8 @@ def api_downloads_overall(package):
else: else:
downloads = OverallDownloadCount.query.\ downloads = OverallDownloadCount.query.\
filter_by(package=package).\ filter_by(package=package).\
order_by(OverallDownloadCount.category, OverallDownloadCount.date).all() order_by(OverallDownloadCount.category,
OverallDownloadCount.date).all()
response = {"package": package, "type": "overall_downloads"} response = {"package": package, "type": "overall_downloads"}
if len(downloads) > 0: if len(downloads) > 0:
@@ -69,19 +76,22 @@ def api_downloads_overall(package):
@blueprint.route("/<package>/python_major") @blueprint.route("/<package>/python_major")
def api_downloads_python_major(package): def api_downloads_python_major(package):
"""Get the python major download time series of a 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") @blueprint.route("/<package>/python_minor")
def api_downloads_python_minor(package): def api_downloads_python_minor(package):
"""Get the python minor download time series of a 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") @blueprint.route("/<package>/system")
def api_downloads_system(package): def api_downloads_system(package):
"""Get the system download time series of a 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): def generic_downloads(model, package, arg, name):

View File

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

View File

@@ -1,37 +1,165 @@
"""General pages.""" """General pages."""
from copy import deepcopy
import os
from flask import Blueprint 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 OverallDownloadCount
from pypistats.models.download import PythonMajorDownloadCount from pypistats.models.download import PythonMajorDownloadCount
from pypistats.models.download import PythonMinorDownloadCount 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 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(): def index():
"""Render the home page.""" """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") @blueprint.route("/about")
def about(): def about():
"""Render the about page.""" """Render the about page."""
return "About this website." return render_template("about.html")
@blueprint.route("/<package>") @blueprint.route("/package/<package>")
def package(package): def package(package):
"""Render the package page.""" """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") @blueprint.route("/top")
def top(): def top():
"""Render the top packages page.""" """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") @blueprint.route("/status")

View File

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