From dea6df0ded0ea8e080dd29d491372cbaa2b8484c Mon Sep 17 00:00:00 2001 From: chris48s Date: Sat, 6 Oct 2018 16:38:41 +0100 Subject: [PATCH] bring back the [PyPI] downloads badges (#2131) --- services/pypi/pypi-downloads.service.js | 106 ++++++++++++++++++++++-- services/pypi/pypi.tester.js | 38 ++++----- 2 files changed, 115 insertions(+), 29 deletions(-) diff --git a/services/pypi/pypi-downloads.service.js b/services/pypi/pypi-downloads.service.js index b38a0794a6..bf33bbec9b 100644 --- a/services/pypi/pypi-downloads.service.js +++ b/services/pypi/pypi-downloads.service.js @@ -1,12 +1,100 @@ 'use strict' -const deprecatedService = require('../deprecated-service') -const PypiBase = require('./pypi-base') +const Joi = require('joi') +const BaseJsonService = require('../base-json') +const { downloadCount } = require('../../lib/color-formatters') +const { metric } = require('../../lib/text-formatters') +const { nonNegativeInteger } = require('../validators') -// https://github.com/badges/shields/issues/716 -module.exports = ['pypi/dm', 'pypi/dw', 'pypi/dd'].map(base => - deprecatedService({ - category: 'downloads', - url: PypiBase.buildUrl(base), - }) -) +const pypiStatsSchema = Joi.object({ + data: Joi.object({ + last_day: nonNegativeInteger, + last_week: nonNegativeInteger, + last_month: nonNegativeInteger, + }), +}).required() + +const periodMap = { + dd: { + api_field: 'last_day', + suffix: '/day', + }, + dw: { + api_field: 'last_week', + suffix: '/week', + }, + dm: { + api_field: 'last_month', + suffix: '/month', + }, +} + +// this badge uses PyPI Stats instead of the PyPI API +// so it doesn't extend PypiBase +module.exports = class PypiDownloads extends BaseJsonService { + async fetch({ pkg }) { + const url = `https://pypistats.org/api/packages/${pkg.toLowerCase()}/recent` + return this._requestJson({ + url, + schema: pypiStatsSchema, + errorMessages: { 404: 'package not found' }, + }) + } + + static render({ period, downloads }) { + return { + message: `${metric(downloads)}${periodMap[period].suffix}`, + color: downloadCount(downloads), + } + } + + async handle({ period, pkg }) { + const json = await this.fetch({ pkg }) + return this.constructor.render({ + period, + downloads: json.data[periodMap[period].api_field], + }) + } + + static get defaultBadgeData() { + return { label: 'downloads' } + } + + static get category() { + return 'downloads' + } + + static get url() { + return { + base: 'pypi', + format: '(dd|dw|dm)/(.+)', + capture: ['period', 'pkg'], + } + } + + static get examples() { + return [ + { + title: 'PyPI - Downloads', + exampleUrl: 'dd/Django', + urlPattern: 'dd/:package', + staticExample: this.render({ period: 'dd', downloads: 14000 }), + keywords: ['python'], + }, + { + title: 'PyPI - Downloads', + exampleUrl: 'dw/Django', + urlPattern: 'dw/:package', + staticExample: this.render({ period: 'dw', downloads: 250000 }), + keywords: ['python'], + }, + { + title: 'PyPI - Downloads', + exampleUrl: 'dm/Django', + urlPattern: 'dm/:package', + staticExample: this.render({ period: 'dm', downloads: 1070100 }), + keywords: ['python'], + }, + ] + } +} diff --git a/services/pypi/pypi.tester.js b/services/pypi/pypi.tester.js index 31a63c5cdd..efbb47e2e4 100644 --- a/services/pypi/pypi.tester.js +++ b/services/pypi/pypi.tester.js @@ -2,7 +2,7 @@ const Joi = require('joi') const ServiceTester = require('../service-tester') -const { isSemver } = require('../test-validators') +const { isMetricOverTimePeriod, isSemver } = require('../test-validators') const isPsycopg2Version = Joi.string().regex(/^v([0-9][.]?)+$/) @@ -15,37 +15,35 @@ const isPipeSeparatedDjangoVersions = isPipeSeparatedPythonVersions const t = new ServiceTester({ id: 'pypi', title: 'PyPi badges' }) module.exports = t -/* - tests for downloads endpoints +// tests for downloads endpoints - Note: - Download statistics are no longer available from pypi - it is exptected that the download badges all show - 'no longer available' -*/ -t.create('daily downloads (expected failure)') +t.create('daily downloads (valid)') .get('/dd/djangorestframework.json') - .expectJSON({ name: 'downloads', value: 'no longer available' }) + .expectJSONTypes({ name: 'downloads', value: isMetricOverTimePeriod }) -t.create('weekly downloads (expected failure)') +t.create('weekly downloads (valid)') .get('/dw/djangorestframework.json') - .expectJSON({ name: 'downloads', value: 'no longer available' }) + .expectJSONTypes({ name: 'downloads', value: isMetricOverTimePeriod }) -t.create('monthly downloads (expected failure)') +t.create('monthly downloads (valid)') .get('/dm/djangorestframework.json') - .expectJSON({ name: 'downloads', value: 'no longer available' }) + .expectJSONTypes({ name: 'downloads', value: isMetricOverTimePeriod }) -t.create('daily downloads (invalid)') +t.create('downloads (mixed-case package name)') + .get('/dd/DjangoRestFramework.json') + .expectJSONTypes({ name: 'downloads', value: isMetricOverTimePeriod }) + +t.create('daily downloads (not found)') .get('/dd/not-a-package.json') - .expectJSON({ name: 'downloads', value: 'no longer available' }) + .expectJSON({ name: 'downloads', value: 'package not found' }) -t.create('weekly downloads (invalid)') +t.create('weekly downloads (not found)') .get('/dw/not-a-package.json') - .expectJSON({ name: 'downloads', value: 'no longer available' }) + .expectJSON({ name: 'downloads', value: 'package not found' }) -t.create('monthly downloads (invalid)') +t.create('monthly downloads (not found)') .get('/dm/not-a-package.json') - .expectJSON({ name: 'downloads', value: 'no longer available' }) + .expectJSON({ name: 'downloads', value: 'package not found' }) /* tests for version endpoint