stalkr 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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