Merge lp:~robru/friends/streams into lp:friends

Proposed by Robert Bruce Park
Status: Merged
Merged at revision: 2
Proposed branch: lp:~robru/friends/streams
Merge into: lp:friends
Diff against target: 129 lines (+14/-14)
3 files modified
friends/protocols/twitter.py (+6/-6)
friends/tests/test_identica.py (+3/-3)
friends/tests/test_twitter.py (+5/-5)
To merge this branch: bzr merge lp:~robru/friends/streams
Reviewer Review Type Date Requested Status
Ken VanDine Approve
Barry Warsaw Pending
Review via email: mp+129544@code.launchpad.net

Description of the change

Hey barry, it occurred to me that we are just dumping all of our messages into the 'messages' stream, regardless of what type of message it really is, and this makes it kinda difficult for GUI frontends to distinguish between different types of messages.

So what I've done here is enabled a couple of streams, named 'private' and 'search/*'. The private one is for holding private messages, and then any search will create a new 'search/[query]' stream. This thus makes it possible for a frontend to have a new tab for each search, because before, any search results would just be dumped directly into the messages stream, and show up intermingled with normal messages.

In the process of implementing this, I noticed that Twitter is the only protocol for which we've implemented searching, so implementing search on the other protocols should probably be a pretty high priority in the coming week. ;-)

To post a comment you must log in.
Revision history for this message
Ken VanDine (ken-vandine) wrote :

Looks good and yay tests!

review: Approve
Revision history for this message
Barry Warsaw (barry) wrote :

I see that Ken merged already, which is great.

I have just a few questions, but it doesn't necessary mean anything needs to
change.

=== modified file 'friends/protocols/twitter.py'
--- friends/protocols/twitter.py 2012-10-12 19:36:46 +0000
+++ friends/protocols/twitter.py 2012-10-12 23:14:17 +0000
> @@ -291,7 +291,7 @@
>
> response = self._get_url('{}?q={}'.format(url, quote(query, safe='')))
> for tweet in response.get(self._search_result_key, []):
> - self._publish_tweet(tweet)
> + self._publish_tweet(tweet, stream='search/{}'.format(query))
>
>
> class RateLimiter(BaseRateLimiter):

This means that the entire search string will be included in the stream name.
Could this cause any problems for database queries? Do we assign any
additional semantics to the contents of the 'stream' column? Or put it
another way, do we treat the stream column as opaque, or do we assign some
semantics to it?

If the contents are opaque, I think that's fine, otherwise we should at least
document the basic semantics of its value. For example, in this case if a
client wanted to know what the search term was, it would have to do a single
left split on slash, e.g.:

    search, slash, query = search_column.partition('/')
    if slash != '/':
        # No separator

This means of course that the query itself could have a slash in it, but only
if we document that just the left-most slash separates the stream name from
the query term.

We're also assuming of course that there are no restrictions on the values of
string columns in Dee.SharedModel. Is that the case?

Revision history for this message
Robert Bruce Park (robru) wrote :

On 12-10-15 12:46 PM, Barry Warsaw wrote:
> === modified file 'friends/protocols/twitter.py'
> --- friends/protocols/twitter.py 2012-10-12 19:36:46 +0000
> +++ friends/protocols/twitter.py 2012-10-12 23:14:17 +0000
>> @@ -291,7 +291,7 @@
>>
>> response = self._get_url('{}?q={}'.format(url, quote(query, safe='')))
>> for tweet in response.get(self._search_result_key, []):
>> - self._publish_tweet(tweet)
>> + self._publish_tweet(tweet, stream='search/{}'.format(query))
>>
>>
>> class RateLimiter(BaseRateLimiter):
>
> This means that the entire search string will be included in the stream name.

Yep. The idea was that Gwibber's frontend UI (or any other frontend)
would have different tabs for different streams. Or if not 'tabs' there
would at least be some visual distinction between "normal messages" and
"search results", so the 'streams' column is just a technical way for
the frontend to distinguish the messages.

> Could this cause any problems for database queries? Do we assign any
> additional semantics to the contents of the 'stream' column? Or put it
> another way, do we treat the stream column as opaque, or do we assign some
> semantics to it?

I'm not sure what kind of semantics you mean, but in my mind the stream
column is simply an identifier that helps the frontends distinguish
different types of messages.

> If the contents are opaque, I think that's fine, otherwise we should at least
> document the basic semantics of its value. For example, in this case if a
> client wanted to know what the search term was, it would have to do a single
> left split on slash, e.g.:
>
> search, slash, query = search_column.partition('/')
> if slash != '/':
> # No separator
>
> This means of course that the query itself could have a slash in it, but only
> if we document that just the left-most slash separates the stream name from
> the query term.

Right. I wasn't anticipating that the clients would need to *parse* the
stream column, I was thinking more along the lines that the client would
initiate a search, and then the client would construct it's own
'search/query' string in order to filter the sharedmodel based on that
value, eg, for the purposes of displaying a tab that contains only the
search results, not the entire model.

> We're also assuming of course that there are no restrictions on the values of
> string columns in Dee.SharedModel. Is that the case?

I assume so. Maybe Ken could comment?

Revision history for this message
Barry Warsaw (barry) wrote :

On Oct 16, 2012, at 03:13 AM, Robert Bruce Park wrote:

>Right. I wasn't anticipating that the clients would need to *parse* the
>stream column, I was thinking more along the lines that the client would
>initiate a search, and then the client would construct it's own
>'search/query' string in order to filter the sharedmodel based on that
>value, eg, for the purposes of displaying a tab that contains only the
>search results, not the entire model.

That's actually kind of nice. The only trick is that the clients and backend
will have to agree on how those stream names are constructed, which, using the
current concatenation algorithm would be pretty simple. It gets a little more
complicated if there are restrictions on the column values such that they'd
have to be encoded somehow.

I'd actually be kind of surprised if that were the case though.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'friends/protocols/twitter.py'
2--- friends/protocols/twitter.py 2012-10-13 01:27:15 +0000
3+++ friends/protocols/twitter.py 2012-10-13 01:38:18 +0000
4@@ -98,7 +98,7 @@
5 return get_json(url, params=data, headers=headers, method=method,
6 rate_limiter=self._rate_limiter)
7
8- def _publish_tweet(self, tweet):
9+ def _publish_tweet(self, tweet, stream='messages'):
10 """Publish a single tweet into the Dee.SharedModel."""
11 tweet_id = tweet.get('id_str')
12 if tweet_id is None:
13@@ -111,7 +111,7 @@
14 message_id=tweet_id,
15 message=tweet.get('text', ''),
16 timestamp=iso8601utc(parsetime(tweet.get('created_at', ''))),
17- stream='messages',
18+ stream=stream,
19 sender=user.get('name', ''),
20 sender_nick=screen_name,
21 from_me=(screen_name == self._account.user_name),
22@@ -172,11 +172,11 @@
23 """Gather the direct messages sent to/from us."""
24 url = self._api_base.format(endpoint='direct_messages')
25 for tweet in self._get_url(url):
26- self._publish_tweet(tweet)
27+ self._publish_tweet(tweet, stream='private')
28
29 url = self._api_base.format(endpoint='direct_messages/sent')
30 for tweet in self._get_url(url):
31- self._publish_tweet(tweet)
32+ self._publish_tweet(tweet, stream='private')
33
34 @feature
35 def receive(self):
36@@ -205,7 +205,7 @@
37 except HTTPError as error:
38 log.error('{}: Does that user follow you?'.format(error))
39 else:
40- self._publish_tweet(tweet)
41+ self._publish_tweet(tweet, stream='private')
42
43 # https://dev.twitter.com/docs/api/1.1/post/statuses/update
44 @feature
45@@ -291,7 +291,7 @@
46
47 response = self._get_url('{}?q={}'.format(url, quote(query, safe='')))
48 for tweet in response.get(self._search_result_key, []):
49- self._publish_tweet(tweet)
50+ self._publish_tweet(tweet, stream='search/{}'.format(query))
51
52
53 class RateLimiter(BaseRateLimiter):
54
55=== modified file 'friends/tests/test_identica.py'
56--- friends/tests/test_identica.py 2012-10-13 01:27:15 +0000
57+++ friends/tests/test_identica.py 2012-10-13 01:38:18 +0000
58@@ -117,7 +117,7 @@
59
60 self.protocol.private()
61
62- publish.assert_called_with('tweet')
63+ publish.assert_called_with('tweet', stream='private')
64 self.assertEqual(
65 get_url.mock_calls,
66 [mock.call('http://identi.ca/api/direct_messages.json'),
67@@ -129,7 +129,7 @@
68
69 self.protocol.send_private('pumpichank', 'Are you mocking me?')
70
71- publish.assert_called_with('tweet')
72+ publish.assert_called_with('tweet', stream='private')
73 get_url.assert_called_with(
74 'http://identi.ca/api/direct_messages/new.json',
75 dict(text='Are you mocking me?', screen_name='pumpichank'))
76@@ -221,6 +221,6 @@
77
78 self.protocol.search('hello')
79
80- publish.assert_called_with('tweet')
81+ publish.assert_called_with('tweet', stream='search/hello')
82 get_url.assert_called_with(
83 'http://identi.ca/api/search.json?q=hello')
84
85=== modified file 'friends/tests/test_twitter.py'
86--- friends/tests/test_twitter.py 2012-10-13 01:27:15 +0000
87+++ friends/tests/test_twitter.py 2012-10-13 01:38:18 +0000
88@@ -236,7 +236,7 @@
89
90 self.protocol.private()
91
92- publish.assert_called_with('tweet')
93+ publish.assert_called_with('tweet', stream='private')
94 self.assertEqual(
95 get_url.mock_calls,
96 [mock.call('https://api.twitter.com/1.1/direct_messages.json'),
97@@ -249,7 +249,7 @@
98
99 self.protocol.send_private('pumpichank', 'Are you mocking me?')
100
101- publish.assert_called_with('tweet')
102+ publish.assert_called_with('tweet', stream='private')
103 get_url.assert_called_with(
104 'https://api.twitter.com/1.1/direct_messages/new.json',
105 dict(text='Are you mocking me?', screen_name='pumpichank'))
106@@ -356,13 +356,13 @@
107
108 self.protocol.tag('yegbike')
109
110- publish.assert_called_with('tweet')
111+ publish.assert_called_with('tweet', stream='search/#yegbike')
112 get_url.assert_called_with(
113 'https://api.twitter.com/1.1/search/tweets.json?q=%23yegbike')
114
115 self.protocol.tag('#yegbike')
116
117- publish.assert_called_with('tweet')
118+ publish.assert_called_with('tweet', stream='search/#yegbike')
119 get_url.assert_called_with(
120 'https://api.twitter.com/1.1/search/tweets.json?q=%23yegbike')
121
122@@ -373,7 +373,7 @@
123
124 self.protocol.search('hello')
125
126- publish.assert_called_with('tweet')
127+ publish.assert_called_with('tweet', stream='search/hello')
128 get_url.assert_called_with(
129 'https://api.twitter.com/1.1/search/tweets.json?q=hello')
130

Subscribers

People subscribed via source and target branches