diff --git a/.gitignore b/.gitignore index 20312e8..fcffe9a 100644 --- a/.gitignore +++ b/.gitignore @@ -45,9 +45,9 @@ build-iPhoneSimulator/ # for a library or gem, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: -# Gemfile.lock -# .ruby-version -# .ruby-gemset +Gemfile.lock +.ruby-version +.ruby-gemset # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: .rvmrc @@ -55,6 +55,4 @@ build-iPhoneSimulator/ # Used by RuboCop. Remote config files pulled in from inherit_from directive. # .rubocop-https?--* -# Ruby Gemfile.lock -*.lock diff --git a/.rspec_status b/.rspec_status deleted file mode 100644 index 661d71d..0000000 --- a/.rspec_status +++ /dev/null @@ -1,12 +0,0 @@ -example_id | status | run_time | ----------------------------- | ------ | --------------- | -./spec/config_spec.rb[1:1:1] | passed | 0.00112 seconds | -./spec/config_spec.rb[1:1:2] | passed | 0.00012 seconds | -./spec/config_spec.rb[1:1:3] | passed | 0.00009 seconds | -./spec/data_spec.rb[1:1:1] | passed | 0.00115 seconds | -./spec/data_spec.rb[1:1:2] | passed | 0.00012 seconds | -./spec/data_spec.rb[1:1:3] | passed | 0.00009 seconds | -./spec/data_spec.rb[1:1:4] | passed | 0.00009 seconds | -./spec/data_spec.rb[2:1:1] | passed | 0.2051 seconds | -./spec/data_spec.rb[2:1:2] | passed | 0.00039 seconds | -./spec/data_spec.rb[2:1:3] | passed | 0.0003 seconds | diff --git a/.rubocop.yml b/.rubocop.yml deleted file mode 100644 index 1de8c2c..0000000 --- a/.rubocop.yml +++ /dev/null @@ -1,3 +0,0 @@ -AllCops: - TargetRubyVersion: '2.6' - NewCops: enable diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml deleted file mode 100644 index 5c39285..0000000 --- a/.rubocop_todo.yml +++ /dev/null @@ -1,22 +0,0 @@ -# This configuration was generated by -# `rubocop --auto-gen-config` -# on 2022-01-26 10:27:09 UTC using RuboCop version 1.24.1. -# 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. - -# Offense count: 2 -# Configuration parameters: IgnoredMethods, CountRepeatedAttributes. -Metrics/AbcSize: - Max: 32 - -# Offense count: 1 -# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods. -# IgnoredMethods: refine -Metrics/BlockLength: - Max: 43 - -AllCops: - NewCops: enable - TargetRubyVersion: '2.6' diff --git a/.ruby-version b/.ruby-version deleted file mode 100644 index 75a22a2..0000000 --- a/.ruby-version +++ /dev/null @@ -1 +0,0 @@ -3.0.3 diff --git a/CHANGELOG.md b/CHANGELOG.md index 1301166..3f589bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,34 +1,24 @@ ------BEGIN PGP SIGNED MESSAGE----- -Hash: SHA512 +## [1.0.0-dev] -## [Unreleased] +Testing changes before merge. - - Prepare `version-0.4.1` to be ready for Open Source under MIT license. - - Removed gem `colorize` because of GPLv2. - - Override String class with color codes for Linux. +**About:** -## [0.4.0] - 2022-01-24 + - Allow multiple sources + - Class-Template based - easy to add more APIs + - New sources are automatically required and added to `.gemspec` + - Each source can be required alone + - List helper - list available sources (required in gem) + - Cli helper - print colorized output (required only in executable) + - Save prices in `.csv` table + - Extract all data in `.json` file - - Add options `--loop` and `--wait` for terminal scripting - - Only signed and verified commits from `v-0.4.0` - - PGP Public key available at https://www.github.com/alx3dev/contact - - Before `v-0.4.0` unverified commits were pushed. - - From now on, only VERIFIED signed commits should be trusted +**Security:** ------BEGIN PGP SIGNATURE----- + - `CTFC::Client` use `eval` - to call source class to extract data. Only source name is dinamic, and we check for persistence of source class before evaluation. There's no place for malicious use. -iQIzBAEBCgAdFiEEtl+v9iZj5AyyvRYdTFhE2vUGMucFAmHvxM0ACgkQTFhE2vUG -MuejCQ//Uw+xaFBNrat3evChlifrxAckEqhtQeUwiuluSkReYmV+TqdSNqhzzezh -BJkgp5d7/Lzt/7wCwK9aVuvejKG0op4Ernk1jRH7jdOkH7TeqVyRe8837XXbn8Q3 -FRh5ih/dbbeQtvIVuI1Z7unRQGvlfMnBKK65wQfHRffJg3kTgO4ICx0Gz1LgxfCa -OwjSYp2+pXQtGOI+ab9/a7avBb6TIW5uQjegZ5LsVR/at3RySki8v1fOA0Tfmsgv -pZ4aKhZkpVhZ2l6fj181hBwBspmJPxpB8v4mZXhXAfe9z/425favlfyKLxRvmFu5 -aQK6hZ/gTEXz2s0ypVVPtwQS5FK3YurhKeun6Vto1lQJM10MngwB53B18eNSAB3h -tsFks8QojwtPH2nttksWOKy5V5tQdBB0uCd72DdEwnAOo1jyMhG7Otkpt4ch5CAd -xRhmrC0Pp1GgqH2avpeezA3uQZ2HW2Gp1nFnKHeRhPutEBF8x8NLjCxbS3GExrgk -+Lijy13Eu/y4f2S7TIfN/LMB+ruqQrZe+NmzE8J1tuMcaDqIYsV17c5FXvaxGS/q -9mJ++v6xHfMykvCNR/FtuUxq+TaD/aNBm+pKYhjqzr4jmoAy2h1gvQEIl4OMXxlf -5xzgdP6ePWkdwWnYvMtrIbkVWfKvfaBUgYsT/+FRVDierWq2aj0= -=/ZSG ------END PGP SIGNATURE----- + - Example of eval use in CTFC - call source class to extract data: +```ruby +eval "CTFC::API::#{source_class} if List.sources.include? source_class" +``` diff --git a/README.md b/README.md index 63071b9..11f1136 100644 --- a/README.md +++ b/README.md @@ -1,178 +1 @@ -[**NOTE:**](https://www.github.com/alx3dev/ctfc/LICENSE_DEPENDENCIES.md) - >Removed gem `colorize` to allow **MIT** license. - >All comits from `v-0.4.0` must be signed and tagged **verified**. - - -# About -Convert any crypto to fiat currency, gather all data and/or save in `.csv` table. -For now only prices are printed/saved, while all data remain easily accessible from variable (for developers). - - -# How to install -Make sure you have ruby and git installed - -Install from source: -```bash - git clone https://github.com/alx3dev/ctfc.git - cd ctfc && bundle install -``` - -Install from rubygems: - -```bash -gem install ctfc -``` -# How to run - - **Read documentation on:** https://rubydoc.info/gems/ctfc/CTFC/Data - -```bash -ruby bin/ctfc fiat_1 fiat_2 fiat_3 -``` - -This command also accept multiple arguments: - - - `--no-save` - do not save `.csv.` output - - `--no-print` - do not print terminal output - - `--coins` - coins to scrap (default: BTC, LTC, XMR, ETH, BCH, ZEC ) - - `--loop` - repeat script N times (default 1) - - `--wait` - wait N seconds between loops (default 0) - - `--help` - help menu - - -# Script Examples - 1 - Run script without arguments (default options) - -```ruby - ruby bin/ctfc - - => return: - print EUR rates for default coins (BTC, LTC, XMR, ETH, BCH, ZEC) - do not save '.csv' table -``` - - - 2 - Add fiat currencies as arguments - -```ruby -ruby bin/ctfc eur usd rsd - - => return: - print EUR, USD, RSD rates for default coins - save data in '.csv' table with pattern: 'crypto_#{CURRENCY}.csv' - -> './crypto_eur.csv', './crypto_usd.csv', './crypto_rsd.csv' -``` - - 3 - Use `--no-save`, `--no-print`, `--loop`, `--wait` - -```ruby -ruby bin/ctfc eur --no-print --coins btc xmr ltc - - => return: - save EUR rates for BTC, XMR and LTC - do not print output - - -ruby bin/ctfc rsd --no-save --coins btc xmr - - => return: - print RSD rates for BTC and XMR - - -# added in version 0.4.0 -ruby bin/ctfc rsd --no-print --loop 1440 --wait 60 - - => return: - save RSD rates without print, run each minute for 24 hours -``` - - -# Developer Examples -```ruby - # define coins to scrap - COINS = %w[ BTC XMR LTC ETH ] - - # initialize Data class - @data = Ctfc.new :eur, save: false, print: false, coins: COINS - => return Ctfc object to work with - -> # - - # execute request - @data.get - => return Hash with upcase string coins as keys, and float prices - -> {"BTC"=>36760.11, "XMR"=>169.55, "LTC"=>114.4, "ETH"=>2746.22} - - # now you can use ::Data instance methods - @data.response - => return RestClient response to cryptocomare API - -> - - # check request url - @data.url - => return Cryptocompare API url - -> "https://min-api.cryptocompare.com/data/pricemultifull?fsyms=BTC&fsyms=LTC&fsyms=XMR&fsyms=ETH&fsyms=BCH&fsyms=ZEC&tsyms=EUR" - - # name of csv table (saved in working directory) - @data.table - => return '.csv' table name - -> 'ctfc_eur.csv' - - # array of coins to work with - @data.coins - => return coins for scrap, also allow setter method @data.coins = [...] - -> ['BTC', 'XMR', 'LTC', 'ETH'] - - # get all data about all coins (json api response) - @data.data - => return all data returned by cryptocompare API - -> {"RAW"=> - {"BTC"=> - {"EUR"=> - {"TYPE"=>"5", - "MARKET"=>"CCCAGG", - "FROMSYMBOL"=>"BTC", - "TOSYMBOL"=>"EUR", - "FLAGS"=>"2049", - "PRICE"=>33851.17, - "LASTUPDATE"=>1642773847, - "MEDIAN"=>33853.8, - "LASTVOLUME"=>0.1, - "LASTVOLUMETO"=>3384.3676, - "LASTTRADEID"=>"2024043", - ... ... ... ... ... ... ... - - - TO BE CONTINIUED ... -``` - -**Class methods as shortcuts:** - -```ruby -# get default coins in EUR, save output without printing - prices = Ctfc.to :eur, print: false - -# get default coins in RSD, print output, don't save - Ctfc.to :rsd, save: false - -# For those who don't like name `Ctfc`, you can use `Crypto` too: - prices = Crypto.to :eur, coins: %w[BTC XMR] -``` - -# Tests -To run tests call `rspec --format doc` -To test code syntax use `./check-syntax.sh`. -This command will run rubocop for code inspection, but with some errors hidden by `.rubocop_todo.yml`. Using check-syntax script, all test should pass. - - -# Contribution -Any contribution is highly appreciated, as long as you follow Code of Conduct. - - - Fork repository - - Make your changes - - Write tests - - Submit pull request - -# License -Don't be a dick - it's MIT. - -# To-Do: -See **Projects** +This is development branch, testing code before merge. diff --git a/bin/console b/bin/console index 45e0f5d..05750db 100755 --- a/bin/console +++ b/bin/console @@ -1,4 +1,4 @@ -#/usr/bin/env ruby +#!/usr/bin/env ruby # frozen_string_literal: true require 'pry' diff --git a/bin/ctfc b/bin/ctfc index 39ac318..f0738fa 100755 --- a/bin/ctfc +++ b/bin/ctfc @@ -1,12 +1,14 @@ #!/usr/bin/env ruby # frozen_string_literal: true -require_relative '../lib/ctfc' require 'optimist' +require_relative '../lib/ctfc' +require_relative '../lib/ctfc/helpers/cli' + +COINS = %w[BTC XMR ETH LTC].freeze opts = Optimist.options do - version "Crypto To Fiat Currency\n".cyan.bold \ - + "Gem Version: #{CTFC::VERSION}" + version "Crypto To Fiat Currency\nGem Version: #{CTFC::VERSION}" banner '' banner ' Enter fiat currencies with/out additional arguments:' @@ -15,44 +17,38 @@ opts = Optimist.options do banner ' ruby bin/ctfc eur usd --no-save --coins btc xmr ltc' banner '' - opt :coins, 'Set crypto coins', default: CTFC::CONFIG::COINS - opt :no_save, "Do not save '.csv' output" - opt :no_print, 'Do not print terminal output' - opt :loop, 'Run script N times', default: 1, type: :integer - opt :wait, 'Wait N seconds between loop', default: 0, type: :integer + opt :coins, 'Set crypto coins', default: COINS + opt :save, "Save prices in '.csv' table", default: false + opt :export, "Export all data in '.json' file", default: false + opt :no_print, 'Do not print terminal output', default: false + opt :source, 'Set source to extract data', default: 'cryptocompare' + opt :loop, 'Run script N times', default: 1, type: :integer + opt :wait, 'Wait N seconds between loop', default: 0, type: :integer end -coins = opts[:coins] -save = opts[:no_save] ? false : true -print = opts[:no_print] ? false : true - -@crypto = Ctfc.new +coins = opts[:coins] +source = opts[:source] +save = opts[:save] +export = opts[:export] +print = true unless opts[:no_print] if ARGV.empty? - - # default behavior without arguments - change to suit your needs - @crypto.get(:eur, save: false, print: true, coins: coins) - + prices = Crypto.to :usd, coins, source.to_sym, save: save, export: export + Cli.print_output :USD, prices if print else - opts[:loop].times do ARGV.each do |fiat| next if opts.include?(fiat.downcase) - # in Ruby 3.1: @crypto.get(fiat, save:, print:, coins:) - @crypto.get(fiat, - save: save, - print: print, - coins: coins) - sleep 1 # wait between requests + + prices = Crypto.to fiat, coins, source.to_sym, save: save, export: export + next unless print + + system 'clear' or system 'cls' + Cli.print_output fiat, prices end - # end if no `--loop` arg next unless opts[:loop] > 1 - # pause between loops sleep opts[:wait] - - # clear screan - depending on OS - system 'clear' or system 'cls' end end diff --git a/check-syntax.sh b/check-syntax.sh deleted file mode 100755 index 65b8e3b..0000000 --- a/check-syntax.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash - -## -# Inspect source code with rubocop. Do not change .rubocop_todo.yml. -# -# To check your changes in source code, run: -# ./check-syntax.sh -# -# To inspect all errors that should be fixed run: -# ./check-syntax.sh --all -# -# This will return all errors hidden by todo file. -# Errors should be fixed, then manualy removed from .rubocop_todo.yml -## - -if [ "$1" == "-a" ] || [ "$1" == "--all" ] || [ "$1" == "--total" ]; then -rubocop --format simple --config .rubocop.yml -else -rubocop --format simple --config .rubocop_todo.yml -fi diff --git a/ctfc.gemspec b/ctfc.gemspec index e462967..da196c6 100644 --- a/ctfc.gemspec +++ b/ctfc.gemspec @@ -1,14 +1,16 @@ # frozen_string_literal: true require_relative './lib/ctfc/version' +require_relative './lib/ctfc/helpers/list' Gem::Specification.new do |s| s.name = 'ctfc' s.version = CTFC::VERSION - s.summary = 'Cryptocurrency to Fiat values, get data and save prices.' + s.summary = 'Cryptocurrency data gathering gem. Scrap and save as CSV and/or JSON.' s.description = <<~DESCRIPTION - Convert any cryptocurrency to any fiat value, export data to csv table. - Print colorized terminal output. + Cryptocurrency data gathering gem. Get data from multiple APIs, print and + save output as you wish. Run script from terminal, or use in another app. + Class-template based, easy to extend to add more sources. MIT License. DESCRIPTION s.license = 'MIT' @@ -27,24 +29,29 @@ Gem::Specification.new do |s| s.metadata['license_uri'] = 'https://github.com/alx3dev/ctfc/LICENSE' s.metadata['rubygems_mfa_required'] = 'true' - s.files = %w[ bin/ctfc - bin/console - lib/ctfc.rb + s.files = %w[ lib/ctfc.rb + lib/ctfc/client.rb + lib/ctfc/export.rb lib/ctfc/version.rb - lib/ctfc/base.rb - lib/ctfc/config.rb + lib/ctfc/api.rb + lib/ctfc/api/apitemplate.rb LICENSE README.md ctfc.gemspec] - s.required_ruby_version = '>= 2.6', '< 4' + # auto-add sources in api dir + List.source_files.select do |source| + file = "lib/ctfc/api/#{source}" + s.files += [file] + end - s.add_runtime_dependency 'kolorit', '~> 0.1.3' + s.required_ruby_version = '> 2.7', '< 3.2' + + s.add_runtime_dependency 'kolorit', '~> 0.2' s.add_runtime_dependency 'optimist', '~> 3.0.1' s.add_runtime_dependency 'rest-client', '~> 2.1.0' - s.add_development_dependency 'bundler', '~> 2.2.9' - s.add_development_dependency 'pry', '~> 0.14.1' - s.add_development_dependency 'rake', '~> 13.0.3' - s.add_development_dependency 'rspec', '~> 3.10.0' + s.add_development_dependency 'bundler', '~> 2.3' + s.add_development_dependency 'pry', '~> 0.14' + s.add_development_dependency 'rake', '~> 13.0' end diff --git a/examples/example.rb b/examples/example.rb deleted file mode 100755 index e3aaf2b..0000000 --- a/examples/example.rb +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -require_relative '../lib/ctfc' - -## -# Use default coins, get prices in RSD every 5 minutes. -# Print terminal output, append data to `.csv` table. -# -@client = Ctfc.new :rsd -loop do - @client.get - sleep 300 -end - -## -# Get prices for EUR, USD and RSD with different configuration -# -# @eur = Ctfc.new :eur, coins: %w[BTC XMR] -# @usd = Ctfc.new :usd, print: false, coins: %w[BTC XMR] -# @rsd = Ctfc.new :rsd, save: false, %w[LTC ETH] -# -# loop do -# @eur.get -# @usd.get -# @rsd.get -# sleep 300 -# end -# diff --git a/examples/example1.rb b/examples/example1.rb deleted file mode 100755 index 11013f4..0000000 --- a/examples/example1.rb +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -require_relative '../lib/ctfc' - -# define coins to scrap -Crypto::COINS = %w[BTC XMR LTC ETH].freeze - -# define class for USD [save only] -class USD - def initialize - Crypto.to :usd, save: true, print: false - end -end - -# define class for EUR [print and save] -class EUR - def initialize - Crypto.to :eur, save: true, print: true - end -end - -## -# Get crypto prices every 5 minutes. -# Print and save EUR rates, print-only USD rates. -# -loop do - USD.new - EUR.new - sleep 300 -end diff --git a/examples/example2.rb b/examples/example2.rb deleted file mode 100755 index 2c8385a..0000000 --- a/examples/example2.rb +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -require_relative '../lib/ctfc' - -## -# @note You can do this with `bin/ruby ctfc usd eur rsd`, this is just example. -# -# Make base class to extend it with class named as currency code. -# Set configuration to save '.csv' table without terminal output, -# and scrap data for Bitcoin, Monero and Ethereum. -# -class Fiat - def initialize - currency = instance_of?(Fiat) ? 'EUR' : self.class.name - - Crypto.to(currency, - save: true, - print: false, - coins: %w[BTC XMR ETH]) - end -end - -# name class as currency code -class USD < Fiat; end -class EUR < Fiat; end -class RSD < Fiat; end -class GBP < Fiat; end - -# check if arguments contain any of defined currencies -ARGV.select do |arg| - case arg - when 'usd', 'USD' then USD.new - when 'rsd', 'RSD' then RSD.new - when 'eur', 'EUR' then EUR.new - when 'gbp', 'GBP' then GBP.new - else puts "#{arg} is not supported at this time." - end -end diff --git a/lib/ctfc.rb b/lib/ctfc.rb index eae1d19..208e53e 100644 --- a/lib/ctfc.rb +++ b/lib/ctfc.rb @@ -1,49 +1,24 @@ # frozen_string_literal: true -require_relative 'ctfc/base' +require_relative 'ctfc/client' +require_relative 'ctfc/version' -## -# For easier job use Ctfc, instead of typing CTFC::Data. -# You can define default coins with Ctfc::COINS= -# -# @note For instance methods look at CTFC::Data. -# -class Ctfc < CTFC::Data - ## - # @todo Allow Ctfc to use proxy and/or tor - # - def initialize(currency = :eur, opts = {}) - opts[:coins] ||= COINS - super(currency, opts) - end - - ## - # @example Get EUR data for BTC, XMR, LTC, ETH, print but don't save output - # - # Ctfc.to :eur, save: false, coins: %w[BTC XMR LTC ETH] - # - # @param [Symbol] currency **Required**. Define fiat currency. - # @param [Hash] opts **Optional**. Additional options hash. - # - # @option opts [Boolean] print **Optional**. Print terminal output. - # @option opts [Boolean] save **Optional**. Save `.csv` output. - # @option opts [Array] coins **Optional**. Define coins to scrap. - # - # @return [Hash] CTFC::Data#prices || CTFC::Data#response - # - def self.to(currency, opts = {}) - new(currency.to_sym, opts).get - end +# Ctfc is shortcut for CTFC::Client. +# @see CTFC::Client +class Ctfc < CTFC::Client end -## -# Same as Ctfc -# @see Ctfc -# @see CTFC::Data +# Shortcut to initialize new client, +# and get prices hash. +# +# @example Get EUR prices for coins: +# coins = %w[BTC XMR LTC ETH] +# Crypto.to :eur, coins, :cryptocompare, save: true # -class Crypto < Ctfc - def initialize(currency = :eur, opts = {}) - opts[:coins] ||= COINS - super(currency, opts) +class Crypto + # @return [Hash] + # @see CTFC::Client + def self.to(*args) + CTFC::Client.new(*args).get end end diff --git a/lib/ctfc/api.rb b/lib/ctfc/api.rb new file mode 100644 index 0000000..3c2285f --- /dev/null +++ b/lib/ctfc/api.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Benchmark say it's faster to use **unless defined?** if we require file +# on multiple locations + +require_relative 'api/apitemplate' unless defined? CTFC::API::ApiTemplate +require_relative 'helpers/list' unless defined? List + +# automatically require new apis +List.sources.select { |source| require_relative "api/#{source}" } + +module CTFC + # + # Keep sources to extract data. Each source has to be a class, + # named as API domain, extending ApiTemplate. This will automatically + # make it available in Client, but also added to .gemspec. + # + # @see CTFC::API::ApiTemplate + # @see CTFC::API::Cryptocompare + # + # @example Add a new source to extract data: + # # make file new_source.rb + # class NewSource < ApiTemplate + # + # private + # + # def process + # # check response hash for persistence of fiat and coins + # super + # # write method to scrap data from NewSource + # end + # end + # + module API + end +end diff --git a/lib/ctfc/api/apitemplate.rb b/lib/ctfc/api/apitemplate.rb new file mode 100644 index 0000000..db06c81 --- /dev/null +++ b/lib/ctfc/api/apitemplate.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require 'rest-client' +require 'json' + +module CTFC + module API + # Template for other sources. Every file in api dir should extend this class. + # Automatically call method #process to send api request after initialization. + # This mean every source should include method #process, that will be executed + # after initialization. + # + # @see CTFC::API + # @see CTFC::API::Cryptocompare + # + class ApiTemplate + attr_reader :response + + # max number of requests to send + MAX_RETRY = 3 + + # Construct response hash from given arguments, and start counting requests. + # Call private method #process to extract data from web. + # + # @example Send request to cryptocompare + # crypto = Cryptocompare.new :eur, %w[BTC XMR] + # + # @param [Symbol] fiat **Required**. Fiat currency to use for conversion. + # @param [Array] coins **Required**. Array of coins to extract data. + # + # @return [Object] Source instance. + # + def initialize(fiat, coins) + @response = { fiat: fiat, coins: coins, success: false } + @counter = 0 + process + end + + # Initialize new instance, send request and return response hash. + # @example + # Cryptocompare[:eur, %w[BTC XMR]] + # + # @param [Symbol] fiat **Required**. Fiat currency. + # @param [Array] coins **Required**. Cryptocurrency coins. + # + # @return [Hash] Response hash object. + # + def self.[](fiat, coins) + new(fiat, coins).response + end + + private + + def process + return false unless response[:fiat] && response[:coins] + end + + def success!(set: true) + @counter = 0 if set == true + @response[:success] = set + end + end + end +end diff --git a/lib/ctfc/api/cryptocompare.rb b/lib/ctfc/api/cryptocompare.rb new file mode 100644 index 0000000..f49d5f5 --- /dev/null +++ b/lib/ctfc/api/cryptocompare.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require_relative 'apitemplate' unless defined? CTFC::API::ApiTemplate + +module CTFC + module API + # Source file for cryptocompare api. + # Initialize will automatically call #process, + # to send request after all attributes and variables are configured. + # + # @see CTFC::API::ApiTemplate + # + class Cryptocompare < ApiTemplate + # Cryptocompare API base url, where we add coins and fiat currency. + BASE_URL = 'https://min-api.cryptocompare.com/data/pricemultifull?' + + private + + def process + super + uri = '' + response[:coins].collect do |coin| + uri += "fsyms=#{coin}&" unless uri.include? coin + end + uri += "tsyms=#{response[:fiat]}" + @response[:uri] = BASE_URL + uri + do_rest_request + end + + def do_rest_request(time = Time.now) + rest = RestClient.get(response[:uri]) + success! if rest.code == 200 + process_json_data JSON.parse(rest), time + rescue StandardError => e + success! set: false + if (@counter += 1) > MAX_RETRY + puts e.message + else + retry + end + end + + def process_json_data(data, time) + fiat = response[:fiat] + prices = {} + response[:coins].each do |coin| + value = data['RAW'][coin.upcase][fiat.to_s.upcase]['PRICE'].round(2) + prices[coin] = value + end + @response.merge!(time: time.to_s, prices: prices, data: data) + end + end + end +end diff --git a/lib/ctfc/base.rb b/lib/ctfc/base.rb deleted file mode 100644 index 97c51da..0000000 --- a/lib/ctfc/base.rb +++ /dev/null @@ -1,204 +0,0 @@ -# frozen_string_literal: true - -require_relative 'config' -require_relative 'version' - -require 'json' -require 'csv' -require 'kolorit' -require 'rest-client' - -## -# Module **CTFC** keep everything together. **CTFC::CONFIG** module for default setup, -# and **CTFC::Data** class for actual request execution. For instance methods look -# at **CTFC::Data**, for class methods look at **Ctfc**. -# -# @see CTFC::Data -# @see Ctfc -# -module CTFC - ## - # Data class keep all the logic to send request, receive response, - # and everything between. Class Ctfc extend CTFC::Data, for easier work. - # - # @note Instead of using CTFC::Data.new, you can also call Ctfc.new - # - class Data - include CONFIG - - attr_reader :response, :data, :url, :table, :count, :prices - attr_accessor :fiat, :coins - - alias currency fiat - - ## - # @example Initialization example - # @data = CTFC::Data.new :eur, save: true - # - # @param [Symbol] currency **Optional**. Define fiat currency. - # @param [Hash] opts **Optional**. Additional options hash. - # - # @option opts [Boolean] print **Optional**. Print terminal output. - # @option opts [Boolean] save **Optional**. Save `.csv` output. - # @option opts [Array] coins **Optional**. Define coins to scrap. - # - # @return [Data] Data object to work with - # - def initialize(currency = :eur, opts = {}) - @fiat = currency.to_s.upcase - @save = opts[:save].nil? ? true : opts[:save] - @print = opts[:print].nil? ? true : opts[:print] - @coins = opts[:coins].nil? ? COINS : Array(opts[:coins]) - end - - ## - # @example Get fiat prices for initialized config - # - # @data.get - # - # @example Get prices and change initialized config "on-the-fly" - # - # @data.get :usd, save: false, coins: %w[BTC XMR ETH] - # - # @param [Symbol || String] currency **Optional**. Change fiat currency and execute request. - # @param [Hash] opts **Optional**. Options hash to change config 'on-the-fly' - see #initialize. - # - # @return [Hash || false] Hash of coins and fiat values, or false if all requests fail - # - def get(currency = nil, opts = {}) - @fiat = currency.to_s.upcase unless currency.nil? - @coins = opts[:coins] unless opts[:coins].nil? - @save = opts[:save] unless opts[:save].nil? - @print = opts[:print] unless opts[:print].nil? - @count = 0 - @table = "ctfc_#{@fiat}.csv".downcase - do_rest_request - end - - ## - # Get fiat value from response hash with crypto prices - # - # @example - # - # @data.price(:btc) - # - # @param [Symbol || String] coin **Required**. Coin name as symbol or string. - # @return [Float] - # - def price(coin) - @prices[coin.to_s.upcase] - end - - ## - # Check if crypto prices will be saved in `.csv` table - # - # @return [true || false] - # - def save? - @save == true - end - - ## - # Check if crypto prices will be printed in terminal - # - # @return [true || false] - # - def print? - @print == true - end - - ## - # Change option to save '.csv' table with prices - # - # @return [true || false] - # - def save=(opt) - @save = opt.is_a?(TrueClass) - end - - ## - # Change option to print prices in terminal - # - # @return [true || false] - # - def print=(opt) - @print = opt.is_a?(TrueClass) - end - - ## - # Check if request was successful or not. - # - # @return [true || false] - # - def success? - return false if @response.nil? - - @response.code == 200 - end - - private - - def do_rest_request - prepare_uri - process_data - @prices - rescue StandardError - if (@count += 1) >= MAX_RETRY - puts @response.to_s.split(',') - false - else - do_rest_request - end - end - - def process_data - @response = RestClient.get @url - @data = JSON.parse @response - - @data_array << Time.now.to_s - @coins.each do |coin| - value = @data['RAW'][coin.to_s.upcase][@fiat.to_s.upcase]['PRICE'].round(2) - @prices[coin] = value - @data_array << value - end - - print_fiat_values - save_csv_data - end - - def prepare_uri - @prices = {} - @data_array = [] - coin_uri = String.new '' - @coins.collect { |coin| coin_uri << "fsyms=#{coin}&" } - @url = URL + "#{coin_uri}tsyms=#{@fiat}" - end - - def print_fiat_values - return unless print? - - 30.times { print '='.cyan } - puts '' - puts "#{'['.cyan.bold}#{@fiat.to_s.upcase.yellow.bold}#{']'.cyan.bold} conversion rate" - 30.times { print '='.cyan } - puts '' - @prices.each do |name, value| - print '['.yellow.bold + name.to_s.cyan.bold + ']'.yellow.bold - puts ": #{value}".bold - end - end - - def save_csv_data - return unless save? - - create_csv_headers unless File.exist?(@table) - CSV.open(@table, 'ab') { |column| column << @data_array } - end - - def create_csv_headers - header_array = ['TIME'] - @coins.each { |coin| header_array << coin } - CSV.open(@table, 'w') { |header| header << header_array } - end - end -end diff --git a/lib/ctfc/client.rb b/lib/ctfc/client.rb new file mode 100644 index 0000000..b72a822 --- /dev/null +++ b/lib/ctfc/client.rb @@ -0,0 +1,146 @@ +# frozen_string_literal: true + +require_relative 'api' +require_relative 'export' + +module CTFC + # + # Client allow us to get data from our sources, and to + # manipulate with that data. While other classes are mostly + # used by each-other, Client is mostly used directly by user. + # + class Client + attr_reader :config, :response, :prices + + # + # Choose fiat currency, coins and source for new client. + # @example Initialize new **EUR** client + # client = CTFC::Client.new :eur, %w[BTC XMR LTC ETH] + # + # @param [Symbol] currency **Required**. Set fiat currency. + # @param [Array] coins **Required**. Set crypto coins. + # @param [Symbol] source Optional. Source for data extraction. + # @param [Hash] opts Options hash for additional configuration. + # + # @option opts [Symbol] source Set source to extract data. + # @option opts [Boolean] save Set option to save prices in csv table. + # @option opts [Boolean] export Set option to export all data in json file. + # + # @return [Client] Client instance. + # + def initialize(fiat, coins, source = nil, opts = {}) + @config = { + fiat: fiat, + coins: coins, + source: source || opts[:source], + save: [nil, true].include?(opts[:save]), + export: opts[:export].is_a?(TrueClass) + } + end + + # Scrap data from source. + # @example + # client.get :cryptocompare + # + # @param [Symbol] source Source to send api request + # @return [Hash] Hash of fiat values for scrapped coins + # + def get(source = nil) + source ||= config[:source] + send_api_request(source) + if success? + Export.to_csv(source, response) if save? + Export.to_json(source, response) if export? + end + @prices = response[:prices] + end + + # Source for data extraction. + # @return [Symbol] + # + def source + config[:source] + end + + # Set source for data extraction. + # @return [Symbol] + # + def source=(param) + @config[:source] = param + end + + # Check if csv output will be saved after request. + # @return [Boolean] + # + def save? + config[:save].is_a?(TrueClass) + end + + # Change option to save prices in csv table after request. + # @return [Boolean] + # + def save=(opt) + @config[:save] = opt.is_a?(TrueClass) + end + + # Check if json output will be exported after request. + # @return [Boolean] + # + def export? + config[:export].is_a?(TrueClass) + end + + # Change option to export all data in json file after request. + # @return [Boolean] + # + def export=(opt) + @config[:export] = opt.is_a?(TrueClass) + end + + # Get fiat value from response hash with crypto prices + # @example + # client.price(:btc) + # + # @param [Symbol] coin **Required**. Coin name as symbol. + # @return [Float] + # + def price(coin) + prices[coin.to_s.upcase] + end + + # Check if request was successful. + # + def success? + return false if response.nil? + + response[:success].is_a?(TrueClass) + end + + private + + def send_api_request(source) + # automatically add new sources to the client, but be careful with eval. + if List.sources.include? source + klass = check_source_name source + + @response = + instance_eval "CTFC::API::#{klass}[ config[:fiat], config[:coins] ]"\ + '# CTFC::API::Cryptocompare[fiat, coins]', __FILE__, __LINE__ - 1 + else + message = 'Add source to extract data' if source.nil? + message = "#{source} not included in API list" if source + raise ArgumentError, message + end + end + + # Check for underscore and capitalize each word. + # + def check_source_name(source) + if source.to_s.include? '_' + source.split('_').select(&:capitalize!).join + else + source.to_s.capitalize + end + end + end +end diff --git a/lib/ctfc/config.rb b/lib/ctfc/config.rb deleted file mode 100644 index e68188d..0000000 --- a/lib/ctfc/config.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -module CTFC - ## - # Keep default configuration data, like coins to scrap, max number - # of retries and cryptocompare API url. - # - module CONFIG - # default coins to use - COINS = %w[BTC LTC XMR ETH BCH ZEC].freeze - - # max number of retries if request fail - MAX_RETRY = 3 - - # Cryptocompare API - base url for requests - URL = 'https://min-api.cryptocompare.com/data/pricemultifull?' - end -end diff --git a/lib/ctfc/export.rb b/lib/ctfc/export.rb new file mode 100644 index 0000000..39f3003 --- /dev/null +++ b/lib/ctfc/export.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'csv' + +# Keep methods to export data as csv or json. +# JSON extract all data, while CSV only prices. +# +module Export + class << self + # + # Save crypto prices in csv table. + # + def to_csv(source, response = {}) + table = "ctfc_#{response[:fiat]}_#{source}.csv" + coins = response[:coins] + data_row = price_array_from response + create_csv_headers(table, coins) unless File.exist?(table) + CSV.open(table, 'ab') { |column| column << data_row } + end + + # Extract all data in json file. + # + def to_json(source, response = {}) + table = "ctfc_#{response[:fiat]}_#{source}.json" + File.open(table, 'ab') do |append| + append.puts JSON.pretty_generate response + end + end + + private + + def create_csv_headers(table, coins) + header_array = ['TIME'] + coins.each { |coin| header_array << coin } + CSV.open(table, 'w') { |header| header << header_array } + end + + def price_array_from(response = {}) + price_array = [response[:time]] + response[:prices].each do |_coin, price| + price_array << price + end + price_array + end + end +end diff --git a/lib/ctfc/helpers/cli.rb b/lib/ctfc/helpers/cli.rb new file mode 100644 index 0000000..46da1fe --- /dev/null +++ b/lib/ctfc/helpers/cli.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require 'kolorit' + +# Helper class to print colorized output in terminal. +# +class Cli + class << self + # + # @example Print colorized output + # Cli.print_output :eur, prices = { 'BTC' => 36985.82, 'XMR' => 151.83 } + # + # @param [Symbol] fiat **Required**. Fiat currency symbol. + # @param [Hash] prices **Required**. Prices hash. + # + def print_output(fiat, prices) + puts line + puts colorize(:bold) { "[#{fiat.upcase.yellow}#{']'.bold} conversion rate" } + puts line + prices.each { |coin, value| print_prices coin, value } + end + + # @example Set terminal colors for price hash + # Cli.colors color1: :yellow, color2: :green + # + def colors(clrs = {}) + configure if @config.nil? + @config[:color1] = clrs[:color1] if clrs[:color1] + @config[:color2] = clrs[:color2] if clrs[:color2] + @config[:line_color] = clrs[:line_color] if clrs[:line_color] + config + end + + private + + def print_prices(coin, value) + color1 = config[:color1] + color2 = config[:color2] + + coin = kolorize coin.bold, color1 + value = kolorize value, :bold + br_open = kolorize '['.bold, color2 + br_close = kolorize ']'.bold, color2 + + puts colorize(:bold) { "#{br_open}#{coin}#{br_close} => #{value}" } + end + + def config + @config ||= { color1: :yellow, color2: :cyan, line_color: :cyan } + end + alias configure config + + # helper to print line + def line + kolorize ('=' * 30).bold, config[:line_color] + end + end +end diff --git a/lib/ctfc/helpers/list.rb b/lib/ctfc/helpers/list.rb new file mode 100644 index 0000000..d595310 --- /dev/null +++ b/lib/ctfc/helpers/list.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +# Get list of sources. One method for :symbolized list, +# and one for complete filenames - to include in gemspec. +# +class List + class << self + # List sources as Array of Symbols. + # @return [Array] Array of sources as :symbols + # + def sources + @sources ||= list_files_in_api_dir + end + + # Get list of sources from files in api dir (for .gemspec). + # @return [Array] Array of sources as strings. + # + def source_files + @source_files ||= list_files_in_api_dir(gemspec: true) + end + + private + + def list_files_in_api_dir(gemspec: false) + sources = [] + skip = %w[. .. apitemplate.rb] + path = File.expand_path(__FILE__).gsub!('helpers/list.rb', 'api') + Dir.entries(path).select do |source| + next if skip.include? source + + source = source.gsub('.rb', '').to_sym unless gemspec == true + sources << source + end + sources + end + end +end diff --git a/lib/ctfc/version.rb b/lib/ctfc/version.rb index 3f89e39..7dcf69d 100644 --- a/lib/ctfc/version.rb +++ b/lib/ctfc/version.rb @@ -2,5 +2,5 @@ module CTFC # gem version - VERSION = '0.4.2' + VERSION = '1.0.0-dev' end diff --git a/spec/config_spec.rb b/spec/config_spec.rb deleted file mode 100644 index b416536..0000000 --- a/spec/config_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -require_relative './spec_helper' - -RSpec.describe CTFC::CONFIG do - context 'Configuration Constants' do - it 'has crypto coins defined before initialisation' do - expect(CTFC::CONFIG::COINS).not_to be nil - end - - it 'has api url defined before initialisation' do - expect(CTFC::CONFIG::URL).not_to be nil - end - - it 'has request max retries defined before initialisation' do - expect(CTFC::CONFIG::MAX_RETRY).not_to be nil - end - end -end diff --git a/spec/data_spec.rb b/spec/data_spec.rb deleted file mode 100644 index 4c8e671..0000000 --- a/spec/data_spec.rb +++ /dev/null @@ -1,63 +0,0 @@ -# frozen_string_literal: true - -require_relative './spec_helper' - -FIAT = :usd -CRYPTO = %w[BTC XMR ETH].freeze - -RSpec.describe CTFC::Data do - let(:crypto) { CTFC::Data.new(FIAT, coins: CRYPTO, save: nil, print: nil) } - - context 'Initialized Configuration' do - it 'sets currency as string from given symbol' do - expect(crypto.fiat).to eq FIAT.to_s.upcase - expect(crypto.fiat.class).to be String - end - - it 'sets passed coins with precedence over configured' do - expect(crypto.coins).not_to be nil - expect(crypto.coins.class).to be Array - expect(crypto.coins).not_to eq CTFC::Data::COINS - end - - it 'save csv table unless otherwise defined' do - expect(crypto.save?).not_to be nil - expect(crypto.save?).to be true - end - - it 'print terminal output unless otherwise defined' do - expect(crypto.print?).not_to be nil - expect(crypto.print?).to be true - end - end -end - -RSpec.describe CTFC::Data do - let(:crypto) { CTFC::Data.new(FIAT, coins: @coins, save: nil, print: nil) } - - context 'Change configuration on-the-fly' do - it 'sets options passed to #get with precedence over initialized ones' do - initialized_currency = crypto.fiat - # execute request - crypto.get(:eur, print: false, save: false) - # test results - expect(crypto.fiat).not_to eq initialized_currency - expect(crypto.fiat.class).to be String - expect(crypto.fiat).to eq 'EUR' - expect(crypto.save?).to be false - expect(crypto.print?).to be false - end - - it 'allow setter method for option save' do - expect(crypto.save?).to be true - crypto.save = false - expect(crypto.save?).to be false - end - - it 'allow setter method for option print' do - expect(crypto.print?).to be true - crypto.print = false - expect(crypto.print?).to be false - end - end -end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb deleted file mode 100644 index 5822de1..0000000 --- a/spec/spec_helper.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -require 'rspec' -require_relative '../lib/ctfc' - -RSpec.configure do |config| - config.example_status_persistence_file_path = '.rspec_status' - - config.expect_with :rspec do |c| - c.syntax = :expect - end -end