partigirb 0.2.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,160 @@
1
+ module Partigirb
2
+
3
+ class Response #:nodoc:
4
+ attr_accessor :method, :request_uri, :status, :body
5
+
6
+ def initialize(method,request_uri,status,body)
7
+ self.method = method
8
+ self.request_uri = request_uri
9
+ self.status = status
10
+ self.body = body
11
+ end
12
+ end
13
+
14
+ class Transport
15
+
16
+ attr_accessor :debug
17
+
18
+ CRLF = "\r\n"
19
+
20
+ def req_class(method)
21
+ case method
22
+ when :get then Net::HTTP::Get
23
+ when :post then Net::HTTP::Post
24
+ when :put then Net::HTTP::Put
25
+ when :delete then Net::HTTP::Delete
26
+ end
27
+ end
28
+
29
+ # Options are one of
30
+ # - :params - a hash of parameters to be sent with the request. If a File is a parameter value, \
31
+ # a multipart request will be sent. If a Time is included, .httpdate will be called on it.
32
+ # - :headers - a hash of headers to send with the request
33
+ def request(method, string_url, options={})
34
+ params = stringify_params(options[:params])
35
+ if method == :get && params
36
+ string_url << query_string(params)
37
+ end
38
+ url = URI.parse(string_url)
39
+ begin
40
+ execute_request(method,url,options)
41
+ rescue Timeout::Error
42
+ raise "Timeout while #{method}ing #{url.to_s}"
43
+ end
44
+ end
45
+
46
+ def execute_request(method,url,options={})
47
+ conn = Net::HTTP.new(url.host, url.port)
48
+ #conn.use_ssl = (url.scheme == 'https')
49
+ conn.start do |http|
50
+ req = req_class(method).new(url.request_uri)
51
+
52
+ add_headers(req,options[:headers])
53
+ if file_param?(options[:params])
54
+ add_multipart_data(req,options[:params])
55
+ else
56
+ add_form_data(req,options[:params])
57
+ end
58
+
59
+ dump_request(req) if debug
60
+ res = http.request(req)
61
+ dump_response(res) if debug
62
+ res
63
+ end
64
+ end
65
+
66
+ def query_string(params)
67
+ query = case params
68
+ when Hash then params.map{|key,value| url_encode_param(key,value) }.join("&")
69
+ else url_encode(params.to_s)
70
+ end
71
+ if !(query == nil || query.length == 0) && query[0,1] != '?'
72
+ query = "?#{query}"
73
+ end
74
+ query
75
+ end
76
+
77
+ private
78
+ def stringify_params(params)
79
+ return nil unless params
80
+ params.inject({}) do |h, pair|
81
+ key, value = pair
82
+ if value.respond_to? :httpdate
83
+ value = value.httpdate
84
+ end
85
+ h[key] = value
86
+ h
87
+ end
88
+ end
89
+
90
+ def file_param?(params)
91
+ return false unless params
92
+ params.any? {|key,value| value.respond_to? :read }
93
+ end
94
+
95
+ def url_encode(value)
96
+ require 'cgi' unless defined?(CGI) && defined?(CGI::escape)
97
+ CGI.escape(value.to_s)
98
+ end
99
+
100
+ def url_encode_param(key,value)
101
+ "#{url_encode(key)}=#{url_encode(value)}"
102
+ end
103
+
104
+ def add_headers(req,headers)
105
+ if headers
106
+ headers.each do |header, value|
107
+ req[header] = value
108
+ end
109
+ end
110
+ end
111
+
112
+ def add_form_data(req,params)
113
+ if req.request_body_permitted? && params
114
+ req.set_form_data(params)
115
+ end
116
+ end
117
+
118
+ def add_multipart_data(req,params)
119
+ boundary = Time.now.to_i.to_s(16)
120
+ req["Content-Type"] = "multipart/form-data; boundary=#{boundary}"
121
+ body = ""
122
+ params.each do |key,value|
123
+ esc_key = url_encode(key)
124
+ body << "--#{boundary}#{CRLF}"
125
+ if value.respond_to?(:read)
126
+ mime_type = MIME::Types.type_for(value.path)[0] || MIME::Types["application/octet-stream"][0]
127
+ body << "Content-Disposition: form-data; name=\"#{esc_key}\"; filename=\"#{File.basename(value.path)}\"#{CRLF}"
128
+ body << "Content-Type: #{mime_type.simplified}#{CRLF*2}"
129
+ body << value.read
130
+ else
131
+ body << "Content-Disposition: form-data; name=\"#{esc_key}\"#{CRLF*2}#{value}"
132
+ end
133
+ body << CRLF
134
+ end
135
+ body << "--#{boundary}--#{CRLF*2}"
136
+ req.body = body
137
+ req["Content-Length"] = req.body.size
138
+ end
139
+
140
+ private
141
+
142
+ def dump_request(req)
143
+ puts "Sending Request"
144
+ puts"#{req.method} #{req.path}"
145
+ dump_headers(req)
146
+ end
147
+
148
+ def dump_response(res)
149
+ puts "Received Response"
150
+ dump_headers(res)
151
+ puts res.body
152
+ end
153
+
154
+ def dump_headers(msg)
155
+ msg.each_header do |key, value|
156
+ puts "\t#{key}=#{value}"
157
+ end
158
+ end
159
+ end
160
+ end
data/lib/partigirb.rb ADDED
@@ -0,0 +1,27 @@
1
+ module Partigirb
2
+ VERSION='0.1.0'
3
+ CURRENT_API_VERSION=1
4
+ end
5
+
6
+ $:.unshift File.dirname(__FILE__)
7
+
8
+ require 'rubygems'
9
+
10
+ require 'open-uri'
11
+ require 'net/http'
12
+ require 'base64'
13
+ require 'digest'
14
+ require 'rexml/document'
15
+ require 'mime/types'
16
+ require 'ostruct'
17
+
18
+ require 'partigirb/core_ext'
19
+
20
+ require 'partigirb/handlers/xml_handler'
21
+ require 'partigirb/handlers/atom_handler'
22
+ require 'partigirb/handlers/json_handler'
23
+ require 'partigirb/handlers/string_handler'
24
+
25
+ require 'partigirb/transport'
26
+ require 'partigirb/client'
27
+
data/partigirb.gemspec ADDED
@@ -0,0 +1,76 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{partigirb}
8
+ s.version = "0.2.7"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Alvaro Bautista", "Fernando Blat"]
12
+ s.date = %q{2009-10-28}
13
+ s.email = ["alvarobp@gmail.com", "ferblape@gmail.com"]
14
+ s.extra_rdoc_files = [
15
+ "LICENSE",
16
+ "README.markdown"
17
+ ]
18
+ s.files = [
19
+ ".gitignore",
20
+ "LICENSE",
21
+ "README.markdown",
22
+ "Rakefile",
23
+ "VERSION",
24
+ "examples/last_reviews_summary.rb",
25
+ "examples/who_ignores_me.rb",
26
+ "lib/partigirb.rb",
27
+ "lib/partigirb/client.rb",
28
+ "lib/partigirb/core_ext.rb",
29
+ "lib/partigirb/handlers/atom_handler.rb",
30
+ "lib/partigirb/handlers/json_handler.rb",
31
+ "lib/partigirb/handlers/string_handler.rb",
32
+ "lib/partigirb/handlers/xml_handler.rb",
33
+ "lib/partigirb/transport.rb",
34
+ "partigirb.gemspec",
35
+ "test/atom_handler_test.rb",
36
+ "test/client_test.rb",
37
+ "test/fixtures/alvaro_friends.atom.xml",
38
+ "test/fixtures/pulp_fiction.atom.xml",
39
+ "test/json_handler_test.rb",
40
+ "test/mocks/net_http_mock.rb",
41
+ "test/mocks/response_mock.rb",
42
+ "test/mocks/transport_mock.rb",
43
+ "test/test_helper.rb",
44
+ "test/transport_test.rb",
45
+ "test/xml_handler_test.rb"
46
+ ]
47
+ s.homepage = %q{http://github.com/partigi/partigirb}
48
+ s.rdoc_options = ["--charset=UTF-8"]
49
+ s.require_paths = ["lib"]
50
+ s.rubygems_version = %q{1.3.5}
51
+ s.summary = %q{A Ruby wrapper for the Partigi API}
52
+ s.test_files = [
53
+ "test/atom_handler_test.rb",
54
+ "test/client_test.rb",
55
+ "test/json_handler_test.rb",
56
+ "test/mocks/net_http_mock.rb",
57
+ "test/mocks/response_mock.rb",
58
+ "test/mocks/transport_mock.rb",
59
+ "test/test_helper.rb",
60
+ "test/transport_test.rb",
61
+ "test/xml_handler_test.rb",
62
+ "examples/last_reviews_summary.rb",
63
+ "examples/who_ignores_me.rb"
64
+ ]
65
+
66
+ if s.respond_to? :specification_version then
67
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
68
+ s.specification_version = 3
69
+
70
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
71
+ else
72
+ end
73
+ else
74
+ end
75
+ end
76
+
@@ -0,0 +1,61 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ class AtomHandlerTest < Test::Unit::TestCase
4
+ def setup
5
+ @handler = Partigirb::Handlers::AtomHandler.new
6
+ end
7
+
8
+ should "return a PartigiStruct with entry elements for feeds with just one entry" do
9
+ xmls = build_xml_string do |xml|
10
+ xml.instruct!
11
+
12
+ xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do
13
+ xml.title "A feed"
14
+ xml.id "http://the.feed.url.com"
15
+ xml.updated "2009-07-24T10:40:22Z"
16
+
17
+ xml.entry({"xmlns:ptUser" => 'http://schemas.partigi.com/v1.0/ptUser'}) do
18
+ xml.category({:scheme => "http://schemas.partigi.com/v1.0#kind", :term => "http://schemas.partigi.com/v1.0#user"})
19
+ xml.id "http://the.feed.url2.com"
20
+ xml.title "User entry"
21
+ xml.ptUser :id, 321
22
+ xml.ptUser :login, "user_login"
23
+ xml.ptUser :name, "User Name"
24
+ end
25
+ end
26
+ end
27
+
28
+ res = @handler.decode_response(xmls)
29
+ assert res.is_a?(Partigirb::PartigiStruct)
30
+
31
+ assert_equal "User entry", res.title
32
+ assert_equal "http://the.feed.url2.com", res.id
33
+ assert_equal 321, res.ptUser_id
34
+ assert_equal "user_login", res.ptUser_login
35
+ assert_equal "User Name", res.ptUser_name
36
+ assert_nil res.updated
37
+ end
38
+
39
+ should "return an array of PartigiStruct with PartigiStruct of each entry in the feed" do
40
+ xmls = build_xml_string do |xml|
41
+ xml.instruct!
42
+
43
+ xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do
44
+ xml.title "A feed"
45
+ xml.entry({"xmlns:ptUser" => 'http://schemas.partigi.com/v1.0/ptUser'}) do
46
+ xml.ptUser :id, 321
47
+ end
48
+ xml.entry({"xmlns:ptUser" => 'http://schemas.partigi.com/v1.0/ptUser'}) do
49
+ xml.ptUser :id, 123
50
+ end
51
+ end
52
+ end
53
+
54
+ res = @handler.decode_response(xmls)
55
+ assert res.is_a?(Array)
56
+ assert_equal 2, res.size
57
+
58
+ assert_equal 321, res[0].ptUser_id
59
+ assert_equal 123, res[1].ptUser_id
60
+ end
61
+ end
@@ -0,0 +1,209 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ class ClientTest < Test::Unit::TestCase
4
+ def setup
5
+ @client = new_client
6
+ end
7
+
8
+ should "use GET method by default" do
9
+ @client.users.show.xml
10
+
11
+ assert_equal(:get, @client.transport.method)
12
+ end
13
+
14
+ should "use GET method for methods ending with ?" do
15
+ @client.users.show.xml?
16
+
17
+ assert_equal(:get, @client.transport.method)
18
+ end
19
+
20
+ should "not make a request if format is missing" do
21
+ MockTransport.any_instance.expects(:request).never
22
+
23
+ @client.users.show
24
+
25
+ assert_nil @client.transport.url
26
+ end
27
+
28
+ should "use POST method for methods ending with !" do
29
+ @client.users.show.xml!
30
+
31
+ assert_equal(:post, @client.transport.method)
32
+ end
33
+
34
+ should "use POST method and default format for methods ending with !" do
35
+ @client.friendships.create!
36
+
37
+ assert_equal(:post, @client.transport.method)
38
+ assert @client.transport.url.path.include?("/friendships/create.atom")
39
+ end
40
+
41
+ should "request to the Partigi API host through HTTP protocol" do
42
+ @client.users.show.xml
43
+
44
+ assert_equal('http', @client.transport.url.scheme)
45
+ assert_equal(Partigirb::Client::PARTIGI_API_HOST, @client.transport.url.host)
46
+ end
47
+
48
+ should "use the current API version by default" do
49
+ @client.users.show.xml
50
+
51
+ assert_equal(Partigirb::CURRENT_API_VERSION, @client.api_version)
52
+ end
53
+
54
+ should "put the requested API version in the request url" do
55
+ @client = new_client(200, '', {:api_version => 3})
56
+ @client.users.show.xml
57
+
58
+ assert_equal '/api/v3/users/show.xml', @client.transport.url.path
59
+ end
60
+
61
+ should "add paremeters to url on GET" do
62
+ @client.items.index.xml :type => 'film', :page => 1, :per_page => 20
63
+
64
+ assert_equal 3, request_query.size
65
+ assert request_query.include?('type=film')
66
+ assert request_query.include?('page=1')
67
+ assert request_query.include?('per_page=20')
68
+ end
69
+
70
+ should "add parameters to request body on POST" do
71
+ @client.reviews.create! :item_id => 'star-wars', :type => 'film', :status => 2, :text => 'My favorite movie', :rating => '5'
72
+
73
+ assert_equal 5, post_data.size
74
+ assert post_data.include?('item_id=star-wars')
75
+ assert post_data.include?('type=film')
76
+ assert post_data.include?('status=2')
77
+ assert post_data.include?('text=My%20favorite%20movie')
78
+ assert post_data.include?('rating=5')
79
+ end
80
+
81
+ should "add any headers to the HTTP requests" do
82
+ @client = new_client(200, '', {:headers => {'Useless' => 'void', 'Fake' => 'header'}})
83
+ @client.user.show.xml
84
+
85
+ assert_not_nil Net::HTTP.request['Useless']
86
+ assert_equal 'void', Net::HTTP.request['Useless']
87
+ assert_not_nil Net::HTTP.request['Fake']
88
+ assert_equal 'header', Net::HTTP.request['Fake']
89
+ end
90
+
91
+ should "add authentication headers when login and secret are provided" do
92
+ @client = new_client(200, '', :auth => {:login => 'auser', :api_secret => 'his_api_secret'})
93
+
94
+ @client.friendships.update! :id => 321
95
+
96
+ assert_not_nil Net::HTTP.request['Authorization']
97
+ assert_equal "WSSE realm=\"#{Partigirb::Client::PARTIGI_API_HOST}\", profile=\"UsernameToken\"", Net::HTTP.request['Authorization']
98
+
99
+ assert_not_nil Net::HTTP.request['X-WSSE']
100
+ assert_match /UsernameToken Username="auser", PasswordDigest="[^"]+", Nonce="[^"]+", Created="[^"]+"/, Net::HTTP.request['X-WSSE']
101
+ end
102
+
103
+ should "not add authentication headers if no auth params are provided" do
104
+ @client.friendships.update! :id => 321
105
+
106
+ assert_nil Net::HTTP.request['Authorization']
107
+ assert_nil Net::HTTP.request['X-WSSE']
108
+ end
109
+
110
+ should "use given nonce for authentication" do
111
+ @client = new_client(200, '', :auth => {:login => 'auser', :api_secret => 'his_api_secret', :nonce => '123456789101112'})
112
+ @client.friendships.update! :id => 321
113
+
114
+ assert_equal "WSSE realm=\"#{Partigirb::Client::PARTIGI_API_HOST}\", profile=\"UsernameToken\"", Net::HTTP.request['Authorization']
115
+ assert_match /UsernameToken Username="auser", PasswordDigest="[^"]+", Nonce="123456789101112", Created="[^"]+"/, Net::HTTP.request['X-WSSE']
116
+ end
117
+
118
+ should "use given timestamp string for authentication" do
119
+ @client = new_client(200, '', :auth => {:login => 'auser', :api_secret => 'his_api_secret', :timestamp => '2009-07-15T14:43:07Z'})
120
+ @client.friendships.update! :id => 321
121
+
122
+ assert_equal "WSSE realm=\"#{Partigirb::Client::PARTIGI_API_HOST}\", profile=\"UsernameToken\"", Net::HTTP.request['Authorization']
123
+ assert_match /UsernameToken Username="auser", PasswordDigest="[^"]+", Nonce="[^"]+", Created="2009-07-15T14:43:07Z"/, Net::HTTP.request['X-WSSE']
124
+ end
125
+
126
+ should "use given Time object as timestamp for authentication" do
127
+ timestamp = Time.now
128
+ @client = new_client(200, '', :auth => {:login => 'auser', :api_secret => 'his_api_secret', :timestamp => timestamp})
129
+ @client.friendships.update! :id => 321
130
+
131
+ assert_equal "WSSE realm=\"#{Partigirb::Client::PARTIGI_API_HOST}\", profile=\"UsernameToken\"", Net::HTTP.request['Authorization']
132
+ assert_match /UsernameToken Username="auser", PasswordDigest="[^"]+", Nonce="[^"]+", Created="#{timestamp.strftime(Partigirb::Client::TIMESTAMP_FORMAT)}"/, Net::HTTP.request['X-WSSE']
133
+ end
134
+
135
+ should "use the PasswordDigest from given parameters" do
136
+ @client = new_client(200, '', :auth => {:login => 'auser', :api_secret => 'his_api_secret', :nonce => '123456789101112', :timestamp => '2009-07-15T14:43:07Z'})
137
+ @client.friendships.update! :id => 321
138
+
139
+ pdigest = Base64.encode64(Digest::SHA1.hexdigest("1234567891011122009-07-15T14:43:07Zauserhis_api_secret")).chomp
140
+
141
+ assert_equal "WSSE realm=\"#{Partigirb::Client::PARTIGI_API_HOST}\", profile=\"UsernameToken\"", Net::HTTP.request['Authorization']
142
+ assert_match /UsernameToken Username="auser", PasswordDigest="#{pdigest}", Nonce="123456789101112", Created="2009-07-15T14:43:07Z"/, Net::HTTP.request['X-WSSE']
143
+ end
144
+
145
+ context "generate_nonce method" do
146
+ should "generate random strings" do
147
+ @client.instance_eval do
148
+ nonces = []
149
+ 1.upto(25) do
150
+ assert !nonces.include?(generate_nonce)
151
+ end
152
+ end
153
+ end
154
+ end
155
+
156
+ should "process XML response by XML handler" do
157
+ Partigirb::Handlers::XMLHandler.any_instance.expects(:decode_response).once
158
+ Partigirb::Handlers::AtomHandler.any_instance.expects(:decode_response).never
159
+ Partigirb::Handlers::JSONHandler.any_instance.expects(:decode_response).never
160
+ @client.items.xml
161
+ end
162
+
163
+ should "process Atom response by Atom handler" do
164
+ Partigirb::Handlers::XMLHandler.any_instance.expects(:decode_response).never
165
+ Partigirb::Handlers::AtomHandler.any_instance.expects(:decode_response).once
166
+ Partigirb::Handlers::JSONHandler.any_instance.expects(:decode_response).never
167
+ @client.items.atom
168
+ end
169
+
170
+ should "process JSON response by JSON handler" do
171
+ Partigirb::Handlers::XMLHandler.any_instance.expects(:decode_response).never
172
+ Partigirb::Handlers::AtomHandler.any_instance.expects(:decode_response).never
173
+ Partigirb::Handlers::JSONHandler.any_instance.expects(:decode_response).once
174
+ @client.items.json
175
+ end
176
+
177
+ should "raise a PartigiError with response error text as the message when http response codes are other than 200" do
178
+ client = new_client(400, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<error>Partigi::BadAPIRequestRequiredParams</error>\n")
179
+
180
+ begin
181
+ client.items.show.xml :id => 'madeup'
182
+ rescue Exception => e
183
+ assert e.is_a?(Partigirb::PartigiError)
184
+ assert_equal 'Partigi::BadAPIRequestRequiredParams', e.message
185
+ end
186
+ end
187
+
188
+ # Copied from Grackle
189
+ should "clear the request path on clear" do
190
+ client = new_client(200,'[{"id":1,"text":"test 1"}]')
191
+ client.some.url.that.does.not.exist
192
+ assert_equal('/some/url/that/does/not/exist',client.send(:request).path,"An unexecuted path should be build up")
193
+ client.clear
194
+ assert_equal('',client.send(:request).path,"The path shoudl be cleared")
195
+ end
196
+
197
+ should "use multipart encoding when using a file param" do
198
+ client = new_client(200,'')
199
+ client.account.update_profile_image! :image=>File.new(__FILE__)
200
+ assert_match(/multipart\/form-data/,Net::HTTP.request['Content-Type'])
201
+ end
202
+
203
+ should "escape and encode time param" do
204
+ client = new_client(200,'')
205
+ time = Time.now-60*60
206
+ client.statuses.public_timeline? :since=>time
207
+ assert_equal("/api/v#{Partigirb::CURRENT_API_VERSION}/statuses/public_timeline.atom?since=#{CGI::escape(time.httpdate)}", Net::HTTP.request.path)
208
+ end
209
+ end
@@ -0,0 +1,80 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <feed xmlns="http://www.w3.org/2005/Atom">
3
+ <title>user/7/friends &#187; Partigi</title>
4
+ <id>http://www.partigi.com/api/v1/friends/show/7.atom</id>
5
+ <updated>2009-07-23T10:08:14Z</updated>
6
+ <link type="application/atom+xml" href="http://www.partigi.com/api/v1/friends/show/7.atom" rel="self"/>
7
+ <link type="text/html" href="http://www.partigi.com/alvarobp/friends/follows" rel="alternate"/>
8
+ <author>
9
+ <name>Partigi</name>
10
+ </author>
11
+ <entry xmlns:ptUser="http://schemas.partigi.com/v1.0/ptUser">
12
+ <category scheme="http://schemas.partigi.com/v1.0#kind" term="http://schemas.partigi.com/v1.0#user"/>
13
+ <id>http://www.partigi.com/friend1</id>
14
+ <title>Friend 1</title>
15
+ <updated>2009-07-24T19:16:14Z</updated>
16
+ <published>2009-07-23T10:07:49Z</published>
17
+ <link type="text/html" href="http://www.partigi.com/friend1" rel="alternate"/>
18
+ <link type="application/atom+xml" href="http://www.partigi.com/api/v1/users/show.atom?id=friend1" rel="self"/>
19
+ <ptUser:id>440</ptUser:id>
20
+ <ptUser:login>friend1</ptUser:login>
21
+ <ptUser:name>Friend 1</ptUser:name>
22
+ <ptUser:surname>Surname 1</ptUser:surname>
23
+ <ptUser:country>ES</ptUser:country>
24
+ </entry>
25
+ <entry xmlns:ptUser="http://schemas.partigi.com/v1.0/ptUser">
26
+ <category scheme="http://schemas.partigi.com/v1.0#kind" term="http://schemas.partigi.com/v1.0#user"/>
27
+ <id>http://www.partigi.com/friend2</id>
28
+ <title>Friend 2</title>
29
+ <updated>2009-07-23T10:15:18Z</updated>
30
+ <published>2009-07-03T04:20:09Z</published>
31
+ <link type="text/html" href="http://www.partigi.com/friend2" rel="alternate"/>
32
+ <link type="application/atom+xml" href="http://www.partigi.com/api/v1/users/show.atom?id=friend2" rel="self"/>
33
+ <ptUser:id>357</ptUser:id>
34
+ <ptUser:login>friend2</ptUser:login>
35
+ <ptUser:name>Friend 2</ptUser:name>
36
+ <ptUser:surname>Surname 2</ptUser:surname>
37
+ <ptUser:country>ES</ptUser:country>
38
+ </entry>
39
+ <entry xmlns:ptUser="http://schemas.partigi.com/v1.0/ptUser">
40
+ <category scheme="http://schemas.partigi.com/v1.0#kind" term="http://schemas.partigi.com/v1.0#user"/>
41
+ <id>http://www.partigi.com/friend3</id>
42
+ <title>Friend 3</title>
43
+ <updated>2009-07-18T17:05:42Z</updated>
44
+ <published>2009-07-18T17:04:43Z</published>
45
+ <link type="text/html" href="http://www.partigi.com/friend3" rel="alternate"/>
46
+ <link type="application/atom+xml" href="http://www.partigi.com/api/v1/users/show.atom?id=friend3" rel="self"/>
47
+ <ptUser:id>408</ptUser:id>
48
+ <ptUser:login>friend3</ptUser:login>
49
+ <ptUser:name>Friend 3</ptUser:name>
50
+ <ptUser:surname>Surname 3</ptUser:surname>
51
+ <ptUser:country>ES</ptUser:country>
52
+ </entry>
53
+ <entry xmlns:ptUser="http://schemas.partigi.com/v1.0/ptUser">
54
+ <category scheme="http://schemas.partigi.com/v1.0#kind" term="http://schemas.partigi.com/v1.0#user"/>
55
+ <id>http://www.partigi.com/friend4</id>
56
+ <title>Friend 4</title>
57
+ <updated>2009-07-18T12:35:46Z</updated>
58
+ <published>2009-05-20T13:51:09Z</published>
59
+ <link type="text/html" href="http://www.partigi.com/friend4" rel="alternate"/>
60
+ <link type="application/atom+xml" href="http://www.partigi.com/api/v1/users/show.atom?id=friend4" rel="self"/>
61
+ <ptUser:id>183</ptUser:id>
62
+ <ptUser:login>friend4</ptUser:login>
63
+ <ptUser:name>Friend 4</ptUser:name>
64
+ <ptUser:surname>Surname 4</ptUser:surname>
65
+ </entry>
66
+ <entry xmlns:ptUser="http://schemas.partigi.com/v1.0/ptUser">
67
+ <category scheme="http://schemas.partigi.com/v1.0#kind" term="http://schemas.partigi.com/v1.0#user"/>
68
+ <id>http://www.partigi.com/friend5</id>
69
+ <title>Friend 5</title>
70
+ <updated>2009-07-19T17:27:53Z</updated>
71
+ <published>2009-06-04T11:37:17Z</published>
72
+ <link type="text/html" href="http://www.partigi.com/friend5" rel="alternate"/>
73
+ <link type="application/atom+xml" href="http://www.partigi.com/api/v1/users/show.atom?id=friend5" rel="self"/>
74
+ <ptUser:id>249</ptUser:id>
75
+ <ptUser:login>friend5</ptUser:login>
76
+ <ptUser:name>Friend 5</ptUser:name>
77
+ <ptUser:surname>Surname 5</ptUser:surname>
78
+ <ptUser:country>ES</ptUser:country>
79
+ </entry>
80
+ </feed>