Managing several Ruby versions and Gem collections on Windows with Pik

April 12th, 2011

If you have several versions and releases of Ruby (e.g. 187-p330, –p334, 1.9.2…) each with their own set of collections of gems for a specific app, then Pik becomes a bit cumbersome to select and manage your Ruby versions – there’s no command to “rename” a Ruby version to a for you memorable name (such as the name for the application linked to the ruby install).

  $ pik list
  
  186: ruby 1.8.6 (2008-08-11 patchlevel 287) [i386-mswin32]
  187-ref-r187-p330: ruby 1.8.7 (2010-12-23 patchlevel 330) [i386-mingw32]
* 187: ruby 1.8.7 (2010-12-23 patchlevel 330) [i386-mingw32]
  187: ruby 1.8.7 (2011-02-18 patchlevel 334) [i386-mingw32]
  192-192-rubies-refinery: ruby 1.9.2p180 (2011-02-18) [i386-mingw32]
  192: ruby 1.9.2p180 (2011-02-18) [i386-mingw32]

Now there’s also a way to do that manually: just edit the config.yml file in your C:\Users\Pascal\.pik\ folder:

--- 
"186: ruby 1.8.6 (2008-08-11 patchlevel 287) [i386-mswin32]": 
  :path: !ruby/object:Pathname 
    path: C:/Ruby/bin
"187-ref-r187-p330: ruby 1.8.7 (2010-12-23 patchlevel 330) [i386-mingw32]": 
  :path: !ruby/object:Pathname 
    path: C:/Users/Pascal/.pik/rubies/Ruby-187-p330/bin
"187: ruby 1.8.7 (2010-12-23 patchlevel 330) [i386-mingw32]": 
  :path: !ruby/object:Pathname 
    path: C:/ruby187/bin
"187: ruby 1.8.7 (2011-02-18 patchlevel 334) [i386-mingw32]": 
  :path: !ruby/object:Pathname 
    path: C:/Users/Pascal/.pik/rubies/Ruby-187-p334/bin
"192-192-rubies-refinery: ruby 1.9.2p180 (2011-02-18) [i386-mingw32]": 
  :path: !ruby/object:Pathname 
    path: C:/Users/Pascal/.pik/rubies/Ruby-192-p180/bin
"192: ruby 1.9.2p180 (2011-02-18) [i386-mingw32]": 
  :path: !ruby/object:Pathname 
    path: C:/Ruby192/bin
--- {}


Becomes (by changing the “logical names” before the colon to something more meaningful):

--- 
"Default-186: ruby 1.8.6 (2008-08-11 patchlevel 287) [i386-mswin32]": 
  :path: !ruby/object:Pathname 
    path: C:/Ruby/bin
"RefineryGemset-187-p330: ruby 1.8.7 (2010-12-23 patchlevel 330) [i386-mingw32]": 
  :path: !ruby/object:Pathname 
    path: C:/Users/Pascal/.pik/rubies/Ruby-187-p330/bin
"Default-187: ruby 1.8.7 (2010-12-23 patchlevel 330) [i386-mingw32]": 
  :path: !ruby/object:Pathname 
    path: C:/ruby187/bin
"Clean-187: ruby 1.8.7 (2011-02-18 patchlevel 334) [i386-mingw32]": 
  :path: !ruby/object:Pathname 
    path: C:/Users/Pascal/.pik/rubies/Ruby-187-p334/bin
"RefineryGemset-192-p189: ruby 1.9.2p180 (2011-02-18) [i386-mingw32]": 
  :path: !ruby/object:Pathname 
    path: C:/Users/Pascal/.pik/rubies/Ruby-192-p180/bin
"Default-192: ruby 1.9.2p180 (2011-02-18) [i386-mingw32]": 
  :path: !ruby/object:Pathname 
    path: C:/Ruby192/bin
--- {}

And now listing and selecting (with Pik use…) the right version is a lot easier:

  $ pik list
  Clean-187: ruby 1.8.7 (2011-02-18 patchlevel 334) [i386-mingw32]
  Default-186: ruby 1.8.6 (2008-08-11 patchlevel 287) [i386-mswin32]
* Default-187: ruby 1.8.7 (2010-12-23 patchlevel 330) [i386-mingw32]
  Default-192: ruby 1.9.2p180 (2011-02-18) [i386-mingw32]
  RefineryGemset-187-p330: ruby 1.8.7 (2010-12-23 patchlevel 330) [i386-mingw32]
  RefineryGemset-192-p189: ruby 1.9.2p180 (2011-02-18) [i386-mingw32]

What you can also do now, is manually copying a base Ruby+gems collections when you start a new app, e.g. copy C:\Users\Pascal\.pik\rubies\Ruby-187-p334 to C:\Users\Pascal\.pik\rubies\Ruby-187-p334-Refinery and add in the config.yml:

"Newapp-187: ruby 1.8.7 (2011-02-18 patchlevel 334) [i386-mingw32]": 
  :path: !ruby/object:Pathname 
    path: C:/Users/Pascal/.pik/rubies/Ruby-187-p334-Refinery/bin

