nanowrimo 0.7

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.
@@ -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