mirror of
https://github.com/LukeHagar/pypistats.dev.git
synced 2025-12-07 20:57:44 +00:00
setup views, templates, plots
This commit is contained in:
1
Pipfile
1
Pipfile
@@ -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
21
Pipfile.lock
generated
@@ -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": {}
|
||||||
|
|||||||
@@ -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.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@@ -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)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
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
|
from pypistats.settings import TestConfig
|
||||||
|
|
||||||
|
|
||||||
|
# change this for migrations
|
||||||
app = create_app(DevConfig)
|
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
|
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
|
||||||
|
|||||||
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 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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
Reference in New Issue
Block a user