Compare commits
No commits in common. "main" and "156ca54906a7bd71b48339e2f4895127f9a4541b" have entirely different histories.
main
...
156ca54906
14
.github/workflows/workflow.yml
vendored
Normal file
14
.github/workflows/workflow.yml
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
name: Do Things
|
||||||
|
on: [push]
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@master
|
||||||
|
- uses: actions/setup-ruby@v1
|
||||||
|
with:
|
||||||
|
ruby-version: '2.6.x'
|
||||||
|
- run: |
|
||||||
|
gem install bundler
|
||||||
|
bundle install --jobs 4 --retry 3
|
||||||
|
bundle exec rake
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,4 +1,3 @@
|
|||||||
.*env
|
.*env
|
||||||
*.csv
|
*.csv
|
||||||
.ruby-version
|
.ruby-version
|
||||||
.envrc
|
|
||||||
|
13
.rubocop.yml
Normal file
13
.rubocop.yml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
inherit_from: .rubocop_todo.yml
|
||||||
|
|
||||||
|
AllCops:
|
||||||
|
TargetRubyVersion: 2.6
|
||||||
|
|
||||||
|
Style/Documentation:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Style/AccessModifierDeclarations:
|
||||||
|
EnforcedStyle: inline
|
||||||
|
|
||||||
|
Layout/MultilineMethodCallIndentation:
|
||||||
|
EnforcedStyle: indented
|
7
.rubocop_todo.yml
Normal file
7
.rubocop_todo.yml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# This configuration was generated by
|
||||||
|
# `rubocop --auto-gen-config`
|
||||||
|
# on 2020-01-11 15:25:31 -0500 using RuboCop version 0.78.0.
|
||||||
|
# The point is for the user to remove these configuration records
|
||||||
|
# one by one as the offenses are removed from the code base.
|
||||||
|
# Note that changes in the inspected code, or installation of new
|
||||||
|
# versions of RuboCop, may require this file to be generated again.
|
42
Code.gs
42
Code.gs
@ -1,42 +0,0 @@
|
|||||||
// vim:filetype=javascript
|
|
||||||
const defaultSeriesURL = "https://api.bls.gov/publicAPI/v2/timeseries/data/";
|
|
||||||
const defaultSeriesID = "CUUR0200SA0";
|
|
||||||
const defaultStartYear = "2014";
|
|
||||||
|
|
||||||
function CURRENTCPI(blsToken, options) {
|
|
||||||
var seriesURL = options?.seriesURL || defaultSeriesURL;
|
|
||||||
var seriesID = options?.seriesID || defaultSeriesID;
|
|
||||||
var startYear = options?.startYear || defaultStartYear;
|
|
||||||
var endYear = options?.endYear || new Date().getFullYear().toString();
|
|
||||||
|
|
||||||
var reqOptions = {
|
|
||||||
"method": "post",
|
|
||||||
"payload": JSON.stringify({
|
|
||||||
"seriesid": [seriesID],
|
|
||||||
"startyear": startYear,
|
|
||||||
"endyear": endYear
|
|
||||||
}),
|
|
||||||
"contentType": "application/json",
|
|
||||||
"headers": {
|
|
||||||
"Authorization": "token "+blsToken,
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var resp = UrlFetchApp.fetch(seriesURL, reqOptions);
|
|
||||||
var rawData = resp.getContentText().toString().trim();
|
|
||||||
var parsedData = JSON.parse(rawData);
|
|
||||||
var respData = parsedData.Results.series[0].data;
|
|
||||||
var rows = [["year", "period", "period_name", "value"]];
|
|
||||||
|
|
||||||
for (var i = 0; i < respData.length; i++) {
|
|
||||||
rows.push([
|
|
||||||
respData[i].year,
|
|
||||||
respData[i].period,
|
|
||||||
respData[i].periodName,
|
|
||||||
respData[i].value
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return rows;
|
|
||||||
}
|
|
11
Gemfile
Normal file
11
Gemfile
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
source 'https://rubygems.org'
|
||||||
|
|
||||||
|
ruby '2.7.2' if ENV.key?('DYNO')
|
||||||
|
|
||||||
|
gem 'aws-sdk', '~> 2'
|
||||||
|
gem 'pry', group: %i[development test]
|
||||||
|
gem 'rack'
|
||||||
|
gem 'rake'
|
||||||
|
gem 'rubocop', group: %i[development test]
|
49
Gemfile.lock
Normal file
49
Gemfile.lock
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
GEM
|
||||||
|
remote: https://rubygems.org/
|
||||||
|
specs:
|
||||||
|
ast (2.4.0)
|
||||||
|
aws-eventstream (1.0.3)
|
||||||
|
aws-sdk (2.11.421)
|
||||||
|
aws-sdk-resources (= 2.11.421)
|
||||||
|
aws-sdk-core (2.11.421)
|
||||||
|
aws-sigv4 (~> 1.0)
|
||||||
|
jmespath (~> 1.0)
|
||||||
|
aws-sdk-resources (2.11.421)
|
||||||
|
aws-sdk-core (= 2.11.421)
|
||||||
|
aws-sigv4 (1.1.0)
|
||||||
|
aws-eventstream (~> 1.0, >= 1.0.2)
|
||||||
|
coderay (1.1.2)
|
||||||
|
jaro_winkler (1.5.4)
|
||||||
|
jmespath (1.4.0)
|
||||||
|
method_source (0.9.2)
|
||||||
|
parallel (1.19.1)
|
||||||
|
parser (2.7.0.1)
|
||||||
|
ast (~> 2.4.0)
|
||||||
|
pry (0.12.2)
|
||||||
|
coderay (~> 1.1.0)
|
||||||
|
method_source (~> 0.9.0)
|
||||||
|
rack (2.0.8)
|
||||||
|
rainbow (3.0.0)
|
||||||
|
rake (13.0.1)
|
||||||
|
rubocop (0.78.0)
|
||||||
|
jaro_winkler (~> 1.5.1)
|
||||||
|
parallel (~> 1.10)
|
||||||
|
parser (>= 2.6)
|
||||||
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
|
ruby-progressbar (~> 1.7)
|
||||||
|
unicode-display_width (>= 1.4.0, < 1.7)
|
||||||
|
ruby-progressbar (1.10.1)
|
||||||
|
unicode-display_width (1.6.0)
|
||||||
|
|
||||||
|
PLATFORMS
|
||||||
|
ruby
|
||||||
|
|
||||||
|
DEPENDENCIES
|
||||||
|
aws-sdk (~> 2)
|
||||||
|
pry
|
||||||
|
rack
|
||||||
|
rake
|
||||||
|
rubocop
|
||||||
|
|
||||||
|
BUNDLED WITH
|
||||||
|
2.1.2
|
2
LICENSE
2
LICENSE
@ -1,6 +1,6 @@
|
|||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright © 2023 Dan Buch
|
Copyright © 2020 Dan Buch
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
15
README.md
15
README.md
@ -1,20 +1,21 @@
|
|||||||
# cpi-feed
|
# cpi-feed
|
||||||
|
|
||||||
Transforms [this](https://api.bls.gov/publicAPI/v2/timeseries/data/CUUSA210SA0)
|
Transforms [this](https://api.bls.gov/publicAPI/v2/timeseries/data/CUUSA210SA0)
|
||||||
into CSV rows.
|
into [this](https://s3.amazonaws.com/meatballhat/cpi/current.csv) :tada:.
|
||||||
|
|
||||||
## usage
|
## usage
|
||||||
|
|
||||||
This is intended for use with Google Sheets via:
|
This is intended for use with Google Sheets via:
|
||||||
|
|
||||||
```
|
```
|
||||||
=CURRENTCPI("{bls-token}")
|
=IMPORTDATA("https://s3.amazonaws.com/meatballhat/cpi/current.csv")
|
||||||
```
|
```
|
||||||
|
|
||||||
where `{bls-token}` is the value issued after registering with
|
|
||||||
[bls.gov](https://www.bls.gov/developers/home.htm).
|
|
||||||
|
|
||||||
## deployment
|
## deployment
|
||||||
|
|
||||||
Just like [this
|
A copy of this thing is deployed to Heroku with a Heroku Scheduler addon
|
||||||
example](https://apipheny.io/import-json-google-sheets/).
|
configured to run the following once daily:
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
bundle exec ./sync
|
||||||
|
```
|
||||||
|
11
Rakefile
Normal file
11
Rakefile
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
begin
|
||||||
|
require 'rubocop/rake_task'
|
||||||
|
rescue LoadError => e
|
||||||
|
warn e
|
||||||
|
end
|
||||||
|
|
||||||
|
RuboCop::RakeTask.new if defined?(RuboCop)
|
||||||
|
|
||||||
|
task default: %i[rubocop]
|
3
config.ru
Normal file
3
config.ru
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
run ->(*) { [301, { 'Location' => ENV['CPI_FEED_URL'] }, []] }
|
94
cpi_fetcher.rb
Normal file
94
cpi_fetcher.rb
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'csv'
|
||||||
|
require 'json'
|
||||||
|
require 'net/http'
|
||||||
|
require 'net/https'
|
||||||
|
require 'uri'
|
||||||
|
|
||||||
|
class CPIFetcher
|
||||||
|
def cpi
|
||||||
|
resp = fetch_raw_response
|
||||||
|
return nil unless resp['Results']['series']
|
||||||
|
|
||||||
|
resp['Results']['series'].first['data']
|
||||||
|
end
|
||||||
|
|
||||||
|
def cpi_csv
|
||||||
|
CSV.generate do |csv|
|
||||||
|
csv << %w[year period period_name value]
|
||||||
|
cpi.each do |rec|
|
||||||
|
csv << %w[
|
||||||
|
year period periodName value
|
||||||
|
].map { |k| rec.fetch(k) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private def url
|
||||||
|
@url ||= URI(ENV['CPI_SERIES_URL'] || File.join(
|
||||||
|
'https://api.bls.gov',
|
||||||
|
'publicAPI/v2/timeseries/data/'
|
||||||
|
))
|
||||||
|
end
|
||||||
|
|
||||||
|
private def series_id
|
||||||
|
@series_id ||= ENV.fetch(
|
||||||
|
'CPI_SERIES_ID', 'CUUR0200SA0'
|
||||||
|
).split(/[, ]/).map(&:strip).reject(&:empty?)
|
||||||
|
end
|
||||||
|
|
||||||
|
private def start_year
|
||||||
|
@start_year ||= ENV.fetch('CPI_SERIES_START_YEAR', '2014')
|
||||||
|
end
|
||||||
|
|
||||||
|
private def end_year
|
||||||
|
@end_year ||= ENV.fetch('CPI_SERIES_END_YEAR', Time.now.year.to_s)
|
||||||
|
end
|
||||||
|
|
||||||
|
private def bls_token
|
||||||
|
@bls_token ||= ENV.fetch('BLS_TOKEN')
|
||||||
|
end
|
||||||
|
|
||||||
|
private def start_cpi_value(data)
|
||||||
|
data = data.sort do |a, b|
|
||||||
|
record_key(a) <=> record_key(b)
|
||||||
|
end
|
||||||
|
data.first.fetch('value')
|
||||||
|
end
|
||||||
|
|
||||||
|
private def record_key(record)
|
||||||
|
"#{record['year']}.#{record['period']}"
|
||||||
|
end
|
||||||
|
|
||||||
|
private def latest_cpi_value(data)
|
||||||
|
data.find { |d| d['latest'] == 'true' }.fetch('value')
|
||||||
|
end
|
||||||
|
|
||||||
|
private def fetch_raw_response
|
||||||
|
Net::HTTP.new(url.hostname, url.port)
|
||||||
|
.tap { |h| h.use_ssl = true }
|
||||||
|
.tap { |h| h.verify_mode = OpenSSL::SSL::VERIFY_PEER }
|
||||||
|
.then { |h| JSON.parse(h.request(build_request).body) }
|
||||||
|
end
|
||||||
|
|
||||||
|
private def build_request
|
||||||
|
Net::HTTP::Post.new(url)
|
||||||
|
.tap { |r| r['Authorization'] = "token #{bls_token}" }
|
||||||
|
.tap { |r| r['Content-Type'] = 'application/json' }
|
||||||
|
.tap { |r| r.body = JSON.generate(build_request_body) }
|
||||||
|
end
|
||||||
|
|
||||||
|
private def build_request_body
|
||||||
|
{
|
||||||
|
'seriesid' => series_id,
|
||||||
|
'startyear' => start_year,
|
||||||
|
'endyear' => end_year
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if $PROGRAM_NAME == __FILE__
|
||||||
|
puts CPIFetcher.new.cpi_csv
|
||||||
|
exit 0
|
||||||
|
end
|
35
mini_s3put.rb
Normal file
35
mini_s3put.rb
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'aws-sdk'
|
||||||
|
|
||||||
|
class MiniS3put
|
||||||
|
def initialize(key: nil, instream: $stdin)
|
||||||
|
@bucket = ENV.fetch('CPI_FEED_AWS_BUCKET')
|
||||||
|
@key = key || ENV.fetch('CPI_FEED_AWS_KEY')
|
||||||
|
@instream = instream
|
||||||
|
end
|
||||||
|
|
||||||
|
attr_reader :bucket, :key, :instream
|
||||||
|
private :bucket, :key, :instream
|
||||||
|
|
||||||
|
def put
|
||||||
|
Aws::S3::Resource.new.bucket(bucket).object(key).put(
|
||||||
|
body: instream.read
|
||||||
|
).on_success(&method(:on_put_success))
|
||||||
|
end
|
||||||
|
|
||||||
|
private def on_put_success(response)
|
||||||
|
puts response.data
|
||||||
|
|
||||||
|
Aws::S3::Client.new.put_object_acl(
|
||||||
|
bucket: bucket, key: key, acl: 'public-read'
|
||||||
|
).on_success do |acl_response|
|
||||||
|
puts acl_response.data
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if $PROGRAM_NAME == __FILE__
|
||||||
|
MiniS3put.new(key: ARGV.first).put
|
||||||
|
exit 0
|
||||||
|
end
|
Loading…
x
Reference in New Issue
Block a user