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