stalkr 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile ADDED
@@ -0,0 +1,35 @@
1
+
2
+ begin
3
+ require 'rubygems'
4
+ require 'jeweler'
5
+ Jeweler::Tasks.new do |gemspec|
6
+ gemspec.name = "stalkr"
7
+ gemspec.summary = "Ruby library for tracking packages"
8
+ gemspec.description = "Ruby library for tracking UPS, Fedex and USPS packages."
9
+ gemspec.email = "chetan@pixelcop.net"
10
+ gemspec.homepage = "http://github.com/chetan/stalkr"
11
+ gemspec.authors = ["Chetan Sarva"]
12
+ gemspec.add_dependency('scrapi', '= 1.2.0')
13
+ gemspec.add_dependency('tzinfo', '>= 0.3.15')
14
+ gemspec.add_dependency('json', '>= 1.4.6')
15
+ gemspec.add_dependency('tidy', '>= 1.1.2')
16
+ gemspec.add_dependency('htmlentities', '>= 4.2.2')
17
+ end
18
+ Jeweler::GemcutterTasks.new
19
+ rescue LoadError
20
+ puts "Jeweler not available. Install it with: sudo gem install jeweler"
21
+ end
22
+
23
+ require "rake/testtask"
24
+ desc "Run unit tests"
25
+ Rake::TestTask.new("test") { |t|
26
+ #t.libs << "test"
27
+ t.ruby_opts << "-rubygems"
28
+ t.pattern = "test/**/*_test.rb"
29
+ t.verbose = false
30
+ t.warning = false
31
+ }
32
+
33
+ require "yard"
34
+ YARD::Rake::YardocTask.new("docs") do |t|
35
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.9.0
data/lib/stalkr.rb ADDED
@@ -0,0 +1,50 @@
1
+
2
+ if RUBY_PLATFORM =~ /darwin/ then
3
+ # fix for scrapi on Mac OS X
4
+ require "tidy"
5
+ Tidy.path = "/usr/lib/libtidy.dylib"
6
+ end
7
+
8
+ require 'scrapi'
9
+ require 'htmlentities'
10
+ require 'date'
11
+
12
+ require 'stalkr/base'
13
+ require 'stalkr/result'
14
+ require 'stalkr/ups'
15
+ require 'stalkr/usps'
16
+ require 'stalkr/fedex'
17
+
18
+ if not DateTime.new.public_methods.include? "to_time" then
19
+ # monkey patch DateTime to add to_time (exists in Ruby 1.9.2 and above)
20
+ class DateTime
21
+ def to_time
22
+ d = new_offset(0)
23
+ t = d.instance_eval do
24
+ Time.utc(year, mon, mday, hour, min, sec +
25
+ sec_fraction)
26
+ end.getlocal
27
+ end
28
+ end
29
+ end
30
+
31
+ module Stalkr
32
+
33
+ def self.shippers
34
+ return [ Stalkr::UPS, Stalkr::USPS, Stalkr::FEDEX ]
35
+ end
36
+
37
+ def self.track(id)
38
+ shipper = nil
39
+ if id =~ /\d{22}/ then
40
+ shipper = Stalkr::USPS
41
+ elsif id =~ /^1Z/ then
42
+ shipper = Stalkr::UPS
43
+ elsif id =~ /\d{20}/ or id =~ /\d{15}/ then
44
+ shipper = Stalkr::FEDEX
45
+ end
46
+ raise 'Unknown shipper code' if shipper.nil?
47
+ return shipper.new.track(id)
48
+ end
49
+
50
+ end # module Stalkr
@@ -0,0 +1,47 @@
1
+
2
+ module Stalkr
3
+
4
+ class Base
5
+
6
+ class << self
7
+ attr_accessor :regex
8
+ end
9
+
10
+ def fetchurl(url)
11
+ return Net::HTTP.get_response(URI.parse(URI.escape(url))).body
12
+ end
13
+
14
+ def strip_tags(html)
15
+
16
+ HTMLEntities.new.decode(
17
+ html.gsub(/<.+?>/,'').
18
+ gsub(/<br *\/>/m, '')
19
+ )
20
+
21
+ end
22
+
23
+ def cleanup_html(str)
24
+ str.gsub!(/&nbsp;/, ' ')
25
+ str = strip_tags(str)
26
+ str.strip!
27
+ str.squeeze!(" \n\r")
28
+ return str
29
+ end
30
+
31
+ def self.is_valid?(id)
32
+ return (id =~ regex() ? true : false)
33
+ end
34
+
35
+ def self.extract_id(str)
36
+ if str =~ regex() then
37
+ # return the first non-nil backreference
38
+ m = $~.to_a
39
+ m.shift
40
+ return m.find{ |s| not s.nil? }.gsub(/ /, '').upcase
41
+ end
42
+ return nil
43
+ end
44
+
45
+ end # class Base
46
+
47
+ end # module Stalkr
@@ -0,0 +1,84 @@
1
+
2
+ require 'json'
3
+ require 'yaml'
4
+
5
+ module Stalkr
6
+
7
+ class FEDEX < Base
8
+
9
+ self.regex = /\b((96\d\d\d\d\d ?\d\d\d\d|96\d\d) ?\d\d\d\d ?d\d\d\d( ?\d\d\d)?)|(\d{12}|\d{15})\b/i
10
+
11
+ def track(id)
12
+
13
+ url = "http://www.fedex.com/Tracking?tracknumbers=%CODE%"
14
+
15
+ url.gsub!(/%CODE%/, id)
16
+ html = fetchurl(url)
17
+
18
+ if html =~ /invalid._header/ then
19
+ ret = Result.new(:FEDEX)
20
+ ret.status = UNKNOWN
21
+ return ret
22
+ end
23
+
24
+ info_scraper = Scraper.define do
25
+
26
+ array :dates
27
+
28
+ process "div.detailshipmentstatus", :status => :text
29
+ process "div.detailshipdates > div.fielddata", :dates => :text
30
+ process "div.detaildestination > div.fielddata", :destination => :text
31
+ process_first "div.detailshipfacts div.dataentry", :service_type => :text
32
+
33
+ result :status, :dates, :destination, :service_type
34
+
35
+ end
36
+
37
+ ret = Result.new(:FEDEX)
38
+
39
+ info = info_scraper.scrape(html)
40
+
41
+ if info.status.strip.downcase == "delivered" then
42
+ ret.status = DELIVERED
43
+ end
44
+ ret.location = info.destination.strip
45
+
46
+ # obj.service_type = info.service_type.strip
47
+
48
+ # try to get dates
49
+ shipped_date = info.dates[0].strip
50
+ delivery_date = info.dates[1].strip if info.dates.length == 2
51
+ if shipped_date.empty? then
52
+ shipped_date = DateTime.strptime( $1, "%b %d, %Y" ).to_time if html =~ /var shipDate = "(.*?);$"/
53
+ delivery_date = DateTime.strptime( "#{$1} -5", "%b %d, %Y %I:%M %p %z" ).to_time if html =~ /var deliveryDateTime = "(.*?)";$/
54
+ end
55
+ ret.delivered_at = delivery_date
56
+ ret.updated_at = delivery_date
57
+
58
+ # pull progress from JSON
59
+ # if html =~ /^var detailInfoObject = (.*);$/ then
60
+ # json = $1
61
+ # progress = JSON.parse(json)
62
+ # scans = progress['scans']
63
+ #
64
+ # progress = []
65
+ #
66
+ # scans.each { |scan|
67
+ # o = OpenStruct.new
68
+ # o.location = scan['scanLocation']
69
+ # o.date = scan['scanDate']
70
+ # o.local_time = scan['scanTime']
71
+ # o.desc = scan['scanStatus']
72
+ # progress << o
73
+ # }
74
+ #
75
+ # obj.progress = progress
76
+ #
77
+ # end
78
+
79
+ return ret
80
+
81
+ end
82
+
83
+ end # class FEDEX
84
+ end # module Stalkr
@@ -0,0 +1,18 @@
1
+
2
+ module Stalkr
3
+
4
+ UNKNOWN = :unknown
5
+ DELIVERED = :delivered
6
+ IN_TRANSIT = :in_transit
7
+
8
+ class Result
9
+
10
+ attr_accessor :shipper, :status, :updated_at, :delivered_at, :location
11
+
12
+ def initialize(shipper)
13
+ @shipper = shipper
14
+ end
15
+
16
+ end # class Result
17
+
18
+ end # module Stalkr
data/lib/stalkr/ups.rb ADDED
@@ -0,0 +1,79 @@
1
+
2
+ class Array
3
+
4
+ def to_perly_hash()
5
+ h = {}
6
+ self.each_index { |i|
7
+ next if i % 2 != 0
8
+ h[ self[i] ] = self[i+1]
9
+ }
10
+ return h
11
+ end
12
+
13
+ end
14
+
15
+ module Stalkr
16
+
17
+ class UPS < Base
18
+
19
+ self.regex = /\b(1Z ?[0-9A-Z]{3} ?[0-9A-Z]{3} ?[0-9A-Z]{2} ?[0-9A-Z]{4} ?[0-9A-Z]{3} ?[0-9A-Z]|[\dT]\d{3} ?\d{4} ?\d{3})\b/i
20
+
21
+ def track(id)
22
+
23
+ url = "http://wwwapps.ups.com/WebTracking/processInputRequest?TypeOfInquiryNumber=T&loc=en_US&InquiryNumber1=%CODE%"
24
+
25
+ url.gsub!(/%CODE%/, id)
26
+ html = fetchurl(url)
27
+
28
+ detail_scraper = Scraper.define do
29
+
30
+ array :keys
31
+ array :vals
32
+ array :lists
33
+
34
+ process "#trkNum", :trackingNumber => :text
35
+ process "#tt_spStatus", :status => :text
36
+ process "#fontControl dt", :keys => :text
37
+ process "#fontControl dd", :vals => :text
38
+ process "#fontControl ul.clearfix li", :lists => :text
39
+
40
+ result :keys, :vals, :trackingNumber, :status, :lists
41
+
42
+ end
43
+
44
+ details = detail_scraper.scrape(html)
45
+
46
+ if not details.trackingNumber then
47
+ raise "UPS scraper failed"
48
+ end
49
+
50
+ ret = Result.new(:UPS)
51
+
52
+ ret.status = case details.status.strip.downcase
53
+ when "in transit"
54
+ IN_TRANSIT
55
+ when "delivered"
56
+ DELIVERED
57
+ else
58
+ UNKNOWN
59
+ end
60
+
61
+ hash = {}
62
+ details.keys.each_with_index do |k,i|
63
+ hash[k] = details.vals[i]
64
+ end
65
+
66
+ if ret.status == DELIVERED then
67
+ delivered_at = cleanup_html( hash["Delivered On:"] )
68
+ ret.delivered_at = DateTime.strptime( delivered_at, "%A, %m/%d/%Y at %I:%M %p" ).to_time
69
+ end
70
+
71
+ cleanup_html( details.lists[3] ) =~ /Updated: (.*?)$/
72
+ ret.updated_at = DateTime.strptime( $1, "%m/%d/%Y %I:%M %p" ).to_time
73
+
74
+ return ret
75
+
76
+ end
77
+
78
+ end # class UPS
79
+ end # module Stalkr
@@ -0,0 +1,78 @@
1
+
2
+ module Stalkr
3
+
4
+ class USPS < Base
5
+
6
+ # 20 or 22 digits, beginning with 91 (and with or without spaces between groupings)
7
+ self.regex = /\b(91\d{2} ?\d{4} ?\d{4} ?\d{4} ?\d{4} ?\d{2}|91\d{2} ?\d{4} ?\d{4} ?\d{4} ?\d{4})\b/i
8
+
9
+ def track(id)
10
+
11
+ # cleanup id
12
+ id.gsub!(/ /, '')
13
+
14
+ url = "http://trkcnfrm1.smi.usps.com/PTSInternetWeb/InterLabelInquiry.do?origTrackNum=%CODE%"
15
+
16
+ url.gsub!(/%CODE%/, id)
17
+ html = fetchurl(url)
18
+
19
+ info_scraper = Scraper.define do
20
+
21
+ array :details
22
+ array :info
23
+ array :txt
24
+
25
+ process "span.mainTextbold", :info => :text
26
+ process "td.mainTextbold", :details => :text
27
+ process "td.mainText", :txt => :text
28
+
29
+ result :details, :info, :txt
30
+
31
+ end
32
+
33
+ scrape = info_scraper.scrape(html)
34
+
35
+ # verify its the correct response page
36
+ if scrape.info[0].gsub(/ /, '') != id then
37
+ raise "USPS scraper failed"
38
+ end
39
+
40
+ # extract and return
41
+ ret = Result.new(:USPS)
42
+ if scrape.txt.find{ |t| t =~ /There is no record of this item/ } then
43
+ ret.status = UNKNOWN
44
+ elsif scrape.info.find{ |i| i.downcase == "delivered" } then
45
+ ret.status = DELIVERED
46
+ else
47
+ ret.status = UNKNOWN
48
+ end
49
+
50
+ if scrape.details and not scrape.details.empty? then
51
+ if scrape.details[0] =~ /^(.*?), (.*? \d+, \d+), (\d+:\d+ .m), (.*?)$/ then
52
+ ret.location = $4
53
+ # not sure if this time is always in EST or the time of the area in which it was delivered?
54
+ ret.updated_at = DateTime.strptime( "#{$2} #{$3} -0500", "%B %d, %Y %I:%M %p %z" ).to_time
55
+ if ret.status == DELIVERED then
56
+ ret.delivered_at = ret.updated_at
57
+ end
58
+
59
+ elsif scrape.details[0] =~ /Electronic Shipping Info Received/ then
60
+ ret.status = IN_TRANSIT
61
+ end
62
+
63
+ elsif s = scrape.txt.find{ |t| t =~ /files offline/ }
64
+ s =~ /at (\d+:\d+ .m) on (.*? \d+, \d+) in (.*?)\.$/
65
+ ret.location = $3
66
+ # not sure if this time is always in EST or the time of the area in which it was delivered?
67
+ ret.updated_at = DateTime.strptime( "#{$2} #{$1} -0500", "%B %d, %Y %I:%M %p %z" ).to_time
68
+ if ret.status == DELIVERED then
69
+ ret.delivered_at = ret.updated_at
70
+ end
71
+
72
+ end
73
+
74
+ return ret
75
+ end
76
+
77
+ end # class USPS
78
+ end # module Stalkr
@@ -0,0 +1,26 @@
1
+ require File.dirname(__FILE__) + '/../test_helper.rb'
2
+
3
+ class FEDEX_Test < Test::Unit::TestCase
4
+
5
+ def test_track
6
+ id = "106050761498748"
7
+ info = Stalkr::FEDEX.new.track(id)
8
+ assert(info.shipper == :FEDEX)
9
+ assert(info.status == Stalkr::DELIVERED)
10
+ assert(info.delivered_at.kind_of? Time)
11
+ end
12
+
13
+ def test_track_bad_code
14
+
15
+ id = "foobar"
16
+ info = Stalkr::FEDEX.new.track(id)
17
+ assert(info.status == Stalkr::UNKNOWN)
18
+
19
+ end
20
+
21
+ def test_extract_id
22
+ str = "asdf 106050761498748 asdfas"
23
+ assert(Stalkr::FEDEX.extract_id(str) == "106050761498748")
24
+ end
25
+
26
+ end
@@ -0,0 +1,44 @@
1
+ require File.dirname(__FILE__) + '/../test_helper.rb'
2
+
3
+ class UPS_Test < Test::Unit::TestCase
4
+
5
+ def test_track
6
+ id = "1ZX799470341200708"
7
+ info = Stalkr::UPS.new.track(id)
8
+ assert(info.shipper == :UPS)
9
+ assert(info.status == Stalkr::DELIVERED)
10
+ assert(info.delivered_at.kind_of? Time)
11
+ end
12
+
13
+ def test_track_bad_code
14
+
15
+ begin
16
+ id = "foobar"
17
+ info = Stalkr::UPS.new.track(id)
18
+ flunk("no exception was thrown")
19
+ rescue => ex
20
+ if ex.message != "UPS scraper failed" then
21
+ flunk("wrong exception was thrown")
22
+ end
23
+ end
24
+
25
+ end
26
+
27
+ def test_valid_id
28
+ id = "1ZX799470341200708"
29
+ ret = Stalkr::UPS.is_valid?(id)
30
+ assert(ret)
31
+ end
32
+
33
+ def test_invalid_id
34
+ id = "1X799470341200708"
35
+ ret = Stalkr::UPS.is_valid?(id)
36
+ assert(!ret)
37
+ end
38
+
39
+ def test_extract_id
40
+ str = "asdf 1ZX799470341200708 asdfas"
41
+ assert(Stalkr::UPS.extract_id(str) == "1ZX799470341200708")
42
+ end
43
+
44
+ end
@@ -0,0 +1,33 @@
1
+ require File.dirname(__FILE__) + '/../test_helper.rb'
2
+
3
+ class USPS_Test < Test::Unit::TestCase
4
+
5
+ def test_track
6
+ id = "9102901001299192824023"
7
+ info = Stalkr::USPS.new.track(id)
8
+ assert(info.shipper == :USPS)
9
+ assert(info.status == Stalkr::DELIVERED)
10
+ assert(info.delivered_at.kind_of? Time)
11
+ end
12
+
13
+ def test_older_pkg
14
+ id = "9102927003525018504915"
15
+ info = Stalkr::USPS.new.track(id)
16
+ assert(info.shipper == :USPS)
17
+ assert(info.status == Stalkr::DELIVERED)
18
+ assert(info.delivered_at.kind_of? Time)
19
+ end
20
+
21
+ def test_track_bad_code
22
+ id = "9102901001312014825845"
23
+ info = Stalkr::USPS.new.track(id)
24
+ assert(info.shipper == :USPS)
25
+ assert(info.status == Stalkr::UNKNOWN)
26
+ end
27
+
28
+ def test_extract_id
29
+ str = "asdf 9102901001299192824023 asdfas"
30
+ assert(Stalkr::USPS.extract_id(str) == "9102901001299192824023")
31
+ end
32
+
33
+ end
@@ -0,0 +1,23 @@
1
+ require File.dirname(__FILE__) + '/test_helper.rb'
2
+
3
+ class Stalkr_Test < Test::Unit::TestCase
4
+
5
+ # useless test is useless :)
6
+ def test_autoload_classes
7
+ assert Stalkr::UPS
8
+ assert Stalkr::USPS
9
+ assert Stalkr::FEDEX
10
+ end
11
+
12
+ def test_unknown_shipper_code
13
+ begin
14
+ Stalkr.track("foobar")
15
+ flunk("no exception was thrown")
16
+ rescue => ex
17
+ if ex.message != "Unknown shipper code" then
18
+ flunk("wrong exception was thrown")
19
+ end
20
+ end
21
+ end
22
+
23
+ end
@@ -0,0 +1,10 @@
1
+
2
+ require "rubygems"
3
+
4
+ if not ENV.include? "TM_MODE" then
5
+ begin; require "turn"; rescue LoadError; end
6
+ end
7
+
8
+ require 'stringio'
9
+ require 'test/unit'
10
+ require File.dirname(__FILE__) + '/../lib/stalkr'
metadata ADDED
@@ -0,0 +1,162 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: stalkr
3
+ version: !ruby/object:Gem::Version
4
+ hash: 59
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 9
9
+ - 0
10
+ version: 0.9.0
11
+ platform: ruby
12
+ authors:
13
+ - Chetan Sarva
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-02-02 00:00:00 -05:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: scrapi
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - "="
28
+ - !ruby/object:Gem::Version
29
+ hash: 31
30
+ segments:
31
+ - 1
32
+ - 2
33
+ - 0
34
+ version: 1.2.0
35
+ type: :runtime
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: tzinfo
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ hash: 13
46
+ segments:
47
+ - 0
48
+ - 3
49
+ - 15
50
+ version: 0.3.15
51
+ type: :runtime
52
+ version_requirements: *id002
53
+ - !ruby/object:Gem::Dependency
54
+ name: json
55
+ prerelease: false
56
+ requirement: &id003 !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ hash: 11
62
+ segments:
63
+ - 1
64
+ - 4
65
+ - 6
66
+ version: 1.4.6
67
+ type: :runtime
68
+ version_requirements: *id003
69
+ - !ruby/object:Gem::Dependency
70
+ name: tidy
71
+ prerelease: false
72
+ requirement: &id004 !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ hash: 23
78
+ segments:
79
+ - 1
80
+ - 1
81
+ - 2
82
+ version: 1.1.2
83
+ type: :runtime
84
+ version_requirements: *id004
85
+ - !ruby/object:Gem::Dependency
86
+ name: htmlentities
87
+ prerelease: false
88
+ requirement: &id005 !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ hash: 51
94
+ segments:
95
+ - 4
96
+ - 2
97
+ - 2
98
+ version: 4.2.2
99
+ type: :runtime
100
+ version_requirements: *id005
101
+ description: Ruby library for tracking UPS, Fedex and USPS packages.
102
+ email: chetan@pixelcop.net
103
+ executables: []
104
+
105
+ extensions: []
106
+
107
+ extra_rdoc_files: []
108
+
109
+ files:
110
+ - Rakefile
111
+ - VERSION
112
+ - lib/stalkr.rb
113
+ - lib/stalkr/base.rb
114
+ - lib/stalkr/fedex.rb
115
+ - lib/stalkr/result.rb
116
+ - lib/stalkr/ups.rb
117
+ - lib/stalkr/usps.rb
118
+ - test/stalkr/fedex_test.rb
119
+ - test/stalkr/ups_test.rb
120
+ - test/stalkr/usps_test.rb
121
+ - test/stalkr_test.rb
122
+ - test/test_helper.rb
123
+ has_rdoc: true
124
+ homepage: http://github.com/chetan/stalkr
125
+ licenses: []
126
+
127
+ post_install_message:
128
+ rdoc_options: []
129
+
130
+ require_paths:
131
+ - lib
132
+ required_ruby_version: !ruby/object:Gem::Requirement
133
+ none: false
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ hash: 3
138
+ segments:
139
+ - 0
140
+ version: "0"
141
+ required_rubygems_version: !ruby/object:Gem::Requirement
142
+ none: false
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ hash: 3
147
+ segments:
148
+ - 0
149
+ version: "0"
150
+ requirements: []
151
+
152
+ rubyforge_project:
153
+ rubygems_version: 1.5.0
154
+ signing_key:
155
+ specification_version: 3
156
+ summary: Ruby library for tracking packages
157
+ test_files:
158
+ - test/stalkr/fedex_test.rb
159
+ - test/stalkr/ups_test.rb
160
+ - test/stalkr/usps_test.rb
161
+ - test/stalkr_test.rb
162
+ - test/test_helper.rb