mirror of
https://github.com/LukeHagar/pypistats.org.git
synced 2025-12-06 04:21:09 +00:00
setup views, templates, plots
This commit is contained in:
1
Pipfile
1
Pipfile
@@ -15,6 +15,7 @@ github-flask = "*"
|
||||
flask-sqlalchemy = "*"
|
||||
flask-migrate = "*"
|
||||
flask-login = "*"
|
||||
flask-wtf = "*"
|
||||
|
||||
|
||||
[dev-packages]
|
||||
|
||||
21
Pipfile.lock
generated
21
Pipfile.lock
generated
@@ -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": {}
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
|
||||
@@ -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)}"
|
||||
)
|
||||
|
||||
|
||||
33
pypistats/plots/data_base.json
Normal file
33
pypistats/plots/data_base.json
Normal 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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
78
pypistats/plots/plot_base.json
Normal file
78
pypistats/plots/plot_base.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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
301
pypistats/static/style.css
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
64
pypistats/templates/about.html
Normal file
64
pypistats/templates/about.html
Normal 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 %}
|
||||
16
pypistats/templates/index.html
Normal file
16
pypistats/templates/index.html
Normal 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 %}
|
||||
84
pypistats/templates/layout.html
Normal file
84
pypistats/templates/layout.html
Normal 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>
|
||||
67
pypistats/templates/package.html
Normal file
67
pypistats/templates/package.html
Normal 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 %}
|
||||
12
pypistats/templates/results.html
Normal file
12
pypistats/templates/results.html
Normal 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 %}
|
||||
1
pypistats/templates/search.html
Normal file
1
pypistats/templates/search.html
Normal file
@@ -0,0 +1 @@
|
||||
{% include "index.html" %}
|
||||
37
pypistats/templates/top.html
Normal file
37
pypistats/templates/top.html
Normal 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 %}
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user