cdojs
Version:
JS wrapper for NCEI's Climate Data Online API
106 lines (90 loc) • 4.21 kB
JavaScript
'use strict'
import qs from 'querystring'
import axios from 'axios'
import _ from 'lodash'
import rateLimit from 'rate-limit-promise'
import dbg from 'debug'
const debug = dbg('CDO')
class CDO {
constructor (token, opts={}) {
this.token = token
this.opts = _.merge({}, opts)
this.secondLimiter = rateLimit(5, 1000 + CDO.RATE_LIMIT_EPSILON_MS)
this.dayLimiter = rateLimit(1000, 1000*60*60*24 + CDO.RATE_LIMIT_EPSILON_MS)
}
datasets (params={}, config={}) { return this.request('datasets', {params, ...config}) }
dataset (id, config={}) { return this.request(`datasets/${id}`, config) }
datacategories (params={}, config={}) { return this.request('datacategories', {params, ...config}) }
datacategory (id, config={}) { return this.request(`datacategories/${id}`, config) }
datatypes (params={}, config={}) { return this.request('datatypes', {params, ...config}) }
datatype (id, config={}) { return this.request(`datatypes/${id}`, config) }
locationcategories (params={}, config={}) { return this.request('locationcategories', {params, ...config}) }
locationcategory (id, config={}) { return this.request(`locationcategories/${id}`, config) }
locations (params={}, config={}) { return this.request('locations', {params, ...config}) }
location (id, config={}) { return this.request(`locations/${id}`, config) }
stations (params={}, config={}) { return this.request('stations', {params, ...config}) }
station (id, config={}) { return this.request(`stations/${id}`, config) }
data (params={}, config={}) { return this.request('data', {params, ...config}) }
all (method/*, params={}*/, ...args/*, iteratee */) {
let iteratee = args.pop()
let params = args.shift() || {}
if (typeof method === 'string') method = _.get(this, method)
return method.call(this, params, ...args).then(res => (
Promise.resolve(iteratee(res)).then(done => {
if (done) return res
let nextParams = this.constructor.paramsForNextPage(_(res.metadata.resultset)
.pick('offset', 'limit')
.defaults(params)
.value())
if (params.offset >= nextParams.offset - nextParams.limit) return null
return this.all(method, nextParams, ...args, iteratee)
})
))
}
static paramsForNextPage (currentPageParams) { return CDO._paramsForSiblingPage(currentPageParams, 'next') }
static paramsForPrevPage (currentPageParams) { return CDO._paramsForSiblingPage(currentPageParams, 'prev') }
static _paramsForSiblingPage (currentPageParams={}, direction) {
let {offset, limit=this.DEFAULT_LIMIT} = currentPageParams
if (typeof offset !== 'number' || offset < 0) offset = 0
else offset = {
next: offset + limit,
prev: offset - limit
}[direction]
return _.defaults({offset, limit}, currentPageParams)
}
request (resource, config={}) {
let mergedConfig = _.merge({
baseURL: 'http://www.ncdc.noaa.gov/cdo-web/api/v2/',
headers: {token: this.token}
}, this.opts.config, {params: this.opts.params}, config)
let params = mergedConfig.params || {}
;['startdate', 'enddate'].forEach(prop => {
if (prop in params) params[prop] = CDO.formatDate(params[prop])
})
let readableURL = `/${resource}${_.isEmpty(params)?'':' '}${qs.stringify(params)}`
return Promise
.all([this.secondLimiter(), this.dayLimiter()])
.then(() => axios(resource, mergedConfig))
.catch(res => {
let {status, statusText} = res
debug(`%s (%s %s)`, readableURL, status, statusText)
if (status === 429) return this.request.apply(this, arguments) // rate limited, try again
throw res
})
.then(({status, statusText, data}) => {
debug(`%s (%s %s)`, readableURL, status, statusText)
return data
})
}
static formatDate (date) {
if (typeof date === 'string') return date
date = new Date(date)
return `${date.getUTCFullYear()}-${CDO._formatDatePart(date.getUTCMonth()+1)}-${CDO._formatDatePart(date.getUTCDate())}`
}
static _formatDatePart (part) {
return _.padStart(part, 2, '0')
}
}
CDO.RATE_LIMIT_EPSILON_MS = 200
CDO.DEFAULT_LIMIT = 25
module.exports = CDO