nanowrimo 0.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,22 @@
1
+ === 0.7 / 2009-05-29
2
+
3
+ * Refactored many similar methods into a Nanowrimo::Core class per a suggestion from ZenSpider.
4
+ * Implemented a basic caching mechanism. This isn't fully implemented yet, and will be tuned in future releases.
5
+ * Documentation added to all classes.
6
+
7
+ === 0.6 / 2009-05-25
8
+
9
+ * page scraping in place for extra user data
10
+ * not all functionality is useful for this, since profile pages have minimal data. submitted request for API changes to Nanowrimo.org crew.
11
+
12
+ === 0.5 / 2009-05-21
13
+
14
+ * first major release
15
+ * API is fully functional as a wrapper for the Nanowrimo API
16
+
17
+ === 0.1 / 2009-05-17
18
+
19
+ * 1 major enhancement
20
+
21
+ * Birthday!
22
+
@@ -0,0 +1,27 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.txt
4
+ Rakefile
5
+ lib/nanowrimo.rb
6
+ lib/nanowrimo/cache.rb
7
+ lib/nanowrimo/core.rb
8
+ lib/nanowrimo/genre.rb
9
+ lib/nanowrimo/region.rb
10
+ lib/nanowrimo/site.rb
11
+ lib/nanowrimo/user.rb
12
+ test/test_cache.rb
13
+ test/test_core.rb
14
+ test/test_genre.rb
15
+ test/test_nanowrimo.rb
16
+ test/test_region.rb
17
+ test/test_site.rb
18
+ test/test_user.rb
19
+ test/fixtures/genre_wc.xml
20
+ test/fixtures/genre_wc_history.xml
21
+ test/fixtures/region_wc.xml
22
+ test/fixtures/region_wc_history.xml
23
+ test/fixtures/site_wc.xml
24
+ test/fixtures/site_wc_history.xml
25
+ test/fixtures/user_page.htm
26
+ test/fixtures/user_wc.xml
27
+ test/fixtures/user_wc_history.xml
@@ -0,0 +1,82 @@
1
+ = nanowrimo
2
+
3
+ * http://nanowrimo.rubyforge.org
4
+ * http://github.com/illuminerdi/nanowrimo
5
+ * http://www.nanowrimo.org
6
+
7
+ == DESCRIPTION:
8
+
9
+ A simple API wrapper for Nanowrimo.org. Nanowrimo Word Count API documentation here:
10
+ http://www.nanowrimo.org/eng/wordcount_api
11
+
12
+ == FEATURES/PROBLEMS:
13
+
14
+ Features:
15
+ * Simple! Clean! Well-tested!
16
+ * Easy to roll into a Rails application (that's next)
17
+ * Separate APIs for Users, Site, Regions, and Genres
18
+ * Page scraping place for basic user data from the profile page
19
+ * Caches data to avoid November bandwidth issues
20
+
21
+ Problems:
22
+ * The Genres API on Nanowrimo.org is a little broken right now, so there's not much data to be loaded.
23
+ * Page scraping is dumb and costly. And the data I get is minimal. Submitted request for new API features with Nanowrimo.org crew.
24
+ * Caching is still fairly immature in this package. Need to tune it further before November hits.
25
+
26
+ == SYNOPSIS:
27
+
28
+ >> me = Nanowrimo::User.new(240659)
29
+ => #<Nanowrimo::User:0x105b904 @uid=240659>
30
+ >> me.load
31
+ => ["uid", "uname", "user_wordcount"]
32
+ >> me.user_wordcount
33
+ => "55415"
34
+ >> me.winner?
35
+ => true
36
+ # YAY!
37
+
38
+ # Want to get a list of your writing buddies?
39
+ >> me.parse_profile
40
+ >> me.buddies
41
+ => ["94450", "208549", "236224", "244939", "301080", "405136", "139709", "229531", "240149", "245095", "309620"]
42
+
43
+ # Want an array of day-by-day progress for yourself?
44
+ >> me.load_history
45
+
46
+ # Want an array of day-by-day progress for your region?
47
+ >> my_region = Nanowrimo::Region.new(84)
48
+ >> my_region.load_history
49
+
50
+ == REQUIREMENTS:
51
+
52
+ * ruby 1.8.6
53
+ * mechanize 0.9.2
54
+
55
+ == INSTALL:
56
+
57
+ * sudo gem install nanowrimo
58
+
59
+ == LICENSE:
60
+
61
+ (The MIT License)
62
+
63
+ Copyright (c) 2009 Joshua Clingenpeel
64
+
65
+ Permission is hereby granted, free of charge, to any person obtaining
66
+ a copy of this software and associated documentation files (the
67
+ 'Software'), to deal in the Software without restriction, including
68
+ without limitation the rights to use, copy, modify, merge, publish,
69
+ distribute, sublicense, and/or sell copies of the Software, and to
70
+ permit persons to whom the Software is furnished to do so, subject to
71
+ the following conditions:
72
+
73
+ The above copyright notice and this permission notice shall be
74
+ included in all copies or substantial portions of the Software.
75
+
76
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
77
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
78
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
79
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
80
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
81
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
82
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,13 @@
1
+ # -*- ruby -*-
2
+
3
+ $: << 'lib'
4
+ require 'rubygems'
5
+ require 'hoe'
6
+ require './lib/nanowrimo.rb'
7
+
8
+ Hoe.new('nanowrimo', Nanowrimo::VERSION) do |p|
9
+ # p.rubyforge_name = 'nanowrimox' # if different than lowercase project name
10
+ p.developer('Joshua Clingenpeel', 'joshua.clingenpeel@gmail.com')
11
+ end
12
+
13
+ # vim: syntax=Ruby
@@ -0,0 +1,76 @@
1
+ #! /usr/bin/env ruby -w
2
+
3
+ # Nanowrimo is an API Wrapper for the Nanowrimo.org WCAPI.
4
+ # In its current implementation it manages all data brought down from the WCAPI.
5
+ #
6
+ # Author:: Joshua Clingenpeel (joshua.clingenpeel@gmail.com)
7
+ # Copyright:: Copyright (c) 2009 Joshua Clingenpeel
8
+ # License:: MIT License
9
+
10
+ require 'rubygems'
11
+ require 'mechanize'
12
+ require 'open-uri'
13
+ require 'nanowrimo/core'
14
+ require 'nanowrimo/user'
15
+ require 'nanowrimo/site'
16
+ require 'nanowrimo/region'
17
+ require 'nanowrimo/genre'
18
+ require 'nanowrimo/cache'
19
+
20
+ # This module handles caching, a few constants, and the all-important XML parsing method
21
+ # for both current data and historical data from the WCAPI. It should be generic enough to
22
+ # never have to be touched again, but keep your fingers crossed anyway.
23
+
24
+ module Nanowrimo
25
+ # Current API version
26
+ VERSION = '0.7'
27
+ # Current static root WCAPI uri
28
+ API_URI = 'http://www.nanowrimo.org/wordcount_api'
29
+ # Current individual user word count goal. For fun!
30
+ GOAL = 50_000
31
+ Nanowrimo::Cache.load_cache if Nanowrimo::Cache.cache_data == {}
32
+
33
+ # Pull requested data from cache or from the WCAPI
34
+ def self.parse(path, key, attribs)
35
+ result = data_from_cache(path, key) ||
36
+ data_from_internets(path, key, attribs)
37
+ end
38
+
39
+ # Finds the data in the cache and returns it, or nil, taking into account age of cache data.
40
+ def self.data_from_cache(path, key)
41
+ type = path.split('/').first
42
+ Nanowrimo::Cache.find_data(type, key)
43
+ end
44
+
45
+ # Parses XML from the WCAPI and returns an array of hashes with the data. Caches it, too.
46
+ def self.data_from_internets(path, key, attribs)
47
+ type = path.split('/').first
48
+ uri = "#{API_URI}/#{type}"
49
+ uri = "#{uri}/#{key}" unless key.nil?
50
+ result = []
51
+ begin
52
+ timeout(2) {
53
+ doc = Nokogiri::XML(open(uri))
54
+ doc.xpath(path).each {|n|
55
+ node = {}
56
+ attribs.each {|d|
57
+ node[d.intern] = n.at(d).content
58
+ }
59
+ result << node
60
+ }
61
+ }
62
+ rescue Timeout::Error
63
+ throw NanowrimoError, "Timed out attempting to connect to Nanowrimo.org"
64
+ end
65
+ key ||= type # kinda hackish, but for the site stats it's needed
66
+ Nanowrimo::Cache.add_to_cache("#{type}","#{key}",result)
67
+ result
68
+ end
69
+
70
+ at_exit {
71
+ Nanowrimo::Cache.save_cache_to_disk
72
+ }
73
+ end
74
+
75
+ # Generic error class
76
+ class NanowrimoError < StandardError; end
@@ -0,0 +1,52 @@
1
+ #! /usr/bin/env ruby -w
2
+
3
+ require 'thread'
4
+ require 'yaml'
5
+
6
+ module Nanowrimo
7
+ # Handles caching of WCAPI data
8
+ class Cache
9
+ CACHE_FILE = './nano_cache'
10
+ # 24 hours in seconds, defines when cached data expires.
11
+ DEFAULT_MAX_CACHE_AGE = (24*60*60)
12
+ @@cache_data = {}
13
+ @@cache_mutex = Mutex.new
14
+ def self.cache_data
15
+ @@cache_data
16
+ end
17
+ def self.cache_mutex
18
+ @@cache_mutex
19
+ end
20
+
21
+ # Load the cached data into memory from disk.
22
+ def self.load_cache
23
+ if File.exist?(CACHE_FILE)
24
+ @@cache_data = YAML::load(File.read(CACHE_FILE))
25
+ end
26
+ end
27
+
28
+ def self.find_data(type, key)
29
+ @@cache_mutex.synchronize {
30
+ return nil unless @@cache_data["#{type}"]
31
+ return nil unless @@cache_data["#{type}"]["#{key}"]
32
+ if Time.now - @@cache_data["#{type}"]["#{key}"][:created_at] < DEFAULT_MAX_CACHE_AGE
33
+ @@cache_data["#{type}"]["#{key}"][:data]
34
+ end
35
+ }
36
+ end
37
+
38
+ # For when the cache needs to be not in memory anymore.
39
+ def self.save_cache_to_disk
40
+ File.open(CACHE_FILE, 'w') do |out|
41
+ YAML.dump(@@cache_data, out)
42
+ end
43
+ end
44
+
45
+ # Receives data that needs to be added to the cache
46
+ def self.add_to_cache type, key, data=[]
47
+ @@cache_mutex.synchronize{
48
+ @@cache_data["#{type}"] = {"#{key}" => Hash[:data, data, :created_at, Time.now]}
49
+ }
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,19 @@
1
+ #! /usr/bin/env ruby -w
2
+
3
+ module Nanowrimo
4
+ # Core load methods
5
+ class Core
6
+ # Returns the values for all attributes for a given WCAPI type
7
+ def load
8
+ attribs = Nanowrimo.parse(load_field,id,self.class::FIELDS).first
9
+ self.class::FIELDS.each do |attrib|
10
+ self.send(:"#{attrib}=", attribs[attrib.intern])
11
+ end
12
+ end
13
+
14
+ # Returns the values for all attributes for a given WCAPI type's history
15
+ def load_history
16
+ self.history = Nanowrimo.parse(load_history_field,id,self.class::HISTORY_FIELDS)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,32 @@
1
+ #! /usr/bin/env ruby -w
2
+
3
+ module Nanowrimo
4
+ # Handles Nanowrimo Genre data.
5
+ class Genre < Core
6
+ # fields expected from the main Genre WCAPI
7
+ FIELDS = %w[gid gname genre_wordcount max min stddev average count]
8
+ # fields expected from the Genre history WCAPI
9
+ HISTORY_FIELDS = %w[wc wcdate max min stddev average count]
10
+ attr_accessor(*FIELDS)
11
+ attr_accessor :history
12
+ # creates a new Region object
13
+ def initialize(gid)
14
+ @gid = gid
15
+ end
16
+
17
+ # converts the WCAPI unique identifier for this type into a Nanowrimo::Core-friendly 'id'
18
+ def id
19
+ @gid
20
+ end
21
+
22
+ # converts the WCAPI path for this type into something Nanowrimo::Core-friendly
23
+ def load_field
24
+ 'wcgenre'
25
+ end
26
+
27
+ # converts the WCAPI history path for this type into something Nanowrimo::Core-friendly
28
+ def load_history_field
29
+ 'wcgenrehist/wordcounts/wcentry'
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,32 @@
1
+ #! /usr/bin/env ruby -w
2
+
3
+ module Nanowrimo
4
+ # Handles Nanowrimo Region data.
5
+ class Region < Core
6
+ # fields expected from the main Region WCAPI
7
+ FIELDS = %w[rid rname region_wordcount max min stddev average count donations numdonors]
8
+ # fields expected from the Region history WCAPI
9
+ HISTORY_FIELDS = %w[wc wcdate max min stddev average count donations donors]
10
+ attr_accessor(*FIELDS)
11
+ attr_accessor :history
12
+ # creates a new Region object
13
+ def initialize(rid)
14
+ @rid = rid
15
+ end
16
+
17
+ # converts the WCAPI unique identifier for this type into a Nanowrimo::Core-friendly 'id'
18
+ def id
19
+ @rid
20
+ end
21
+
22
+ # converts the WCAPI path for this type into something Nanowrimo::Core-friendly
23
+ def load_field
24
+ 'wcregion'
25
+ end
26
+
27
+ # converts the WCAPI history path for this type into something Nanowrimo::Core-friendly
28
+ def load_history_field
29
+ 'wcregionhist/wordcounts/wcentry'
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,27 @@
1
+ #! /usr/bin/env ruby -w
2
+
3
+ module Nanowrimo
4
+ # Handles Nanowrimo Site data.
5
+ class Site < Core
6
+ # fields expected from the main Site WCAPI
7
+ FIELDS = %w[site_wordcount max min stddev average count]
8
+ # fields expected from the Site history WCAPI
9
+ HISTORY_FIELDS = %w[wc wcdate max min stddev average count]
10
+ attr_accessor(*FIELDS)
11
+ attr_accessor :history
12
+ # converts the WCAPI unique identifier for this type into a Nanowrimo::Core-friendly 'id'
13
+ def id
14
+ nil
15
+ end
16
+
17
+ # converts the WCAPI path for this type into something Nanowrimo::Core-friendly
18
+ def load_field
19
+ 'wcstatssummary'
20
+ end
21
+
22
+ # converts the WCAPI history path for this type into something Nanowrimo::Core-friendly
23
+ def load_history_field
24
+ 'wcstats/wordcounts/wcentry'
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,69 @@
1
+ #! /usr/bin/env ruby -w
2
+
3
+ module Nanowrimo
4
+ # Handles Nanowrimo User data.
5
+ class User < Core
6
+ # fields expected from the main User WCAPI
7
+ FIELDS = %w[uid uname user_wordcount]
8
+ # history fields expected from the User History WCAPI
9
+ HISTORY_FIELDS = %w[wc wcdate]
10
+ # fields needed to store data ripped from a user's profile page
11
+ USER_FIELDS = %w[rid novel genre buddies]
12
+ # profile page base URI
13
+ PROFILE_URI = "http://www.nanowrimo.org/eng/user"
14
+
15
+ attr_accessor(*FIELDS)
16
+ attr_accessor(*USER_FIELDS)
17
+ attr_accessor :history, :profile_data
18
+ # creates a new User object
19
+ def initialize uid
20
+ @uid = uid
21
+ @novel = {}
22
+ @genre = {}
23
+ @buddies = []
24
+ end
25
+
26
+ # converts the WCAPI 'uid' into a Nanowrimo::Core-friendly 'id'
27
+ def id
28
+ @uid
29
+ end
30
+
31
+ # converts the WCAPI path for this type into something Nanowrimo::Core-friendly
32
+ def load_field
33
+ 'wc'
34
+ end
35
+
36
+ # converts the WCAPI history path for this type into something Nanowrimo::Core-friendly
37
+ def load_history_field
38
+ 'wchistory/wordcounts/wcentry'
39
+ end
40
+
41
+ # Determines if the User's current wordcount meets the month's goal.
42
+ def winner?
43
+ self.user_wordcount.to_i >= Nanowrimo::GOAL
44
+ end
45
+
46
+ # Method to pull down a WWW::Mechanize::Page instance of the User's profile page
47
+ def load_profile_data
48
+ # mechanize might be overkill, but at some point if they don't add more to the API
49
+ # I'll have to dig deeper behind the site's authentication layer in order to pull out
50
+ # some needed data.
51
+ agent = WWW::Mechanize.new
52
+ @profile_data = agent.get("#{PROFILE_URI}/#{@uid}")
53
+ end
54
+
55
+ # Parses the profile page data pulling out extra information for the User.
56
+ def parse_profile
57
+ load_profile_data
58
+ # get the buddies
59
+ @buddies = @profile_data.search("div[@class='buddies']//a").map{|b| b['href'].split('/').last; }
60
+ @buddies.reject!{|b| b.to_i == 0}
61
+ # title and genre are in the same element
62
+ titlegenre = @profile_data.search("div[@class='titlegenre']").text.split("Genre:")
63
+ @genre[:name] = titlegenre.last.strip
64
+ @novel[:title] = titlegenre.first.gsub('Novel:','').strip
65
+ # finally, the region is annoying to grab
66
+ @rid = @profile_data.search("div[@class='infoleft']//a").first['href'].split('/').last
67
+ end
68
+ end
69
+ end