nanowrimo 0.7
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +22 -0
- data/Manifest.txt +27 -0
- data/README.txt +82 -0
- data/Rakefile +13 -0
- data/lib/nanowrimo.rb +76 -0
- data/lib/nanowrimo/cache.rb +52 -0
- data/lib/nanowrimo/core.rb +19 -0
- data/lib/nanowrimo/genre.rb +32 -0
- data/lib/nanowrimo/region.rb +32 -0
- data/lib/nanowrimo/site.rb +27 -0
- data/lib/nanowrimo/user.rb +69 -0
- data/test/fixtures/genre_wc.xml +22 -0
- data/test/fixtures/genre_wc_history.xml +23 -0
- data/test/fixtures/region_wc.xml +26 -0
- data/test/fixtures/region_wc_history.xml +355 -0
- data/test/fixtures/site_wc.xml +18 -0
- data/test/fixtures/site_wc_history.xml +289 -0
- data/test/fixtures/user_page.htm +399 -0
- data/test/fixtures/user_wc.xml +12 -0
- data/test/fixtures/user_wc_history.xml +138 -0
- data/test/test_cache.rb +49 -0
- data/test/test_core.rb +16 -0
- data/test/test_genre.rb +42 -0
- data/test/test_nanowrimo.rb +109 -0
- data/test/test_region.rb +39 -0
- data/test/test_site.rb +39 -0
- data/test/test_user.rb +99 -0
- metadata +102 -0
data/History.txt
ADDED
@@ -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
|
+
|
data/Manifest.txt
ADDED
@@ -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
|
data/README.txt
ADDED
@@ -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.
|
data/Rakefile
ADDED
@@ -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
|
data/lib/nanowrimo.rb
ADDED
@@ -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
|