RusaMember 1.0.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,4 +3,28 @@ Manifest.txt
3
3
  README.txt
4
4
  Rakefile
5
5
  bin/rusa_member
6
+ lib/member.rb
7
+ lib/parse_member.rb
8
+ lib/parse_result.rb
6
9
  lib/rusa_member.rb
10
+ test/files/boston.html
11
+ test/files/clubMembers.txt
12
+ test/files/doe.txt
13
+ test/files/dussler.html
14
+ test/files/dussler.txt
15
+ test/files/dussler_results_2006.html
16
+ test/files/dussler_results_2007.html
17
+ test/files/dussler_results_2008.html
18
+ test/files/member_pre.txt
19
+ test/files/member_search.txt
20
+ test/files/multiSmith.txt
21
+ test/files/no_results.html
22
+ test/files/one_result.html
23
+ test/files/post.txt
24
+ test/files/smith.html
25
+ test/files/smith.txt
26
+ test/files/thomas.html
27
+ test/files/thomas_results_2008.html
28
+ test/html_frags.rb
29
+ test/test_parse_member.rb
30
+ test/test_parse_results.rb
data/README.txt CHANGED
@@ -12,8 +12,11 @@ Extract data from RUSA web site to get members information, results, awards
12
12
  * FIX (list of features or problems)
13
13
 
14
14
  == SYNOPSIS:
15
-
16
- FIX (code sample of usage)
15
+ print membership information, current year results for member 136
16
+ bin/rusa_member --id 136
17
+
18
+ print membership information, 2003 and 2007 results for Peter Dusel
19
+ bin/rusa_member --lname dusel --year 2003,2007
17
20
 
18
21
  == REQUIREMENTS:
19
22
 
@@ -21,7 +24,7 @@ Extract data from RUSA web site to get members information, results, awards
21
24
 
22
25
  == INSTALL:
23
26
 
24
- * FIX (sudo gem install, anything else)
27
+ sudo gem install RusaMember
25
28
 
26
29
  == LICENSE:
27
30
 
data/Rakefile CHANGED
@@ -13,5 +13,4 @@ Hoe.new('RusaMember', RusaMember::VERSION) do |p|
13
13
 
14
14
  end
15
15
 