BTW: don’t forget to also add the path to the new Ruby version to your Devkit/config.yml file and to run ruby dk.rb install if you want to use native extensions.

“Pik install” command downloads and extracts a Ruby version, but: “Couldn’t find a Ruby version at <path>”

April 12th, 2011

I had this issue several times up to the point I was unable to install any Ruby version.  Then I went looking in the

C:\Users\Pascal\.pik\downloads

directory and found several .7x files with file size = 0 from failed previous installs.

Delete those, and you’re able to install the corresponding Ruby versions again…

sh.exe “command not found” for an installed gem (while using pik and msysgit on Windows)

March 16th, 2011

Just for the people googling this problem…  If you have installed a gem and the promised command does not work (in my case: “annotate” after installing the Annotate gem), try:

bundle exec annotate

That way, you force a lookup in the bundled gems rather than the system gems.

Using Rails for Batch processes (example: Moneybird export via API and command-line)

March 2nd, 2011

My accountant needs to have my financial data in a somewhat slightly different format than the export my online invoicing application provides. Luckily Moneybird has an api so I set out to write my alternative export.  I googled around for some best practices, and this is the workflow I came up with:

Create a new Rails app

> rails new moneybirdexport 
> cd moneybirdexport


Generate models for api access and conversion logic

Our goal is a list of incoming and outgoing invoices – so we generate models with the required fields.

> rails generate model dumped_invoice klant:string, factuurdatum:date, periode:string, factuurnummer:string, bedrag_excl_btw:decimal, btw_percentage:decimal, bedrag_incl_btw:decimal, omschrijving:text, soort:string, betaald:string, datum:date, betaalwijze:string 
> rails generate model dumped_incoming_invoice factuurdatum:date, periode:string, factuurnummer:string, bedrag_excl_btw:decimal, btw_bedrag:decimal, bedrag_incl_btw:decimal, omschrijving:text, soort:string, betaald:string, datum:date, betaalwijze:string 
> rake db:migrate

The Moneybird API exposes invoice, incoming_invoice and contact. No need to use a generator here, create the app/models/ files contact.rb, incoming_invoice.rb, and invoice.rb, to create the ActiveResources Contact, IncomingInvoice and Invoice – the source is identical apart from the classnames:

class Invoice < ActiveResource::Base
  self.site         = "https://" + Moneybirdexport::MBX_ACCOUNT_NAME + ".moneybird.com"
  self.user         = Moneybirdexport::MBX_USER_NAME 
  self.password     = Moneybirdexport::MBX_PASSWORD 
  self.timeout = 5
end

Not sure what the best practice is here, but I put per-app configurable values in config/application.rb, so if you use the constant values in the same way I did in the previous lines, you need to define them there as well:

module Moneybirdexport
  class Application < Rails::Application
    # Settings in config/environments/* take precedence over those specified here.
    # ...
  end
    
  MBX_ACCOUNT_NAME  = "username"
  MBX_USER_NAME     = "someemailaddress@domain.com"
  MBX_PASSWORD      = "secret"
end

You should be able to test the api already on the command-line (YAML::dump gives a well-formatted output of objects):

> rails console
irb(main):> puts YAML::dump Invoice.first
irb(main):> puts YAML::dump IncomingInvoice.first
irb(main):> puts YAML::dump Contact.first

Write the conversion logic

Some fields I need for my export (dumped_invoices and dumped_incoming_invoices) are not attributes of Invoices and IncomingInvoices as Moneybird sees them.  So in the models DumpedInvoice and DumpedIncomingInvoice there’s a method set_attributes_from_invoice that digs out the info from somewhat deeper in the Moneybird model (looks up the related contact name, or fabricates a description from the invoice line items’ description…).  Exactly how this is done is not important, I’m just copy-pasting my code here if you need to do something similar with the Moneybird API:

class DumpedInvoice < ActiveRecord::Base

  # need to include ActionView::helper because I am using the truncate method
  # source: http://stackoverflow.com/questions/489641/using-helpers-in-model-how-do-i-include-helper-dependencies
  include ActionView::Helpers

  def set_attributes_from_invoice(i) # i should be an Invoice ActiveResource
    self.klant            = i.company_name
    self.factuurdatum     = i.invoice_date
    self.periode          = ((i.invoice_date.month + 2)/3).to_s + "e kw" 
    self.factuurnummer    = i.invoice_id
    self.bedrag_excl_btw  = i.total_price_excl_tax
    self.bedrag_incl_btw  = i.total_price_incl_tax 
    
    self.btw_percentage   = 0  if ( self.bedrag_excl_btw == 0) 
    self.btw_percentage   = ((self.bedrag_incl_btw - self.bedrag_excl_btw + 0.001)/ self.bedrag_excl_btw * 100).to_i if ( self.bedrag_excl_btw >0) 
    
    # concatenate the detail description and remove tabs, linebreaks with squish
    concat_details_descr  = i.details.map(&:description).join(" - ").squish
    self.omschrijving     = truncate(sanitize(concat_details_descr), :length=> 100)
    
    self.betaald          = "Ja" # shortcut - all of them paid by now...
    self.datum            = i.payments.last.payment_date # shortcut - everything was just one payment...
    self.betaalwijze      = i.payments.last.payment_method
    return self
  end
  
