RusaMember 1.0.0 → 1.0.1
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/Manifest.txt +24 -0
- data/README.txt +6 -3
- data/Rakefile +0 -1
- data/bin/rusa_member +6 -0
- data/lib/member.rb +71 -0
- data/lib/parse_member.rb +164 -0
- data/lib/parse_result.rb +94 -0
- data/lib/rusa_member.rb +1 -1
- data/test/files/boston.html +209 -0
- data/test/files/clubMembers.txt +1674 -0
- data/test/files/doe.txt +8 -0
- data/test/files/dussler.html +101 -0
- data/test/files/dussler.txt +8 -0
- data/test/files/dussler_results_2006.html +243 -0
- data/test/files/dussler_results_2007.html +278 -0
- data/test/files/dussler_results_2008.html +211 -0
- data/test/files/member_pre.txt +82 -0
- data/test/files/member_search.txt +82 -0
- data/test/files/multiSmith.txt +241 -0
- data/test/files/no_results.html +164 -0
- data/test/files/one_result.html +172 -0
- data/test/files/post.txt +13 -0
- data/test/files/smith.html +173 -0
- data/test/files/smith.txt +10 -0
- data/test/files/thomas.html +101 -0
- data/test/files/thomas_results_2008.html +369 -0
- data/test/html_frags.rb +113 -0
- data/test/test_parse_results.rb +94 -20
- metadata +34 -2
data/Manifest.txt
CHANGED
@@ -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
|
-
|
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
|
-
|
27
|
+
sudo gem install RusaMember
|
25
28
|
|
26
29
|
== LICENSE:
|
27
30
|
|
data/Rakefile
CHANGED
data/bin/rusa_member
CHANGED
data/lib/member.rb
ADDED
@@ -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
|
data/lib/parse_member.rb
ADDED
@@ -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__
|
data/lib/parse_result.rb
ADDED
@@ -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
|
data/lib/rusa_member.rb
CHANGED
@@ -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>© 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>
|