16
- #task :gem => 'test'
17
16
  # vim: syntax=Ruby
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby -w
2
+
3
+ require 'rubygems'
4
+ require 'lib/parse_member'
5
+
6
+ ParseMember.run
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ ##
4
+ # collection of people in the club
5
+ class Members < Hash
6
+
7
+ def << person
8
+ self[person.id.to_i] = person
9
+ end
10
+
11
+ def find_by_id id
12
+ self[id]
13
+ end
14
+
15
+ end
16
+
17
+ class Person
18
+ ATTRIBUTES =
19
+ [:first_name, :last_name, :id, :member_expiration_date, :club, :results, :awards]
20
+ attr_accessor(*ATTRIBUTES)
21
+
22
+ def initialize meta = nil
23
+ ## I was trying to avoid initializing an ivar in the do loop
24
+ ## if it had already been initialized, but couldn't figure out how to do that
25
+ ##
26
+ ## I was trying to make this work without having to init the array and hash
27
+ ## after the do loop
28
+ #@results = []
29
+ #@awards = {}
30
+ #ATTRIBUTES.find_all{|a| self.a.nil?}.each do |item| <-- does not work
31
+ #
32
+ # the 'unless' below does work but doesn't look right
33
+
34
+ @results = []
35
+ @awards = {}
36
+
37
+ ATTRIBUTES.each do |item|
38
+ self.send("#{item}=", (meta[item] || "")) unless item == :results or item == :awards
39
+ end if meta
40
+
41
+ end
42
+
43
+ end
44
+
45
+ ##
46
+ # the result of an event that the member participated in
47
+ class Result
48
+ ATTRIBUTES = [ :certificate, :date, :distance, :organizer, :type ]
49
+ attr_accessor(*ATTRIBUTES)
50
+
51
+ def initialize meta = nil
52
+ ATTRIBUTES.each do |item|
53
+ self.send("#{item}=", (meta[item] || ""))
54
+ end if meta
55
+ end
56
+
57
+ end
58
+
59
+ ##
60
+ # awards for total distance, number of events
61
+ class Award
62
+ ATTRIBUTES = [ :type, :year ]
63
+ attr_accessor(*ATTRIBUTES)
64
+
65
+ def initialize meta = nil
66
+ ATTRIBUTES.each do |item|
67
+ self.send("#{item}=", (meta[item] || ""))
68
+ end if meta
69
+ end
70
+
71
+ end
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env ruby -w
2
+
3
+ ##
4
+ # Student Name: bill dussler
5
+ # Homework Week: 8
6
+ #
7
+ # screen scrape data from a web site, parse content
8
+ # and instatntiate club member information
9
+ #
10
+ require 'rubygems'
11
+ require 'nokogiri'
12
+ require 'net/http'
13
+ require 'optparse'
14
+ require 'time'
15
+ require 'lib/member'
16
+ require 'lib/parse_result'
17
+
18
+ class ParseMember
19
+ attr_accessor :members, :options, :url, :parse_result
20
+
21
+ MEMBER_QUERY_URL = 'http://www.rusa.org/cgi-bin/membersearch_PF.pl'
22
+
23
+ # match parm key with parameter we will pass
24
+ PARMS = {'clubs' => 'club',
25
+ 'ids' => 'mid',
26
+ 'lnames' => 'sname'}
27
+
28
+ def self.run
29
+ pm = ParseMember.new
30
+ pm.run
31
+ pm.report
32
+
33
+ end
34
+
35
+ def initialize
36
+ @members = Members.new
37
+ @url = MEMBER_QUERY_URL
38
+ @parse_result = ParseResult.new
39
+
40
+ # The options specified on the command line will be collected in *options*.
41
+ # We set default values here.
42
+ @options = {}
43
+ @options['status'] = 'current'
44
+ @options['year'] = [Time.now.year]
45
+
46
+ opts = OptionParser.new do |opts|
47
+ opts.banner = "Usage: parse_member.rb [options]"
48
+
49
+ # include these clubs
50
+ opts.on("--club seattle,oregon,boston", Array, "include the following clubs") do |list|
51
+ @options['clubs'] = list
52
+ end
53
+ # include membership ids
54
+ opts.on("--id 137,2245,2157", Array, "include the following membership ids") do |list|
55
+ @options['ids'] = list
56
+ end
57
+
58
+ # include these last names
59
+ opts.on("--lname smith,jones,brown", Array, "Include members with the following last names") do |list|
60
+ @options['lnames'] = list
61
+ end
62
+
63
+ # get results for these years
64
+ opts.on("--year ", Array, "get results for the following years. Defaults to current year") do |list|
65
+ @options['year'] = list
66
+ end
67
+
68
+ # Boolean switch. default is current members (no expired memberships)
69
+ opts.on("-a", "Current and past members") do |a|
70
+ @options['status'] = a ? 'all' : 'current'
71
+ end
72
+
73
+ # No argument, shows at tail. This will print an options summary.
74
+ opts.on_tail("-h", "--help", "Show this message") do
75
+ puts opts
76
+ exit
77
+ end
78
+ end
79
+
80
+ opts.parse!(ARGV)
81
+
82
+ end
83
+
84
+ def run &block
85
+ if block_given? then
86
+ doc = fetch(@url, {}, &block)
87
+ parse(doc, &block)
88
+
89
+ else
90
+ # get requested parms and fetch url with them
91
+ parms = {'status' => @options['status']}
92
+ PARMS.each do |key, parm|
93
+ next if @options[key].nil?
94
+ @options[key].each do |value|
95
+ parms[parm] = value
96
+ doc = fetch(@url, parms, &block)
97
+ parse(doc)
98
+ end
99
+ parms.delete parm
100
+ end
101
+ end
102
+ end
103
+
104
+ def fetch url, parms=nil
105
+ if block_given? then
106
+ f = yield url
107
+ else
108
+ f = Net::HTTP.post_form(URI.parse(url), parms).body
109
+ end
110
+ Nokogiri::HTML(f)
111
+ end
112
+
113
+ def parse doc
114
+ ##
115
+ # the data we care about starts in <td> that lists count of matches found
116
+ doc.css('td').each do |link|
117
+ if ( link.text =~ /(\d+) Matches Found\n/im ) then
118
+ # member element is in a <p>, instantiate Person with relevant data
119
+ link.css('p').each do |member|
120
+ member.text =~ /(.*), (.*) \| RUSA No\. (\d+) \|.*\n(.*) \|.*(\d{4}\/\d{2}\/\d{2})/im
121
+
122
+ member = Person.new({
123
+ :last_name => $1.strip,
124
+ :first_name => $2.strip,
125
+ :id => $3,
126
+ :club => $4.strip,
127
+ :member_expiration_date => Time.parse($5)
128
+ } )
129
+
130
+ # don't run if block given (such as during test)
131
+ unless block_given? then
132
+ @options['year'].each do |year|
133
+ @parse_result.results member, year
134
+ end
135
+ end
136
+
137
+ @members<< member
138
+ end
139
+ end
140
+ end
141
+ end
142
+
143
+ def report
144
+ @members.sort{|a,b| a[1].last_name + a[1].first_name <=> b[1].last_name + b[1].first_name}.each do |id, member|
145
+ puts "%25s\t%7s\t%12s\t%30s\n" % ["#{member.first_name} #{member.last_name}",
146
+ member.id,
147
+ member.member_expiration_date,
148
+ member.club]
149
+ member.results.sort{|a,b| a.date <=> b.date}.each do |result|
150
+ puts " Result: %12s\t%10s\t%10s\t%8s\n" % [result.date, result.distance,
151
+ result.certificate, result.type]
152
+ end
153
+
154
+ member.awards.each do |year, awards|
155
+ awards.each do |award|
156
+ puts " Award: %8s\t%10s\n" % [award.year, award.type]
157
+ end
158
+ end
159
+ end
160
+ end
161
+
162
+ end
163
+
164
+ ParseMember.run if $0 == __FILE__
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env ruby -w
2
+
3
+ ##
4
+ # Student Name: bill dussler
5
+ # Homework Week: 8
6
+ #
7
+ # screen scrape data from a web site, parse content
8
+ # and instatntiate club member information
9
+ #
10
+ require 'rubygems'
11
+ require 'nokogiri'
12
+ require 'net/http'
13
+ require 'optparse'
14
+ require 'time'
15
+ require 'date'
16
+ require 'lib/member'
17
+
18
+ class ParseResult
19
+ RESULT_QUERY_URL = 'http://www.rusa.org/cgi-bin/results.pl'
20
+
21
+ # match parm key with parameter we will pass
22
+ PARMS = {'clubs' => 'club',
23
+ 'ids' => 'mid',
24
+ 'lnames' => 'Last_Name',
25
+ 'fnames' => 'First_Name',
26
+ 'years' => 'searchyear',
27
+ }
28
+
29
+ def results member, year=Time.now.year, url=RESULT_QUERY_URL
30
+
31
+ if block_given? then
32
+ f = yield url
33
+ else
34
+ parms = {'Last_Name' => member.last_name,
35
+ 'First_Name' => member.first_name,
36
+ 'searchyear' => year
37
+ }
38
+
39
+ f = Net::HTTP.post_form(URI.parse(url), parms).body
40
+ end
41
+ doc = Nokogiri::HTML(f)
42
+
43
+ parse(member, doc)
44
+ end
45
+
46
+ def parse member, doc
47
+ ##
48
+ # sanity check-- the li items gives us search terms. compare to member data
49
+ doc.css('li').each_with_index do |li, i|
50
+ if li.text =~ /first name=(.*)/i && member.first_name.downcase != $1.downcase then
51
+ raise "first name #{$1} in results does not match #{member.first_name}"
52
+ end
53
+
54
+ if li.text =~ /last name=(.*)/i && member.last_name.downcase != $1.downcase then
55
+ raise "last name #{$1} in results does not match #{member.last_name}"
56
+ end
57
+ end
58
+
59
+ # year is only displayed in results, not as parm anywhere
60
+ year_results = nil
61
+ ##
62
+ # the data we care about starts in a <td> that lists the count of matches found
63
+ doc.css('table table tr').each do |row|
64
+ result_data = row.css('td')
65
+ next if result_data.size.zero?
66
+
67
+ member.results<< Result.new({
68
+ :certificate => result_data[0].text,
69
+ :type => result_data[1].text,
70
+ :distance => result_data[2].text,
71
+ # Time.parse needs slashes, not dashes
72
+ :date => Time.parse(result_data[3].text.gsub("-","/")),
73
+ :organizer => result_data[4].text,
74
+ } )
75
+ end
76
+
77
+ year_results = member.results.last.date.year unless member.results.empty?
78
+ member.awards[year_results] = [] unless year_results.nil? or member.awards.has_key? year_results
79
+ ##
80
+ # find the awards for this year
81
+ doc.css('table tr td').each do |row|
82
+ next unless row.text =~ /Award status: (.*)/i
83
+
84
+ $1.split(",").each do |award_type|
85
+ member.awards[year_results]<< Award.new({
86
+ :type => award_type.strip,
87
+ :year => year_results.to_i,
88
+ })
89
+ end
90
+ end
91
+
92
+ end
93
+
94
+ end
@@ -1,3 +1,3 @@
1
1
  class RusaMember