end

class DumpedIncomingInvoice < ActiveRecord::Base
  
  # need to include ActionView::helper because I am using the truncate method
  # source: http://stackoverflow.com/questions/489641/using-helpers-in-model-how-do-i-include-helper-dependencies
  include ActionView::Helpers

  def set_attributes_from_invoice(i) # i should be an IncomingInvoice ActiveResource
    self.factuurdatum     = i.invoice_date
    self.periode          = ((i.invoice_date.month + 2)/3).to_s + "e kw" 
    self.factuurnummer    = i.invoice_id
    self.bedrag_excl_btw  = i.price_incl_tax - i.price_tax
    self.btw_bedrag       = i.price_tax 
    self.bedrag_incl_btw  = i.price_incl_tax
 
    invoice_company       = Contact.find(i.contact_id)
    self.omschrijving     = invoice_company.company_name
    
    self.betaald          = "Ja" # shortcut - ze zijn idd allemaal betaald...
    self.datum            = i.payments.last.payment_date # shortcut - everything was just one payment...
    self.betaalwijze      = i.payments.last.payment_method
    return self
  end

end

(If this was actual code for an app, I would of course factor out the duplication… 😉 )

Define the actual Batch script as a Rake task

There are several ways to load the Rails libraries, but using a Rake task is probably the best solution and there’s an excellent railscast on Rake. I use the FasterCSV library here because I couldn’t figure out quickly how to use tabs instead of commas as separators with the built-in CSV library.  Don’t forget to include gem "fastercsv" in your Gemfile and run bundle install if you do as well.  I followed the Railscast tutorial step by step to come up with a lib/tasks/export.rake that accepts an optional year parameter so you can run:

rake export:invoices
rake export:incoming_invoices year=2011
rake export:all

namespace :export do

  # Note: you need to add :environment as dependency so the rake "environment" 
  # task is called that loads the enviromennt

  desc "exports invoices"
  task :invoices => :environment  do      
    
    param_year = ENV["year"].to_i
    param_year = 2010 if param_year == 0
    
    # make sure you have included "gem 'fastercsv'" in your Gemfile and run "bundle install"
    # needs to be tab-separated because data contains commas...
    FasterCSV.open("dumped_invoices.csv", 'wb', :col_sep => "\t") do |csv|
      # write the columns headers
      csv << DumpedInvoice.new.attributes.keys      
      #iterate over invoices
      Invoice.all.select{|i| i.invoice_date.year == param_year}.sort_by(&:invoice_date).each do |i|
        csv <<  DumpedInvoice.new.set_attributes_from_invoice(i).attributes.values
      end
    
    end 
  end
  
  desc "exports incoming invoices"
  task :incoming_invoices => :environment  do      
    
    param_year = ENV["year"].to_i
    param_year = 2010 if param_year == 0
    
    # make sure you have included "gem 'fastercsv'" in your Gemfile and run "bundle install"
    # needs to be tab-separated because data contains commas...
    FasterCSV.open("dumped_incoming_invoices.csv", 'wb', :col_sep => "\t") do |csv|
      # write the columns headers
      csv << DumpedIncomingInvoice.new.attributes.keys      
      #iterate over invoices
      IncomingInvoice.all.select{|i| i.invoice_date.year == param_year}.sort_by(&:invoice_date).each do |i|
        csv <<  DumpedIncomingInvoice.new.set_attributes_from_invoice(i).attributes.values
      end    
    end 
  end
  
  task :all => [:invoices, :incoming_invoices]
  
end

That’s it!  My first blogpost since I started studying Ruby and Rails again after some dabbling with it back in 2006!

Getting an object’s methods without the inherited methods

November 7th, 2006

test = SomeObject.new
test.methods - test.class.superclass.new.methods

Or is there a shorter, less convoluted way?

Update November 12th: yes there seems to be…

puts String.instance_methods(false)

From the Ruby For Rails Book, page 254, paragraph 9.8.1….

But: String.instance_methods(false).size = 83 , whereas (test.methods – test.class.superclass.new.methods).size = 102 (with test a string object).

>> puts (test.methods - test.class.superclass.new.methods - String.instance_methods(false)).sort
<
<=
>
>=
all?
any?
between?
collect
detect
each_with_index
entries
find
find_all
grep
inject
map
max
member?
min
partition
reject
select
sort
sort_by
zip
=> nil
>> exit

The difference seems to be the methods of the mixed in modules…

Installing a Gem from a local file

October 8th, 2006

What if you want to install a Gem that you did not find on Rubyforge – one that was mailed to you or that you have downloaded?

Simple: just add the downloaded file name to the gem install command, instead of the name of the gem. For example, installing the “simple-rss” extension would be:

  • with the path included if the file is not in the directory where your command line is:
    gem install D:\downloads\ruby_gems\simple-rss-1.1.gem
  • or without, if you change directory in your command line to where the file is
    gem install simple-rss-1.1.gem

Instead of just:
gem install simple-rss

This might seem evident to most of you, but it took me a while to figure out as rubyforge.org was down today and I had to rely on this mirror.