2
- VERSION = '1.0.0'
2
+ VERSION = '1.0.1'
3
3
  end
@@ -0,0 +1,209 @@
1
+
2
+
3
+
4
+
5
+
6
+
7
+
8
+
9
+
10
+
11
+
12
+
13
+
14
+
15
+
16
+
17
+
18
+
19
+
20
+ <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML Level 3.2//EN">
21
+ <HTML>
22
+ <HEAD>
23
+ <TITLE>RUSA: Members </TITLE>
24
+ <META HTTP-EQUIV="Keywords" CONTENT="Bicycle Touring, Bicycle Racing, Long Distance Cycling, Cycling, Bicycling, Randonneur, Randonneuring, Brevet, RUSA">
25
+ <META HTTP-EQUIV="Reply-to" CONTENT="lois_springsteen@prodigy.net (Lois Springsteen)">
26
+ <BASEFONT SIZE=3>
27
+ </HEAD>
28
+ <BODY BGCOLOR="#FFFFFF" TEXT="#003399" LINK="#FF0000" VLINK="#FF0000" ALINK="#003399">
29
+ <TABLE BORDER=0 HSPACE=0 cellpadding=10 WIDTH=100%>
30
+ <TR>
31
+ <TD ALIGN=left VALIGN=middle width=140>
32
+ <IMG SRC="http://www.rusa.org/Images/logorusa.gif" ALT="RUSA Logo" HSPACE=0 ALIGN=absmiddle>
33
+ </TD>
34
+ <TD ALIGN=left VALIGN=middle>
35
+ <H1><FONT FACE="ARIAL, HELVETICA"><B>Members</B></FONT></H1>
36
+ </TD>
37
+ <TD ALIGN=right VALIGN=middle></TD>
38
+ </TR>
39
+ </TABLE>
40
+ <HR SIZE=3 WIDTH="100%" ALIGN=CENTER>
41
+ <TABLE BORDER=0 HSPACE=0 cellpadding=10>
42
+ <TR ALIGN=left VALIGN=top width=140>
43
+ <TD NOWRAP>
44
+ <FONT FACE="ARIAL, HELVETICA" SIZE=-1>
45
+ <B>General</B><SMALL>
46
+ <BR>-- <A HREF="http://www.rusa.org/announce.html"><FONT COLOR="#3cb545">Announcements (11/28)</FONT></A>
47
+ <BR>-- <A HREF="http://www.rusa.org/about.html">About Randonneuring</A>
48
+ <BR>-- <A HREF="http://www.rusa.org/links.html">Links</A>
49
+ <BR>-- <A HREF="http://www.rusa.org/index.html">Home</A>
50
+ </SMALL>
51
+ <P><B>Search For</B><SMALL>
52
+ <BR>-- <A HREF="http://www.rusa.org/cgi-bin/eventsearch_GF.pl">Rides</A>
53
+ <BR>-- <A HREF="http://www.rusa.org/results_search.html">Results</A>
54
+ <BR>-- <A HREF="http://www.rusa.org/cgi-bin/membersearch_GF.pl">Members</A>
55
+ <BR>-- <A HREF="http://www.rusa.org/cgi-bin/officialsearch_GF.pl">Officials</A>
56
+ <BR>-- <A HREF="http://www.rusa.org/cgi-bin/clubsearch_GF.pl">ACP Club Codes</A>
57
+ <BR>-- <A HREF="http://www.rusa.org/cgi-bin/routesearch_GF.pl">Routes</A>
58
+ <BR>-- <A HREF="http://www.rusa.org/teamrando.html">Team Events</A>
59
+ <BR>-- <A HREF="http://www.rusa.org/perminfo.html">Permanents</A>
60
+ </SMALL>
61
+ <P><B>Members' Info</B><SMALL>
62
+ <BR>-- <A HREF="http://www.rusa.org/join.html">Join/Renew</A>
63
+ <BR>-- <A HREF="http://www.rusa.org/awards.html">Awards</A>
64
+ <BR>-- <A HREF="http://www.rusa.org/cgi-bin/store_GF.pl">Online Store</A>
65
+ <BR>-- <A HREF="http://www.rusa.org/nletter.html">Newsletter</A>
66
+ <BR>-- <A HREF="http://www.rusa.org/pbpphotogallery.html">Photo Gallery</A>
67
+ <BR>-- <A HREF="http://www.rusa.org/stories.html">Rider Stories</A>
68
+ </SMALL>
69
+ <P><B>Long Brevets</B><SMALL>
70
+ <BR>-- <A HREF="http://www.rusa.org/pbp.html">PBP: Paris-Brest-Paris</A>
71
+ <BR>-- <A HREF="http://www.rusa.org/1200kms.html">Others</A>
72
+ </SMALL>
73
+ <P><B>Administration</B><SMALL>
74
+ <BR>-- <A HREF="http://www.rusa.org/rules.html">Rules</A>
75
+ <BR>-- <A HREF="http://www.rusa.org/rbakit.html">RBAs</A>
76
+ <BR>-- <A HREF="http://www.rusa.org/records.html">Records</A>
77
+ </SMALL>
78
+ </FONT>
79
+ </TD>
80
+ <TD ALIGN=left VALIGN=top>
81
+
82
+
83
+ 28 Matches Found
84
+ <P><B>Beaulieu</B>, Lynda M | RUSA No. 5213 | Cambridge, MA
85
+ <BR>Boston Brevet Series | ACP Club Code: 921030
86
+ <BR><A HREF="/join.html">Membership Expires:</A>
87
+ <B>2009/12/31</B>
88
+ <P><B>Brandhorst</B>, Eric | RUSA No. 5138 | Carlisle, MA
89
+ <BR>Boston Brevet Series | ACP Club Code: 921030
90
+ <BR><A HREF="/join.html">Membership Expires:</A>
91
+ <B>2009/12/31</B>
92
+ <P><B>Brooke</B>, Justin | RUSA No. 4193 | Cambridge, MA
93
+ <BR>Boston Brevet Series | ACP Club Code: 921030
94
+ <BR><A HREF="/join.html">Membership Expires:</A>
95
+ <B>2010/12/31</B>
96
+ <P><B>Carrington</B>, Dave | RUSA No. 4696 | Newport, RI
97
+ <BR>Boston Montreal Boston | ACP Club Code: 921003
98
+ <BR><A HREF="/join.html">Membership Expires:</A>
99
+ <B>2009/12/31</B>
100
+ <P><B>Coldwell</B>, Charles | RUSA No. 2271 | Somerville, MA
101
+ <BR>Boston Brevet Series | ACP Club Code: 921030
102
+ <BR><A HREF="/join.html">Membership Expires:</A>
103
+ <B>2009/12/31</B>
104
+ <P><B>Concepcion</B>, Cris | RUSA No. 3425 | Belmont, MA
105
+ <BR>Boston Brevet Series | ACP Club Code: 921030
106
+ <BR><A HREF="/join.html">Membership Expires:</A>
107
+ <B>2009/12/31</B>
108
+ <P><B>Creason</B>, Gary L | RUSA No. 3354 | Bedford, MA
109
+ <BR>Boston Brevet Series | ACP Club Code: 921030
110
+ <BR><A HREF="/join.html">Membership Expires:</A>
111
+ <B>2008/12/31</B>
112
+ <P><B>Dancy</B>, Abram | RUSA No. 1258 | Shrewsbury, MA
113
+ <BR>Boston Brevet Series | ACP Club Code: 921030
114
+ <BR><A HREF="/join.html">Membership Expires:</A>
115
+ <B>2009/12/31</B>
116
+ <P><B>Dancy</B>, Leigh | RUSA No. 5205 | Shrewsbury, MA
117
+ <BR>Boston Brevet Series | ACP Club Code: 921030
118
+ <BR><A HREF="/join.html">Membership Expires:</A>
119
+ <B>2009/12/31</B>
120
+ <P><B>Fallon</B>, David | RUSA No. 3604 | Somerville, MA
121
+ <BR>Boston Brevet Series | ACP Club Code: 921030
122
+ <BR><A HREF="/join.html">Membership Expires:</A>
123
+ <B>2009/12/31</B>
124
+ <P><B>Gafgen</B>, Pierce | RUSA No. 0009 | Middletown, RI
125
+ <BR>Boston Montreal Boston | ACP Club Code: 921003
126
+ <BR><A HREF="/join.html">Membership Expires:</A>
127
+ <B>2009/12/31</B>
128
+ <P><B>Howland</B>, David | RUSA No. 4950 | Worcester, MA
129
+ <BR>Boston Brevet Series | ACP Club Code: 921030
130
+ <BR><A HREF="/join.html">Membership Expires:</A>
131
+ <B>2008/12/31</B>
132
+ <P><B>Ingle</B>, Bruce | RUSA No. 0607 | Framingham, MA
133
+ <BR>Boston Brevet Series | ACP Club Code: 921030
134
+ <BR><A HREF="/join.html">Membership Expires:</A>
135
+ <B>2008/12/31</B>
136
+ <P><B>Ingle</B>, Tracey A L | RUSA No. 1591 | Stow, MA
137
+ <BR>Boston Brevet Series | ACP Club Code: 921030
138
+ <BR><A HREF="/join.html">Membership Expires:</A>
139
+ <B>2008/12/31</B>
140
+ <P><B>Kassen</B>, Jonathan | RUSA No. 3598 | Medford, MA
141
+ <BR>Boston Brevet Series | ACP Club Code: 921030
142
+ <BR><A HREF="/join.html">Membership Expires:</A>
143
+ <B>2010/12/31</B>
144
+ <P><B>Lafferty</B>, David | RUSA No. 4284 | Billerica, MA
145
+ <BR>Boston Brevet Series | ACP Club Code: 921030
146
+ <BR><A HREF="/join.html">Membership Expires:</A>
147
+ <B>2008/12/31</B>
148
+ <P><B>Levesque</B>, Daniel | RUSA No. 2930 | Wilton, NH
149
+ <BR>Boston Brevet Series | ACP Club Code: 921030
150
+ <BR><A HREF="/join.html">Membership Expires:</A>
151
+ <B>2008/12/31</B>
152
+ <P><B>Martin</B>, Tim | RUSA No. 4234 | Westford, MA
153
+ <BR>Boston Brevet Series | ACP Club Code: 921030
154
+ <BR><A HREF="/join.html">Membership Expires:</A>
155
+ <B>2008/12/31</B>
156
+ <P><B>McLaughlin</B>, Jake | RUSA No. 5063 | Hanover, NH
157
+ <BR>Boston Brevet Series | ACP Club Code: 921030
158
+ <BR><A HREF="/join.html">Membership Expires:</A>
159
+ <B>2008/12/31</B>
160
+ <P><B>Omara</B>, Bill | RUSA No. 2512 | Briston, RI
161
+ <BR>Boston Brevet Series | ACP Club Code: 921030
162
+ <BR><A HREF="/join.html">Membership Expires:</A>
163
+ <B>2009/12/31</B>
164
+ <P><B>Page</B>, Walter | RUSA No. 0980 | Lincoln, MA
165
+ <BR>Boston Brevet Series | ACP Club Code: 921030
166
+ <BR><A HREF="/join.html">Membership Expires:</A>
167
+ <B>2009/12/31</B>
168
+ <P><B>Pope-Lance</B>, Elton | RUSA No. 4734 | Sudbury, MA
169
+ <BR>Boston Brevet Series | ACP Club Code: 921030
170
+ <BR><A HREF="/join.html">Membership Expires:</A>
171
+ <B>2008/12/31</B>
172
+ <P><B>Roy</B>, Matt | RUSA No. 4947 | Arlington, MA
173
+ <BR>Boston Brevet Series | ACP Club Code: 921030
174
+ <BR><A HREF="/join.html">Membership Expires:</A>
175
+ <B>2009/12/31</B>
176
+ <P><B>Schwarz</B>, Bill | RUSA No. 0586 | Kinderhook, NY
177
+ <BR>Boston Brevet Series | ACP Club Code: 921030
178
+ <BR><A HREF="/join.html">Membership Expires:</A>
179
+ <B>2009/12/31</B>
180
+ <P><B>Searles</B>, Emily | RUSA No. 4813 | Billerica, MA
181
+ <BR>Boston Brevet Series | ACP Club Code: 921030
182
+ <BR><A HREF="/join.html">Membership Expires:</A>
183
+ <B>2008/12/31</B>
184
+ <P><B>Spatz</B>, Harry | RUSA No. 4362 | Lexington, MA
185
+ <BR>Boston Brevet Series | ACP Club Code: 921030
186
+ <BR><A HREF="/join.html">Membership Expires:</A>
187
+ <B>2008/12/31</B>
188
+ <P><B>Tolonen</B>, Andrew | RUSA No. 4292 | Boston, MA
189
+ <BR>Boston Brevet Series | ACP Club Code: 921030
190
+ <BR><A HREF="/join.html">Membership Expires:</A>
191
+ <B>2008/12/31</B>
192
+ <P><B>Wise</B>, Jennifer | RUSA No. 0001 | Middletown, RI
193
+ <BR>Boston Montreal Boston | ACP Club Code: 921003
194
+ <BR><A HREF="/join.html">Membership Expires:</A>
195
+ <B>2009/12/31</B>
196
+
197
+ </TD>
198
+ </TR>
199
+ </TABLE>
200
+ <HR SIZE=3 WIDTH="100%" ALIGN=Center>
201
+ <P><FONT FACE="ARIAL, HELVETICA" SIZE=-1>Revision: November 29, 2008<BR>
202
+ Please direct questions, comments, or problem reports to the <A HREF="http://www.rusa.org/webmaster.html">webmaster</A>.<BR>
203
+ <B>&copy; Copyright 2008, <A HREF="http://www.rusa.org">Randonneurs USA</A></B>,
204
+ except as noted otherwise.</FONT></P>
205
+ </BODY>
206
+ </HTML>
207
+
208
+ </BODY>
209
+ </HTML>