Merge lp:~robru/gwibber/dee into lp:~barry/gwibber/py3
- dee
- Merge into py3
Status: | Merged |
---|---|
Merged at revision: | 1433 |
Proposed branch: | lp:~robru/gwibber/dee |
Merge into: | lp:~barry/gwibber/py3 |
Diff against target: |
2162 lines (+327/-1550) 21 files modified
gwibber/gwibber/main.py (+3/-13) gwibber/gwibber/service/dispatcher.py (+2/-2) gwibber/gwibber/service/messages.py (+0/-47) gwibber/gwibber/service/searches.py (+0/-87) gwibber/gwibber/service/streams.py (+0/-93) gwibber/gwibber/storage/messages.py (+0/-172) gwibber/gwibber/storage/searches.py (+0/-63) gwibber/gwibber/storage/streams.py (+0/-68) gwibber/gwibber/testing/com.Gwibber.Messages.service.in (+0/-3) gwibber/gwibber/testing/com.Gwibber.Searches.service.in (+0/-3) gwibber/gwibber/testing/com.Gwibber.Streams.service.in (+0/-3) gwibber/gwibber/testing/dbus.py (+0/-3) gwibber/gwibber/testing/service.py (+1/-48) gwibber/gwibber/tests/test_account.py (+5/-12) gwibber/gwibber/tests/test_dbus.py (+0/-237) gwibber/gwibber/tests/test_model.py (+39/-0) gwibber/gwibber/tests/test_protocols.py (+95/-0) gwibber/gwibber/tests/test_storage.py (+0/-688) gwibber/gwibber/utils/account.py (+7/-6) gwibber/gwibber/utils/base.py (+66/-2) gwibber/gwibber/utils/model.py (+109/-0) |
To merge this branch: | bzr merge lp:~robru/gwibber/dee |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Ken VanDine | Pending | ||
Barry Warsaw | Pending | ||
Review via email: mp+124827@code.launchpad.net |
Commit message
Description of the change
Implement a Dee.SharedModel with a libgwibber-
Note: Before I wrote this code, I had two terminals open running two interactive python sessions, and I was toying around with Dee.SharedModel. I was not actually able to succeed in getting the two different models to share their data... they never synced up and adding a row from one terminal never resulted in the 'row-added' signal being emitted on the other terminal. None of the documentaton I was able to find offered any help. I was thinking there might need to be some kind of 'flush' method to indicate that the data was ready to be sent over dbus, but I couldn't find any such thing.
So, with that utter failure in mind, I set out to write the simplest possible API that would allow our plugins to "publish" the sanitized json into the model, and this is what I came up with. It works internally, but there is probably something missing before libgwibber is actually able to access this over dbus. Gotta check with kenvandine about this before you approve the merge.
The idea behind my API choice is that all protocol actions will call something like "self._
- 1434. By Robert Bruce Park
-
Fix up a copy&paste error. Oops ;-)
- 1435. By Robert Bruce Park
-
Stop hard-coding the number of columns in case the schema changes later.
Robert Bruce Park (robru) wrote : | # |
- 1436. By Robert Bruce Park
-
Rename our SharedModel bus name from com.Gwibber.
SharedModel to com.Gwibber. Streams. This name is more semantically meaningful, rather than being named
after an implementation detail. - 1437. By Robert Bruce Park
-
By my estimation, this is all the code that is obsoleted by Dee.SharedModel.
- 1438. By Robert Bruce Park
-
Test that we can get the message back from the SharedModel, too.
- 1439. By Robert Bruce Park
-
Schema fixup, plus working de-duplication logic.
The de-dup logic is modelled very closely on the way libgwibber does
it in Vala, but now we're doing it right at the source before
libgwibber even gets there. Three cheers for efficiency! - 1440. By Robert Bruce Park
-
Tweak the return value logic in Base._publish.
Also stop hard-coding column index numbers.
- 1441. By Robert Bruce Park
-
Add a publish_lock to avoid race conditions.
We are checking for duplicates first, and then appending new rows later,
which creates a race condition in between (eg, some other thread can easily
insert a duplicate row after we find that there are no duplicates present, but
before we get around to appending our own row).This makes duplicate checking / row appends atomic.
- 1442. By Robert Bruce Park
-
Looks like I didn't rip out enough old crufty stuff last time.
Barry Warsaw (barry) wrote : | # |
I'm going to review the latest and greatest version of the branch.
First of all, this is fantastic! You've done a great job at removing tons of
crufty, legacy, mysterious, horrible old code and replaced it with just a
little bit of good, new, readable code. This is a huge win. Just to show you
how big, here's the diffstat:
service/
service/
service/streams.py | 93 ----
storage/
storage/
storage/streams.py | 68 ---
testing/
testing/
testing/
testing/dbus.py | 3
tests/
tests/
tests/
utils/base.py | 57 ++
utils/model.py | 100 ++++
15 files changed, 231 insertions(+), 1232 deletions(-)
You gotta love it when you remove 5x more code than you add, *and* make it so
much more understandable.
One thing I noticed was lots of skipped tests when running under `make
check_all` or a virtualenv. As we discussed on IRC, you're going to fix
those, and I'll review what you've got so far.
I'll skip the deletions and any uncontroversial new code.
=== modified file 'gwibber/
--- gwibber/
+++ gwibber/
> @@ -27,6 +27,7 @@
> from gwibber.
> from gwibber.utils.base import Base
> from gwibber.
> +from gwibber.utils.model import SharedModel
>
>
> class TestProtocolMan
> @@ -126,6 +127,42 @@
> thread.join()
> self.assertEqua
>
> + def test_publish(self):
> + base = Base(None)
> + with self.assertRais
> + base._publish(
I don't think you need the context manager version of this, since you're not
checking anything about the raised exception. You could possibly check
details about the exception, e.g. the bad column name (more on this later),
but if not, then I think this would be fine:
One other thing we discussed on IRC is that these tests are not well isolated
from the environment. Because these are not running inside TestDBus, they
actually pollute the user's session. Other than the problem that running the
test would show bogus data in the Gwibber ui, the other problem is that the
model doesn't get reset after every test, so I bet it persists between test
runs. Which means it's not clear this is actually testing what you think it's
testing.
The solution is to move all the _publish() tests to TestDBus, since that
ensures it's ru...
Robert Bruce Park (robru) wrote : | # |
On 12-09-18 05:16 PM, Barry Warsaw wrote:
> First of all, this is fantastic! You've done a great job at removing tons of
> crufty, legacy, mysterious, horrible old code and replaced it with just a
> little bit of good, new, readable code. This is a huge win.
Thanks. I can't take all the credit though... somebody had to write all
this terrible code in the first place! Lots of low-hanging fruit on this
project ;-)
> Just to show you how big, here's the diffstat:
[snip]
> 15 files changed, 231 insertions(+), 1232 deletions(-)
This is actually an old diffstat! The final total was actually 1550
lines deleted ;-)
> You gotta love it when you remove 5x more code than you add, *and* make it so
> much more understandable.
Heh. It was 10x more, but then I expanded the tests... ;-)
> One thing I noticed was lots of skipped tests when running under `make
> check_all` or a virtualenv. As we discussed on IRC, you're going to fix
> those, and I'll review what you've got so far.
Yep, that's already fixed and pushed.
>> + def test_publish(self):
>> + base = Base(None)
>> + with self.assertRais
>> + base._publish(
>
> I don't think you need the context manager version of this, since you're not
> checking anything about the raised exception. You could possibly check
> details about the exception, e.g. the bad column name (more on this later),
> but if not, then I think this would be fine:
>
> self.assertRais
Fair. I had forgotten about your proposed syntax and was thinking that
the with statement was the only way to test for exceptions. You can
change this if you feel strongly about it, but I'm indifferent on this
point.
> One other thing we discussed on IRC is that these tests are not well isolated
> from the environment. Because these are not running inside TestDBus, they
> actually pollute the user's session. Other than the problem that running the
> test would show bogus data in the Gwibber ui, the other problem is that the
> model doesn't get reset after every test, so I bet it persists between test
> runs. Which means it's not clear this is actually testing what you think it's
> testing.
Yes, I know. It's not clear to me exactly how this will fit into
TestDBus though, because SharedModel is not like our other DBus classes
where we are defining the classes ourselves and have a lot of control
over them. I have no idea if SharedModel will respect the custom
sessionbus that TestDBus creates, or how to ensure that that is
happening. I was hoping you'd offer a bit more guidance in this aspect.
Also, on some level we need to understand that we don't need to test
SharedModel itself, but just the code that we use to interface with it.
To that end, we might almost be better off leaving it outside of
TestDBus, and simply create a new SharedModel that uses a different bus
name, the mock that in place of the 'real' sharedmodel. At least as far
as testing Base._publish goes, anyway.
I'll play with this some more tonight I suppose.
> The solution is to move all the _publish() tests to TestDBus, sinc...
Robert Bruce Park (robru) wrote : | # |
On 12-09-18 05:16 PM, Barry Warsaw wrote:
> First of all, this is fantastic! You've done a great job at removing tons of
> crufty, legacy, mysterious, horrible old code and replaced it with just a
> little bit of good, new, readable code. This is a huge win.
Thanks. I can't take all the credit though... somebody had to write all
this terrible code in the first place! Lots of low-hanging fruit on this
project ;-)
> Just to show you how big, here's the diffstat:
[snip]
> 15 files changed, 231 insertions(+), 1232 deletions(-)
This is actually an old diffstat! The final total was actually 1550
lines deleted ;-)
> You gotta love it when you remove 5x more code than you add, *and* make it so
> much more understandable.
Heh. It was 10x more, but then I expanded the tests... ;-)
> One thing I noticed was lots of skipped tests when running under `make
> check_all` or a virtualenv. As we discussed on IRC, you're going to fix
> those, and I'll review what you've got so far.
Yep, that's already fixed and pushed.
>> + def test_publish(self):
>> + base = Base(None)
>> + with self.assertRais
>> + base._publish(
>
> I don't think you need the context manager version of this, since you're not
> checking anything about the raised exception. You could possibly check
> details about the exception, e.g. the bad column name (more on this later),
> but if not, then I think this would be fine:
>
> self.assertRais
Fair. I had forgotten about your proposed syntax and was thinking that
the with statement was the only way to test for exceptions. You can
change this if you feel strongly about it, but I'm indifferent on this
point.
> One other thing we discussed on IRC is that these tests are not well isolated
> from the environment. Because these are not running inside TestDBus, they
> actually pollute the user's session. Other than the problem that running the
> test would show bogus data in the Gwibber ui, the other problem is that the
> model doesn't get reset after every test, so I bet it persists between test
> runs. Which means it's not clear this is actually testing what you think it's
> testing.
Yes, I know. It's not clear to me exactly how this will fit into
TestDBus though, because SharedModel is not like our other DBus classes
where we are defining the classes ourselves and have a lot of control
over them. I have no idea if SharedModel will respect the custom
sessionbus that TestDBus creates, or how to ensure that that is
happening. I was hoping you'd offer a bit more guidance in this aspect.
Also, on some level we need to understand that we don't need to test
SharedModel itself, but just the code that we use to interface with it.
To that end, we might almost be better off leaving it outside of
TestDBus, and simply create a new SharedModel that uses a different bus
name, the mock that in place of the 'real' sharedmodel. At least as far
as testing Base._publish goes, anyway.
I'll play with this some more tonight I suppose.
> The solution is to move all the _publish() tests to TestDBus, sinc...
- 1443. By Robert Bruce Park
-
Part one of cleanup based on barry's review.
a) Stop iterating over the SharedModel, because that's stupidly slow
in practise.b) cache the output of strip(), making the performance of _publish
O(1) instead of O(n) - 1444. By Robert Bruce Park
-
Part two of the cleanup based on barry's review.
a) SharedModel is now mocked in the tests so that we are not dumping
test data into the live SharedModel.b) Each test is broken down into more bitesize units for easier
understanding when something goes wrong. - 1445. By Robert Bruce Park
-
Avoid regex in order to improve readability.
Barry Warsaw (barry) wrote : | # |
On Sep 18, 2012, at 11:48 PM, Robert Bruce Park wrote:
>This is actually an old diffstat! The final total was actually 1550
>lines deleted ;-)
Happy happy, joy joy!
>> I don't think you need the context manager version of this, since you're not
>> checking anything about the raised exception. You could possibly check
>> details about the exception, e.g. the bad column name (more on this later),
>> but if not, then I think this would be fine:
>>
>> self.assertRais
>
>Fair. I had forgotten about your proposed syntax and was thinking that
>the with statement was the only way to test for exceptions. You can
>change this if you feel strongly about it, but I'm indifferent on this
>point.
Right, the context manager is really useful if you want to make additional
assertions about the exception that was raised.
For example, let's say you raised some Gwibber-specific exception, and you
stored the set of invalid arguments on an attribute of that exception. You
could add an assertion that the attribute had the set of strings you expected
it to have, and you'd have to use the context manager syntax to do that. I'm
not suggesting this as a change, just illustrating an example (which I know
you understand, just for anyone else following along).
>> One other thing we discussed on IRC is that these tests are not well
>> isolated from the environment. Because these are not running inside
>> TestDBus, they actually pollute the user's session. Other than the problem
>> that running the test would show bogus data in the Gwibber ui, the other
>> problem is that the model doesn't get reset after every test, so I bet it
>> persists between test runs. Which means it's not clear this is actually
>> testing what you think it's testing.
>
>Yes, I know. It's not clear to me exactly how this will fit into
>TestDBus though, because SharedModel is not like our other DBus classes
>where we are defining the classes ourselves and have a lot of control
>over them. I have no idea if SharedModel will respect the custom
>sessionbus that TestDBus creates, or how to ensure that that is
>happening. I was hoping you'd offer a bit more guidance in this aspect.
I don't know a lot about SharedModel yet, but Ken mentioned on IRC that using
a temporary session bus will isolate the model changes. This should be fairly
easy to verify, though the minimal documentation I've found on Dee doesn't
really say. I'd be more worried about any caching polluting the user's
environment. We'll just have to hack on this to make sure.
>Also, on some level we need to understand that we don't need to test
>SharedModel itself, but just the code that we use to interface with it.
>To that end, we might almost be better off leaving it outside of
>TestDBus, and simply create a new SharedModel that uses a different bus
>name, the mock that in place of the 'real' sharedmodel. At least as far
>as testing Base._publish goes, anyway.
It probably does make sense to have two types of tests, one for outside-dbus
which mocks the SharedModel and one inside-dbus which uses the real shared
model on a temporary bus (which TestDBus...
Robert Bruce Park (robru) wrote : | # |
On 12-09-19 10:52 AM, Barry Warsaw wrote:
>> Yes, I know. It's not clear to me exactly how this will fit into
>> TestDBus though, because SharedModel is not like our other DBus classes
>> where we are defining the classes ourselves and have a lot of control
>> over them. I have no idea if SharedModel will respect the custom
>> sessionbus that TestDBus creates, or how to ensure that that is
>> happening. I was hoping you'd offer a bit more guidance in this aspect.
>
> I don't know a lot about SharedModel yet, but Ken mentioned on IRC that using
> a temporary session bus will isolate the model changes. This should be fairly
> easy to verify, though the minimal documentation I've found on Dee doesn't
> really say. I'd be more worried about any caching polluting the user's
> environment. We'll just have to hack on this to make sure.
The testsuite, as it currently stands in the mp, does not pollute the
users environment. The tests that test Base._publish are using a mocked
SharedModel using a different bus name, and there's even a test that
confirms the user's environ is not polluted.
The other test, that tests SharedModel more directly, does operate on
the user's live environment, but it's a read-only test that doesn't do
any polluting. It basically just tests that the right schema is in
place, which is probably good enough, because anything more than that
would become a test against Dee, rather than a test against Gwibber.
>> Is it done in such a way that importing SharedModel will force it to use the
>> custom bus and not the real bus?
>
> Only in the gwibber-service executed by dbus-daemon described above.
>
>> Because SharedModel handles it's own dbus logic internally, I don't see any
>> way to tell SharedModel 'hey, use this other bus instead' unless your
>> TestDBus magic is somehow affecting the dbus module itself in a
>> process-global way.
>
> Yep, it is. `M-x man RET dbus-daemon RET` will provide additional
> information. Essentially, it's dbus-daemon that's doing all the magic that
> the dbus library needs to run gwibber-service on an isolated session bus.
Ok, so importing our SharedModel instance in main.py would result in an
isolated, testable SharedModel. good to know.
> We can keep the regexp.
Funny, because I actually did replace it with a string comprehension,
and the readability did improve incredibly. My performance concerns were
mitigated by a) no longer iterating over the sharedmodel, calling
strip() each time, and b) caching the results of strip(), such that it's
only necessary to call it once for each new message addition. That
effectively upgrades it from O(n) to O(1), which means the slower
implementation won't really be noticable.
>> It turns out that concatenating the sender's name and message text
>> *only* is the correct approach for fuzzy equivalence testing ;-)
>
> No, because it's just that strip() or whatever would pull out row[sender] and
> row[message] and do the concatenation itself. It just moves that logic into
> the method that calculates the unique key from the row data.
Oh, now i see. You can make that change if you insist ;-)
>>> Also, is there no more efficient way of asking ...
- 1446. By Robert Bruce Park
-
Rename some things to indicate that they are private members.
- 1447. By Robert Bruce Park
-
Add a helpful comment.
Preview Diff
1 | === modified file 'gwibber/gwibber/main.py' |
2 | --- gwibber/gwibber/main.py 2012-09-13 18:04:03 +0000 |
3 | +++ gwibber/gwibber/main.py 2012-09-19 17:22:19 +0000 |
4 | @@ -31,10 +31,7 @@ |
5 | |
6 | from gwibber.service.connection import ConnectionMonitor |
7 | from gwibber.service.dispatcher import Dispatcher |
8 | -from gwibber.service.messages import MessageManager |
9 | -from gwibber.service.searches import SearchManager |
10 | from gwibber.service.shortener import URLShorten |
11 | -from gwibber.service.streams import StreamManager |
12 | from gwibber.testing.service import TestService |
13 | from gwibber.utils.logging import initialize |
14 | from gwibber.utils.menus import MenuManager |
15 | @@ -66,11 +63,7 @@ |
16 | debug=args.debug or gsettings.get_boolean('debug')) |
17 | log = logging.getLogger('gwibber.service') |
18 | log.info('Gwibber backend service starting') |
19 | - # Where is the SQLite database? |
20 | - sqlite_file = (DEFAULT_SQLITE_FILE |
21 | - if args.database is None |
22 | - else args.database) |
23 | - database = sqlite3.connect(sqlite_file) |
24 | + |
25 | # Set up the DBus main loop. |
26 | DBusGMainLoop(set_as_default=True) |
27 | loop = GLib.MainLoop() |
28 | @@ -82,15 +75,12 @@ |
29 | # about unused local variables. |
30 | class services: |
31 | connection = ConnectionMonitor() |
32 | - streams = StreamManager(database) |
33 | - dispatcher = Dispatcher(loop, streams, refresh_interval) |
34 | + dispatcher = Dispatcher(loop, refresh_interval) |
35 | menus = MenuManager(dispatcher.Refresh, loop.quit) |
36 | - messages = MessageManager(database) |
37 | - searches = SearchManager(database) |
38 | shorten = URLShorten(gsettings) |
39 | |
40 | if args.test: |
41 | - services.test = TestService(database, services.connection) |
42 | + services.test = TestService(services.connection) |
43 | try: |
44 | log.info('Starting gwibber-service main loop') |
45 | loop.run() |
46 | |
47 | === modified file 'gwibber/gwibber/service/dispatcher.py' |
48 | --- gwibber/gwibber/service/dispatcher.py 2012-09-13 18:04:03 +0000 |
49 | +++ gwibber/gwibber/service/dispatcher.py 2012-09-19 17:22:19 +0000 |
50 | @@ -40,13 +40,13 @@ |
51 | """This is the primary handler of dbus method calls.""" |
52 | __dbus_object_path__ = '/com/gwibber/Service' |
53 | |
54 | - def __init__(self, mainloop, streams, interval): |
55 | + def __init__(self, mainloop, interval): |
56 | self.bus = dbus.SessionBus() |
57 | bus_name = dbus.service.BusName('com.Gwibber.Service', bus=self.bus) |
58 | super().__init__(bus_name, self.__dbus_object_path__) |
59 | self.mainloop = mainloop |
60 | self._interval = interval |
61 | - self.account_manager = AccountManager(self.Refresh, streams) |
62 | + self.account_manager = AccountManager(self.Refresh) |
63 | self._timer_id = None |
64 | signaler.add_signal('ConnectionOnline', self._on_connection_online) |
65 | signaler.add_signal('ConnectionOffline', self._on_connection_offline) |
66 | |
67 | === removed file 'gwibber/gwibber/service/messages.py' |
68 | --- gwibber/gwibber/service/messages.py 2012-08-21 14:51:01 +0000 |
69 | +++ gwibber/gwibber/service/messages.py 1970-01-01 00:00:00 +0000 |
70 | @@ -1,47 +0,0 @@ |
71 | -# Copyright (C) 2012 Canonical Ltd |
72 | -# |
73 | -# This program is free software: you can redistribute it and/or modify |
74 | -# it under the terms of the GNU General Public License version 2 as |
75 | -# published by the Free Software Foundation. |
76 | -# |
77 | -# This program is distributed in the hope that it will be useful, |
78 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
79 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
80 | -# GNU General Public License for more details. |
81 | -# |
82 | -# You should have received a copy of the GNU General Public License |
83 | -# along with this program. If not, see <http://www.gnu.org/licenses/>. |
84 | - |
85 | -"""DBus service object for message manager.""" |
86 | - |
87 | -__all__ = [ |
88 | - 'MessageManager', |
89 | - ] |
90 | - |
91 | - |
92 | -import dbus |
93 | -import dbus.service |
94 | - |
95 | -from gwibber.storage.messages import MessageStorage |
96 | - |
97 | - |
98 | -class MessageManager(dbus.service.Object): |
99 | - __dbus_object_path__ = '/com/gwibber/Messages' |
100 | - |
101 | - def __init__(self, db): |
102 | - self.bus = dbus.SessionBus() |
103 | - bus_name = dbus.service.BusName('com.Gwibber.Messages', bus=self.bus) |
104 | - super().__init__(bus_name, self.__dbus_object_path__) |
105 | - self.db = db |
106 | - # Initialize the table. |
107 | - MessageStorage(self.db) |
108 | - |
109 | - @dbus.service.signal('com.Gwibber.Messages', signature='ss') |
110 | - def Message(self, change, data): |
111 | - pass |
112 | - |
113 | - @dbus.service.method('com.Gwibber.Messages', |
114 | - in_signature='s', |
115 | - out_signature='s') |
116 | - def Get(self, id): |
117 | - return MessageStorage(self.db).get_data(id) |
118 | |
119 | === removed file 'gwibber/gwibber/service/searches.py' |
120 | --- gwibber/gwibber/service/searches.py 2012-08-21 22:42:24 +0000 |
121 | +++ gwibber/gwibber/service/searches.py 1970-01-01 00:00:00 +0000 |
122 | @@ -1,87 +0,0 @@ |
123 | -# Copyright (C) 2012 Canonical Ltd |
124 | -# |
125 | -# This program is free software: you can redistribute it and/or modify |
126 | -# it under the terms of the GNU General Public License version 2 as |
127 | -# published by the Free Software Foundation. |
128 | -# |
129 | -# This program is distributed in the hope that it will be useful, |
130 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
131 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
132 | -# GNU General Public License for more details. |
133 | -# |
134 | -# You should have received a copy of the GNU General Public License |
135 | -# along with this program. If not, see <http://www.gnu.org/licenses/>. |
136 | - |
137 | -"""DBus service object for searches.""" |
138 | - |
139 | -__all__ = [ |
140 | - 'SearchManager', |
141 | - ] |
142 | - |
143 | - |
144 | -import json |
145 | -import uuid |
146 | -import logging |
147 | - |
148 | -import dbus |
149 | -import dbus.service |
150 | - |
151 | -from gwibber.storage.messages import MessageStorage |
152 | -from gwibber.storage.searches import SearchStorage |
153 | - |
154 | - |
155 | -log = logging.getLogger('gwibber.service') |
156 | - |
157 | - |
158 | -class SearchManager(dbus.service.Object): |
159 | - __dbus_object_path__ = '/com/gwibber/Searches' |
160 | - |
161 | - def __init__(self, db): |
162 | - self.bus = dbus.SessionBus() |
163 | - bus_name = dbus.service.BusName('com.Gwibber.Searches', bus=self.bus) |
164 | - super().__init__(bus_name, self.__dbus_object_path__) |
165 | - self.db = db |
166 | - # Initialize the table. |
167 | - SearchStorage(self.db) |
168 | - |
169 | - @dbus.service.signal('com.Gwibber.Searches', signature='s') |
170 | - def Updated(self, data): |
171 | - log.debug('Search Changed: {}'.format(data)) |
172 | - |
173 | - @dbus.service.signal('com.Gwibber.Searches', signature='s') |
174 | - def Deleted(self, data): |
175 | - log.debug('Search Deleted: {}'.format(data)) |
176 | - |
177 | - @dbus.service.signal('com.Gwibber.Searches', signature='s') |
178 | - def Created(self, data): |
179 | - log.debug('Search Created: {}'.format(data)) |
180 | - |
181 | - @dbus.service.method('com.Gwibber.Searches', |
182 | - in_signature='s', |
183 | - out_signature='s') |
184 | - def Get(self, id): |
185 | - return SearchStorage(self.db).get_data(id) |
186 | - |
187 | - @dbus.service.method('com.Gwibber.Searches', out_signature='s') |
188 | - def List(self): |
189 | - return SearchStorage(self.db).list_all_data() |
190 | - |
191 | - @dbus.service.method('com.Gwibber.Searches', |
192 | - in_signature='s', |
193 | - out_signature='s') |
194 | - def Create(self, search): |
195 | - data = json.loads(search) |
196 | - id = data['id'] = uuid.uuid1().hex |
197 | - encoded = json.dumps(data) |
198 | - SearchStorage(self.db).add(**data) |
199 | - self.Created(encoded) |
200 | - return id |
201 | - |
202 | - @dbus.service.method('com.Gwibber.Searches', in_signature='s') |
203 | - def Delete(self, id): |
204 | - data = self.Get(id) |
205 | - if data: |
206 | - with self.db: |
207 | - SearchStorage(self.db).remove(id) |
208 | - MessageStorage(self.db).remove_transient(id) |
209 | - self.Deleted(data) |
210 | |
211 | === removed file 'gwibber/gwibber/service/streams.py' |
212 | --- gwibber/gwibber/service/streams.py 2012-08-17 22:30:54 +0000 |
213 | +++ gwibber/gwibber/service/streams.py 1970-01-01 00:00:00 +0000 |
214 | @@ -1,93 +0,0 @@ |
215 | -# Copyright (C) 2012 Canonical Ltd |
216 | -# |
217 | -# This program is free software: you can redistribute it and/or modify |
218 | -# it under the terms of the GNU General Public License version 2 as |
219 | -# published by the Free Software Foundation. |
220 | -# |
221 | -# This program is distributed in the hope that it will be useful, |
222 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
223 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
224 | -# GNU General Public License for more details. |
225 | -# |
226 | -# You should have received a copy of the GNU General Public License |
227 | -# along with this program. If not, see <http://www.gnu.org/licenses/>. |
228 | - |
229 | -"""DBus service object for the streams manager.""" |
230 | - |
231 | -__all__ = [ |
232 | - 'StreamManager', |
233 | - ] |
234 | - |
235 | - |
236 | -import json |
237 | -import uuid |
238 | -import logging |
239 | - |
240 | -import dbus |
241 | -import dbus.service |
242 | - |
243 | -from gwibber.storage.messages import MessageStorage |
244 | -from gwibber.storage.streams import StreamStorage |
245 | - |
246 | - |
247 | -log = logging.getLogger('gwibber.service') |
248 | - |
249 | - |
250 | -class StreamManager(dbus.service.Object): |
251 | - __dbus_object_path__ = '/com/gwibber/Streams' |
252 | - |
253 | - def __init__(self, db): |
254 | - self.bus = dbus.SessionBus() |
255 | - bus_name = dbus.service.BusName('com.Gwibber.Streams', bus=self.bus) |
256 | - super().__init__(bus_name, self.__dbus_object_path__) |
257 | - self.db = db |
258 | - # Initialize the table. |
259 | - StreamStorage(self.db) |
260 | - |
261 | - @dbus.service.signal('com.Gwibber.Streams', signature='s') |
262 | - def Updated(self, data): |
263 | - log.debug('Stream Changed: {}'.format(data)) |
264 | - |
265 | - @dbus.service.signal('com.Gwibber.Streams', signature='s') |
266 | - def Deleted(self, data): |
267 | - log.debug('Stream Deleted: {}'.format(data)) |
268 | - |
269 | - @dbus.service.signal('com.Gwibber.Streams', signature='s') |
270 | - def Created(self, data): |
271 | - log.debug('Stream Created: {}'.format(data)) |
272 | - |
273 | - @dbus.service.method('com.Gwibber.Streams', |
274 | - in_signature='s', |
275 | - out_signature='s') |
276 | - def Get(self, id): |
277 | - return StreamStorage(self.db).get_data(id) |
278 | - |
279 | - @dbus.service.method('com.Gwibber.Streams', out_signature='s') |
280 | - def List(self): |
281 | - return StreamStorage(self.db).list_all_data() |
282 | - |
283 | - @dbus.service.method('com.Gwibber.Streams', |
284 | - in_signature='ssdssssd', |
285 | - out_signature='s') |
286 | - def Messages(self, stream, account, time, transient, recipient, |
287 | - orderby, order, limit): |
288 | - return MessageStorage(self.db).get_messages( |
289 | - stream, account, time, transient, recipient, orderby, order, limit) |
290 | - |
291 | - @dbus.service.method('com.Gwibber.Streams', |
292 | - in_signature='s', |
293 | - out_signature='s') |
294 | - def Create(self, search): |
295 | - data = json.loads(search) |
296 | - id = data['id'] = uuid.uuid1().hex |
297 | - encoded = json.dumps(data) |
298 | - StreamStorage(self.db).add(**data) |
299 | - self.Created(encoded) |
300 | - return id |
301 | - |
302 | - @dbus.service.method('com.Gwibber.Streams', in_signature='s') |
303 | - def Delete(self, id): |
304 | - data = self.Get(id) |
305 | - if data: |
306 | - StreamStorage(self.db).remove(id) |
307 | - self.Deleted(data) |
308 | |
309 | === removed directory 'gwibber/gwibber/storage' |
310 | === removed file 'gwibber/gwibber/storage/__init__.py' |
311 | === removed file 'gwibber/gwibber/storage/messages.py' |
312 | --- gwibber/gwibber/storage/messages.py 2012-08-21 22:42:24 +0000 |
313 | +++ gwibber/gwibber/storage/messages.py 1970-01-01 00:00:00 +0000 |
314 | @@ -1,172 +0,0 @@ |
315 | -# Copyright (C) 2012 Canonical Ltd |
316 | -# |
317 | -# This program is free software: you can redistribute it and/or modify |
318 | -# it under the terms of the GNU General Public License version 2 as |
319 | -# published by the Free Software Foundation. |
320 | -# |
321 | -# This program is distributed in the hope that it will be useful, |
322 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
323 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
324 | -# GNU General Public License for more details. |
325 | -# |
326 | -# You should have received a copy of the GNU General Public License |
327 | -# along with this program. If not, see <http://www.gnu.org/licenses/>. |
328 | - |
329 | -"""Message storage.""" |
330 | - |
331 | -__all__ = [ |
332 | - 'MessageStorage', |
333 | - ] |
334 | - |
335 | - |
336 | -import logging |
337 | - |
338 | -log = logging.getLogger('gwibber.service') |
339 | - |
340 | - |
341 | -COMMASPACE = ', ' |
342 | -MAX_COUNT = 2000 |
343 | - |
344 | - |
345 | -class MessageStorage: |
346 | - """Message storage.""" |
347 | - |
348 | - def __init__(self, db): |
349 | - self.db = db |
350 | - # Create the table if it does not yet exist. |
351 | - if not self.db.execute('PRAGMA table_info(messages)').fetchall(): |
352 | - with self.db: |
353 | - self.db.execute(""" |
354 | - CREATE TABLE messages ( |
355 | - id text, |
356 | - mid text, |
357 | - account text, |
358 | - service text, |
359 | - operation text, |
360 | - transient text, |
361 | - stream text, |
362 | - time integer, |
363 | - text text, |
364 | - from_me integer, |
365 | - to_me integer, |
366 | - sender text, |
367 | - recipient text, |
368 | - data text) |
369 | - """) |
370 | - self.db.execute(""" |
371 | - CREATE UNIQUE INDEX idx1 ON messages ( |
372 | - mid, account, operation, transient) |
373 | - """) |
374 | - self.db.execute(""" |
375 | - CREATE INDEX IF NOT EXISTS idx2 on messages ( |
376 | - stream, time, transient) |
377 | - """) |
378 | - |
379 | - def get_data(self, id): |
380 | - results = self.db.execute( |
381 | - 'SELECT data FROM messages WHERE id = ?', (id,)).fetchone() |
382 | - if results: |
383 | - return results[0] |
384 | - return '' |
385 | - |
386 | - def get_messages(self, stream, account, time, transient, recipient, |
387 | - orderby, order, limit): |
388 | - # The home stream is an alias for all streams in all accounts. |
389 | - if stream == 'home': |
390 | - stream = 'all' |
391 | - account = 'all' |
392 | - |
393 | - with self.db: |
394 | - if stream == 'sent' and account != 'all': |
395 | - # Get all messages I've sent from a specific account. |
396 | - where = """ |
397 | - WHERE from_me = 1 AND account = ? |
398 | - AND time >= ? AND transient = ? |
399 | - """ |
400 | - args = account, time, transient |
401 | - elif stream == 'sent' and account == 'all': |
402 | - # Get all messages I've sent from any account. |
403 | - where = 'WHERE from_me = 1 AND time >= ? AND transient = ?' |
404 | - args = time, transient |
405 | - elif transient != '0' and account != 'all' and recipient != '0': |
406 | - where = """ |
407 | - WHERE time >= ? AND transient = ? |
408 | - OR (time >= ? AND account = ? AND recipient = ?) |
409 | - """ |
410 | - args = time, transient, time, account, recipient |
411 | - elif stream != 'all' and account != 'all': |
412 | - where = """ |
413 | - WHERE stream = ? AND account = ? AND time >= ? |
414 | - AND transient = ? |
415 | - """ |
416 | - args = stream, account, time, transient |
417 | - elif stream == 'all' and account != 'all': |
418 | - where = 'WHERE account = ? AND time >= ? AND transient = ?' |
419 | - args = account, time, transient |
420 | - elif stream != 'all' and account == 'all': |
421 | - where = 'WHERE stream = ? AND time >= ? AND transient = ?' |
422 | - args = stream, time, transient |
423 | - else: |
424 | - where = 'WHERE time >= ? AND transient = ?' |
425 | - args = time, transient |
426 | - query = 'SELECT data FROM messages ' |
427 | - query += where |
428 | - query += ' ORDER BY {} {}'.format(orderby, order) |
429 | - if limit > 0: |
430 | - query += ' LIMIT {:d}'.format(limit) |
431 | - results = self.db.execute(query, args) |
432 | - # Collating happens outside of the database transaction. |
433 | - return '[{}]'.format(COMMASPACE.join(row[0] for row in results)) |
434 | - |
435 | - def maintenance(self, account_manager): |
436 | - log.info('DB maintenance: cleaning up messages table...') |
437 | - accounts = self.db.execute( |
438 | - 'SELECT distinct(account) FROM messages').fetchall() |
439 | - for account in accounts: |
440 | - # Adjust for the query returning a tuple. |
441 | - account = account[0] |
442 | - # Does the account exist? |
443 | - global_id, slash, rest = account.partition('/') |
444 | - if slash == '': |
445 | - # There is no global id, thus the account does not exist. |
446 | - exists = False |
447 | - else: |
448 | - exists = account_manager.get_account(global_id) |
449 | - if not exists: |
450 | - log.info('DB maintenance: Removing data for ' |
451 | - 'unknown account: {}'.format(account)) |
452 | - self.db.execute('DELETE FROM messages WHERE account = ?', |
453 | - (account,)) |
454 | - count = self.db.execute(""" |
455 | - SELECT count(data) FROM messages |
456 | - WHERE operation = 'receive' |
457 | - AND stream = 'messages' |
458 | - AND account = ? |
459 | - AND time != 0 |
460 | - """, (account,)).fetchone()[0] |
461 | - log.info('DB maintenance: found {} records in messages ' |
462 | - 'stream for account {}'.format(count, account)) |
463 | - log.info('DB maintenance: purging old data for {}'.format( |
464 | - account)) |
465 | - delete_limit = count - MAX_COUNT |
466 | - if delete_limit > 0: |
467 | - self.db.execute(""" |
468 | - DELETE FROM messages |
469 | - WHERE account = ? |
470 | - AND operation = 'receive' |
471 | - AND stream = 'messages' |
472 | - AND time IN ( |
473 | - SELECT CAST (time AS int) FROM ( |
474 | - SELECT time FROM messages |
475 | - WHERE account = ? |
476 | - AND operation = 'receive' |
477 | - AND stream = 'messages' |
478 | - AND time != 0 ORDER BY time ASC LIMIT ?) |
479 | - ORDER BY time ASC) |
480 | - """, (account, account, delete_limit)) |
481 | - self.db.execute('VACUUM') |
482 | - |
483 | - def remove_transient(self, transient_id): |
484 | - with self.db: |
485 | - self.db.execute('DELETE FROM messages WHERE transient = ?', |
486 | - (transient_id,)) |
487 | |
488 | === removed file 'gwibber/gwibber/storage/searches.py' |
489 | --- gwibber/gwibber/storage/searches.py 2012-08-21 21:35:08 +0000 |
490 | +++ gwibber/gwibber/storage/searches.py 1970-01-01 00:00:00 +0000 |
491 | @@ -1,63 +0,0 @@ |
492 | -# Copyright (C) 2012 Canonical Ltd |
493 | -# |
494 | -# This program is free software: you can redistribute it and/or modify |
495 | -# it under the terms of the GNU General Public License version 2 as |
496 | -# published by the Free Software Foundation. |
497 | -# |
498 | -# This program is distributed in the hope that it will be useful, |
499 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
500 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
501 | -# GNU General Public License for more details. |
502 | -# |
503 | -# You should have received a copy of the GNU General Public License |
504 | -# along with this program. If not, see <http://www.gnu.org/licenses/>. |
505 | - |
506 | -"""Search storage.""" |
507 | - |
508 | -__all__ = [ |
509 | - 'SearchStorage', |
510 | - ] |
511 | - |
512 | - |
513 | -COMMASPACE = ', ' |
514 | - |
515 | - |
516 | -class SearchStorage: |
517 | - """Search storage.""" |
518 | - |
519 | - def __init__(self, db): |
520 | - self.db = db |
521 | - # Create the table if it does not yet exist. |
522 | - if not self.db.execute('PRAGMA table_info(searches)').fetchall(): |
523 | - with self.db: |
524 | - self.db.execute(""" |
525 | - CREATE TABLE searches ( |
526 | - id text, |
527 | - name text, |
528 | - query text, |
529 | - data text) |
530 | - """) |
531 | - |
532 | - def get_data(self, id): |
533 | - """Get the row's data.""" |
534 | - results = self.db.execute('SELECT data FROM searches WHERE id = ?', |
535 | - (id,)).fetchone() |
536 | - if results: |
537 | - return results[0] |
538 | - return '' |
539 | - |
540 | - def list_all_data(self): |
541 | - """Get a string listing all data in the table.""" |
542 | - results = self.db.execute('SELECT data FROM searches') |
543 | - return '[{}]'.format(COMMASPACE.join(row[0] for row in results)) |
544 | - |
545 | - def add(self, id, name, query, data): |
546 | - """Add a row to the table.""" |
547 | - with self.db: |
548 | - self.db.execute('INSERT INTO searches VALUES (?, ?, ?, ?)', |
549 | - (id, name, query, data)) |
550 | - |
551 | - def remove(self, id): |
552 | - """Remove a row from the table.""" |
553 | - with self.db: |
554 | - self.db.execute('DELETE FROM searches WHERE id = ?', (id,)) |
555 | |
556 | === removed file 'gwibber/gwibber/storage/streams.py' |
557 | --- gwibber/gwibber/storage/streams.py 2012-08-17 22:30:54 +0000 |
558 | +++ gwibber/gwibber/storage/streams.py 1970-01-01 00:00:00 +0000 |
559 | @@ -1,68 +0,0 @@ |
560 | -# Copyright (C) 2012 Canonical Ltd |
561 | -# |
562 | -# This program is free software: you can redistribute it and/or modify |
563 | -# it under the terms of the GNU General Public License version 2 as |
564 | -# published by the Free Software Foundation. |
565 | -# |
566 | -# This program is distributed in the hope that it will be useful, |
567 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
568 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
569 | -# GNU General Public License for more details. |
570 | -# |
571 | -# You should have received a copy of the GNU General Public License |
572 | -# along with this program. If not, see <http://www.gnu.org/licenses/>. |
573 | - |
574 | -"""Streams storage.""" |
575 | - |
576 | -__all__ = [ |
577 | - 'StreamStorage', |
578 | - ] |
579 | - |
580 | - |
581 | -COMMASPACE = ', ' |
582 | - |
583 | - |
584 | -class StreamStorage: |
585 | - """Stream storage.""" |
586 | - |
587 | - def __init__(self, db): |
588 | - self.db = db |
589 | - # Create the table if it does not yet exist. |
590 | - if not self.db.execute('PRAGMA table_info(streams)').fetchall(): |
591 | - with self.db: |
592 | - self.db.execute(""" |
593 | - CREATE TABLE streams ( |
594 | - id text, |
595 | - name text, |
596 | - account text, |
597 | - operation text, |
598 | - data text) |
599 | - """) |
600 | - |
601 | - def get_data(self, id): |
602 | - """Get the row's data.""" |
603 | - with self.db: |
604 | - results = self.db.execute( |
605 | - 'SELECT data FROM streams WHERE id = ?', |
606 | - (id,)).fetchone() |
607 | - if results: |
608 | - return results[0] |
609 | - return '' |
610 | - |
611 | - def list_all_data(self): |
612 | - """Get a string listing all data in the table.""" |
613 | - with self.db: |
614 | - results = self.db.execute('SELECT data FROM streams') |
615 | - return '[{}]'.format( |
616 | - COMMASPACE.join(row[0] for row in results.fetchall())) |
617 | - |
618 | - def add(self, id, name, account, operation, data): |
619 | - """Add a row to the table.""" |
620 | - with self.db: |
621 | - self.db.execute('INSERT INTO streams VALUES (?, ?, ?, ?, ?)', |
622 | - (id, name, account, operation, data)) |
623 | - |
624 | - def remove(self, id): |
625 | - """Remove a row from the table.""" |
626 | - with self.db: |
627 | - self.db.execute('DELETE FROM streams WHERE id = ?', (id,)) |
628 | |
629 | === removed file 'gwibber/gwibber/testing/com.Gwibber.Messages.service.in' |
630 | --- gwibber/gwibber/testing/com.Gwibber.Messages.service.in 2012-08-21 14:51:01 +0000 |
631 | +++ gwibber/gwibber/testing/com.Gwibber.Messages.service.in 1970-01-01 00:00:00 +0000 |
632 | @@ -1,3 +0,0 @@ |
633 | -[D-BUS Service] |
634 | -Name=com.Gwibber.Messages |
635 | -Exec={BINDIR}/gwibber-service --database {TMPDIR}/gwibber.db --test |
636 | |
637 | === removed file 'gwibber/gwibber/testing/com.Gwibber.Searches.service.in' |
638 | --- gwibber/gwibber/testing/com.Gwibber.Searches.service.in 2012-08-21 21:05:20 +0000 |
639 | +++ gwibber/gwibber/testing/com.Gwibber.Searches.service.in 1970-01-01 00:00:00 +0000 |
640 | @@ -1,3 +0,0 @@ |
641 | -[D-BUS Service] |
642 | -Name=com.Gwibber.Searches |
643 | -Exec={BINDIR}/gwibber-service --database {TMPDIR}/gwibber.db --test |
644 | |
645 | === removed file 'gwibber/gwibber/testing/com.Gwibber.Streams.service.in' |
646 | --- gwibber/gwibber/testing/com.Gwibber.Streams.service.in 2012-08-21 14:51:01 +0000 |
647 | +++ gwibber/gwibber/testing/com.Gwibber.Streams.service.in 1970-01-01 00:00:00 +0000 |
648 | @@ -1,3 +0,0 @@ |
649 | -[D-BUS Service] |
650 | -Name=com.Gwibber.Streams |
651 | -Exec={BINDIR}/gwibber-service --database {TMPDIR}/gwibber.db --test |
652 | |
653 | === modified file 'gwibber/gwibber/testing/dbus.py' |
654 | --- gwibber/gwibber/testing/dbus.py 2012-09-11 22:32:44 +0000 |
655 | +++ gwibber/gwibber/testing/dbus.py 2012-09-19 17:22:19 +0000 |
656 | @@ -49,10 +49,7 @@ |
657 | |
658 | SERVICES = [ |
659 | 'com.Gwibber.Connection', |
660 | - 'com.Gwibber.Messages', |
661 | - 'com.Gwibber.Searches', |
662 | 'com.Gwibber.Service', |
663 | - 'com.Gwibber.Streams', |
664 | 'com.Gwibber.Test', |
665 | 'com.Gwibber.URLShorten', |
666 | ] |
667 | |
668 | === modified file 'gwibber/gwibber/testing/service.py' |
669 | --- gwibber/gwibber/testing/service.py 2012-09-07 15:20:41 +0000 |
670 | +++ gwibber/gwibber/testing/service.py 2012-09-19 17:22:19 +0000 |
671 | @@ -35,11 +35,10 @@ |
672 | class TestService(dbus.service.Object): |
673 | __dbus_object_path__ = '/com/gwibber/Test' |
674 | |
675 | - def __init__(self, db, monitor): |
676 | + def __init__(self, monitor): |
677 | self.bus = dbus.SessionBus() |
678 | bus_name = dbus.service.BusName('com.Gwibber.Test', bus=self.bus) |
679 | super().__init__(bus_name, self.__dbus_object_path__) |
680 | - self.db = db |
681 | self.id = 0 |
682 | self.connection_monitor = monitor |
683 | # Connect signals |
684 | @@ -51,52 +50,6 @@ |
685 | signaler.add_signal('ConnectionOnline', online_callback) |
686 | signaler.add_signal('ConnectionOffline', offline_callback) |
687 | |
688 | - @dbus.service.method('com.Gwibber.Test', |
689 | - in_signature='s', |
690 | - out_signature='s') |
691 | - def InsertDataIntoMessages(self, data): |
692 | - # Initialize the database if not already created. |
693 | - MessageStorage(self.db) |
694 | - stored_id = str(self.id) |
695 | - self.db.execute(""" |
696 | - INSERT INTO messages VALUES |
697 | - (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) |
698 | - """, (stored_id, stored_id, '', '', '', '', '', 0, '', 0, 0, '', '', |
699 | - data)) |
700 | - self.id += 1 |
701 | - return stored_id |
702 | - |
703 | - @dbus.service.method('com.Gwibber.Test', |
704 | - in_signature='sss', |
705 | - out_signature='s') |
706 | - def InsertSearchTransientData(self, name, query, data): |
707 | - # Set things up to test the com.Gwibber.Searches.Delete() method. |
708 | - # Start by initializing the tables. |
709 | - MessageStorage(self.db) |
710 | - SearchStorage(self.db) |
711 | - stored_id = str(self.id) |
712 | - self.db.execute(""" |
713 | - INSERT INTO searches VALUES (?, ?, ?, ?) |
714 | - """, (stored_id, name, query, data)) |
715 | - # The key thing about adding a row to messages is that the |
716 | - # messages.transient column must be equal to the searches.id column. |
717 | - # It's kind of like a foreign reference. |
718 | - self.db.execute(""" |
719 | - INSERT INTO messages VALUES |
720 | - (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) |
721 | - """, (stored_id, stored_id, '', '', '', |
722 | - stored_id, '', 0, '', 0, 0, '', '', data)) |
723 | - self.id += 1 |
724 | - return stored_id |
725 | - |
726 | - @dbus.service.method('com.Gwibber.Test', in_signature='s') |
727 | - def DeleteDataFromMessages(self, id): |
728 | - # Initialize the database if not already created. |
729 | - MessageStorage(self.db) |
730 | - self.db.execute(""" |
731 | - DELETE FROM messages WHERE id = ? |
732 | - """, id) |
733 | - |
734 | @dbus.service.method('com.Gwibber.Test', out_signature='i') |
735 | def SignalTestOn(self): |
736 | self.connection_monitor.ConnectionOnline() |
737 | |
738 | === modified file 'gwibber/gwibber/tests/test_account.py' |
739 | --- gwibber/gwibber/tests/test_account.py 2012-09-13 17:01:59 +0000 |
740 | +++ gwibber/gwibber/tests/test_account.py 2012-09-19 17:22:19 +0000 |
741 | @@ -171,11 +171,6 @@ |
742 | """Test the AccountManager API.""" |
743 | |
744 | def setUp(self): |
745 | - self.stream = mock.Mock() |
746 | - self.stream.List.return_value = json.dumps( |
747 | - [dict(account='faker/than fake', |
748 | - id='foo'), |
749 | - ]) |
750 | self.account_service = mock.Mock() |
751 | |
752 | def test_account_manager(self): |
753 | @@ -183,14 +178,14 @@ |
754 | # internal mapping. |
755 | def refresh(): |
756 | pass |
757 | - AccountManager(refresh, self.stream) |
758 | + AccountManager(refresh) |
759 | |
760 | def test_account_manager_add_new_account(self): |
761 | # Explicitly adding a new account puts the account's global_id into |
762 | # the account manager's mapping. |
763 | def refresh(): |
764 | pass |
765 | - manager = AccountManager(refresh, self.stream) |
766 | + manager = AccountManager(refresh) |
767 | manager.add_new_account(self.account_service) |
768 | self.assertIn('fake global id', manager._accounts) |
769 | |
770 | @@ -201,7 +196,7 @@ |
771 | nonlocal refreshed |
772 | refreshed = True |
773 | accounts_manager.get_account().list_services.return_value = [] |
774 | - manager = AccountManager(refresh, self.stream) |
775 | + manager = AccountManager(refresh) |
776 | manager._on_enabled_event(accounts_manager, 'fake global id') |
777 | self.assertTrue(refreshed) |
778 | |
779 | @@ -213,7 +208,7 @@ |
780 | def refresh(): |
781 | nonlocal refreshed |
782 | refreshed = True |
783 | - manager = AccountManager(refresh, self.stream) |
784 | + manager = AccountManager(refresh) |
785 | self.assertNotIn('fake global id', manager._accounts) |
786 | manager._on_account_deleted(accounts_manager, 'fake global id') |
787 | self.assertNotIn('fake global id', manager._accounts) |
788 | @@ -226,11 +221,9 @@ |
789 | def refresh(): |
790 | nonlocal refreshed |
791 | refreshed = True |
792 | - manager = AccountManager(refresh, self.stream) |
793 | + manager = AccountManager(refresh) |
794 | manager.add_new_account(self.account_service) |
795 | # Now manager._accounts has a 'fake global id' key. |
796 | manager._on_account_deleted(accounts_manager, 'fake global id') |
797 | - self.stream.List.assert_called_once_with() |
798 | - self.stream.Delete.assert_called_once_with('foo') |
799 | self.assertNotIn('fake global id', manager._accounts) |
800 | self.assertTrue(refreshed) |
801 | |
802 | === modified file 'gwibber/gwibber/tests/test_dbus.py' |
803 | --- gwibber/gwibber/tests/test_dbus.py 2012-09-12 22:15:43 +0000 |
804 | +++ gwibber/gwibber/tests/test_dbus.py 2012-09-19 17:22:19 +0000 |
805 | @@ -67,24 +67,6 @@ |
806 | |
807 | def setUp(self): |
808 | self.session_bus = dbus.SessionBus() |
809 | - # We have to make sure to remove any rows that the individual tests |
810 | - # may have added, otherwise there are order dependencies. |
811 | - self.stream_ids = [] |
812 | - self.search_ids = [] |
813 | - |
814 | - def tearDown(self): |
815 | - obj = self.session_bus.get_object( |
816 | - 'com.Gwibber.Streams', |
817 | - '/com/gwibber/Streams') |
818 | - iface = dbus.Interface(obj, 'com.Gwibber.Streams') |
819 | - for id in self.stream_ids: |
820 | - iface.Delete(id) |
821 | - obj = self.session_bus.get_object( |
822 | - 'com.Gwibber.Searches', |
823 | - '/com/gwibber/Searches') |
824 | - iface = dbus.Interface(obj, 'com.Gwibber.Searches') |
825 | - for id in self.search_ids: |
826 | - iface.Delete(id) |
827 | |
828 | def test_shorten(self): |
829 | obj = self.session_bus.get_object( |
830 | @@ -94,125 +76,6 @@ |
831 | short_url = iface.Shorten('http://www.python.org') |
832 | self.assertIsInstance(short_url, dbus.String) |
833 | |
834 | - def test_streams_list_empty(self): |
835 | - obj = self.session_bus.get_object( |
836 | - 'com.Gwibber.Streams', |
837 | - '/com/gwibber/Streams') |
838 | - iface = dbus.Interface(obj, 'com.Gwibber.Streams') |
839 | - self.assertEqual(iface.List(), '[]') |
840 | - |
841 | - def test_streams_create(self): |
842 | - obj = self.session_bus.get_object( |
843 | - 'com.Gwibber.Streams', |
844 | - '/com/gwibber/Streams') |
845 | - iface = dbus.Interface(obj, 'com.Gwibber.Streams') |
846 | - name = 'bob' |
847 | - account = 'twitter' |
848 | - operation = 'tweet' |
849 | - data = 'payload' |
850 | - search = json.dumps(dict(name=name, account=account, |
851 | - operation=operation, data=data)) |
852 | - id = iface.Create(search) |
853 | - self.stream_ids.append(id) |
854 | - self.assertEqual(iface.List(), '[payload]') |
855 | - |
856 | - def test_streams_create_two(self): |
857 | - obj = self.session_bus.get_object( |
858 | - 'com.Gwibber.Streams', |
859 | - '/com/gwibber/Streams') |
860 | - iface = dbus.Interface(obj, 'com.Gwibber.Streams') |
861 | - name = 'bob' |
862 | - account = 'twitter' |
863 | - operation = 'tweet' |
864 | - search = json.dumps(dict(name=name, account=account, |
865 | - operation=operation, data='one')) |
866 | - id = iface.Create(search) |
867 | - self.stream_ids.append(id) |
868 | - search = json.dumps(dict(name=name, account=account, |
869 | - operation=operation, data='two')) |
870 | - id = iface.Create(search) |
871 | - self.stream_ids.append(id) |
872 | - self.assertEqual(iface.List(), '[one, two]') |
873 | - |
874 | - def test_streams_delete(self): |
875 | - obj = self.session_bus.get_object( |
876 | - 'com.Gwibber.Streams', |
877 | - '/com/gwibber/Streams') |
878 | - iface = dbus.Interface(obj, 'com.Gwibber.Streams') |
879 | - name = 'bob' |
880 | - account = 'twitter' |
881 | - operation = 'tweet' |
882 | - data = 'payload' |
883 | - search = json.dumps(dict(name=name, account=account, |
884 | - operation=operation, data=data)) |
885 | - id = iface.Create(search) |
886 | - self.assertEqual(iface.List(), '[payload]') |
887 | - iface.Delete(id) |
888 | - self.assertEqual(iface.List(), '[]') |
889 | - |
890 | - def test_streams_get(self): |
891 | - obj = self.session_bus.get_object( |
892 | - 'com.Gwibber.Streams', |
893 | - '/com/gwibber/Streams') |
894 | - iface = dbus.Interface(obj, 'com.Gwibber.Streams') |
895 | - name = 'bob' |
896 | - account = 'twitter' |
897 | - operation = 'tweet' |
898 | - data = 'getme' |
899 | - search = json.dumps(dict(name=name, account=account, |
900 | - operation=operation, data=data)) |
901 | - id = iface.Create(search) |
902 | - self.assertEqual(iface.Get(id), 'getme') |
903 | - self.stream_ids.append(id) |
904 | - |
905 | - # XXX Need tests for the Updated(), Deleted(), and Created() signals in |
906 | - # com.Gwibber.Streams. |
907 | - |
908 | - def test_messages_get(self): |
909 | - # First, insert some data into the messages table. We do this through |
910 | - # the dbus testing interface so that we don't have to worry about |
911 | - # SQLite lock contention. |
912 | - obj = self.session_bus.get_object( |
913 | - 'com.Gwibber.Test', |
914 | - '/com/gwibber/Test') |
915 | - test_iface = dbus.Interface(obj, 'com.Gwibber.Test') |
916 | - stored_ids = {} |
917 | - stored_ids[test_iface.InsertDataIntoMessages('one')] = 'one' |
918 | - stored_ids[test_iface.InsertDataIntoMessages('two')] = 'two' |
919 | - # Now get the data back through the non-testing API. |
920 | - obj = self.session_bus.get_object( |
921 | - 'com.Gwibber.Messages', |
922 | - '/com/gwibber/Messages') |
923 | - iface = dbus.Interface(obj, 'com.Gwibber.Messages') |
924 | - for key, value in stored_ids.items(): |
925 | - self.assertEqual(iface.Get(key), value) |
926 | - # Clean up. |
927 | - for id in stored_ids: |
928 | - test_iface.DeleteDataFromMessages(id) |
929 | - |
930 | - def test_messages_get_missing(self): |
931 | - # First, insert some data into the messages table. We do this through |
932 | - # the dbus testing interface so that we don't have to worry about |
933 | - # SQLite lock contention. |
934 | - obj = self.session_bus.get_object( |
935 | - 'com.Gwibber.Test', |
936 | - '/com/gwibber/Test') |
937 | - test_iface = dbus.Interface(obj, 'com.Gwibber.Test') |
938 | - stored_ids = [] |
939 | - stored_ids.append(test_iface.InsertDataIntoMessages('one')) |
940 | - stored_ids.append(test_iface.InsertDataIntoMessages('two')) |
941 | - # Now get the data back through the non-testing API. |
942 | - obj = self.session_bus.get_object( |
943 | - 'com.Gwibber.Messages', |
944 | - '/com/gwibber/Messages') |
945 | - iface = dbus.Interface(obj, 'com.Gwibber.Messages') |
946 | - self.assertEqual(iface.Get('9999'), '') |
947 | - # Clean up. |
948 | - for id in stored_ids: |
949 | - test_iface.DeleteDataFromMessages(id) |
950 | - |
951 | - # XXX Need test for the Message() signal in com.Gwibber.Messages. |
952 | - |
953 | def test_connection_is_connected(self): |
954 | # Test whether the network manager is connected or not. This uses the |
955 | # standard system service, and we can't guarantee its state will be |
956 | @@ -242,106 +105,6 @@ |
957 | # tests for various failure modes in ConnectionMonitor.__init__() and |
958 | # .isConnected(). |
959 | |
960 | - def test_searches_list_empty(self): |
961 | - obj = self.session_bus.get_object( |
962 | - 'com.Gwibber.Searches', |
963 | - '/com/gwibber/Searches') |
964 | - iface = dbus.Interface(obj, 'com.Gwibber.Searches') |
965 | - self.assertEqual(iface.List(), '[]') |
966 | - |
967 | - def test_searches_create(self): |
968 | - obj = self.session_bus.get_object( |
969 | - 'com.Gwibber.Searches', |
970 | - '/com/gwibber/Searches') |
971 | - iface = dbus.Interface(obj, 'com.Gwibber.Searches') |
972 | - name = 'bob' |
973 | - query = 'myquery' |
974 | - data = 'payload' |
975 | - search = json.dumps(dict(name=name, query=query, data=data)) |
976 | - id = iface.Create(search) |
977 | - self.search_ids.append(id) |
978 | - self.assertEqual(iface.List(), '[payload]') |
979 | - |
980 | - def test_searches_create_two(self): |
981 | - obj = self.session_bus.get_object( |
982 | - 'com.Gwibber.Searches', |
983 | - '/com/gwibber/Searches') |
984 | - iface = dbus.Interface(obj, 'com.Gwibber.Searches') |
985 | - name = 'bob' |
986 | - query = 'myquery' |
987 | - search = json.dumps(dict(name=name, query=query, data='one')) |
988 | - id = iface.Create(search) |
989 | - self.search_ids.append(id) |
990 | - search = json.dumps(dict(name=name, query=query, data='two')) |
991 | - id = iface.Create(search) |
992 | - self.search_ids.append(id) |
993 | - self.assertEqual(iface.List(), '[one, two]') |
994 | - |
995 | - def test_searches_delete(self): |
996 | - obj = self.session_bus.get_object( |
997 | - 'com.Gwibber.Searches', |
998 | - '/com/gwibber/Searches') |
999 | - iface = dbus.Interface(obj, 'com.Gwibber.Searches') |
1000 | - name = 'bob' |
1001 | - query = 'myquery' |
1002 | - data = 'payload' |
1003 | - search = json.dumps(dict(name=name, query=query, data=data)) |
1004 | - id = iface.Create(search) |
1005 | - self.assertEqual(iface.List(), '[payload]') |
1006 | - iface.Delete(id) |
1007 | - self.assertEqual(iface.List(), '[]') |
1008 | - |
1009 | - def test_searches_delete_transients(self): |
1010 | - # Deleting search data also deletes rows from the messages table where |
1011 | - # the transients column matches the id in the storage table. Call |
1012 | - # into the test service to set up the data for this. |
1013 | - obj = self.session_bus.get_object( |
1014 | - 'com.Gwibber.Test', |
1015 | - '/com/gwibber/Test') |
1016 | - test_iface = dbus.Interface(obj, 'com.Gwibber.Test') |
1017 | - stored_id_1 = test_iface.InsertSearchTransientData( |
1018 | - 'anne', 'query1', 'one') |
1019 | - stored_id_2 = test_iface.InsertSearchTransientData( |
1020 | - 'bart', 'query2', 'two') |
1021 | - # Now there should be two rows in both tables. |
1022 | - obj = self.session_bus.get_object( |
1023 | - 'com.Gwibber.Searches', |
1024 | - '/com/gwibber/Searches') |
1025 | - searches_iface = dbus.Interface(obj, 'com.Gwibber.Searches') |
1026 | - self.assertEqual(searches_iface.List(), '[one, two]') |
1027 | - obj = self.session_bus.get_object( |
1028 | - 'com.Gwibber.Messages', |
1029 | - '/com/gwibber/Messages') |
1030 | - messages_iface = dbus.Interface(obj, 'com.Gwibber.Messages') |
1031 | - self.assertEqual(messages_iface.Get(stored_id_1), 'one') |
1032 | - self.assertEqual(messages_iface.Get(stored_id_2), 'two') |
1033 | - # Now, delete just the first id. |
1034 | - searches_iface.Delete(stored_id_1) |
1035 | - # And verify that only 'two' exists in both tables. |
1036 | - self.assertEqual(searches_iface.List(), '[two]') |
1037 | - self.assertEqual(messages_iface.Get(stored_id_1), '') |
1038 | - self.assertEqual(messages_iface.Get(stored_id_2), 'two') |
1039 | - # Deleting the second one should clean everything back up. |
1040 | - searches_iface.Delete(stored_id_2) |
1041 | - self.assertEqual(searches_iface.List(), '[]') |
1042 | - self.assertEqual(messages_iface.Get(stored_id_1), '') |
1043 | - self.assertEqual(messages_iface.Get(stored_id_2), '') |
1044 | - |
1045 | - def test_searches_get(self): |
1046 | - obj = self.session_bus.get_object( |
1047 | - 'com.Gwibber.Searches', |
1048 | - '/com/gwibber/Searches') |
1049 | - iface = dbus.Interface(obj, 'com.Gwibber.Searches') |
1050 | - name = 'bob' |
1051 | - query = 'myquery' |
1052 | - data = 'getme' |
1053 | - search = json.dumps(dict(name=name, query=query, data=data)) |
1054 | - id = iface.Create(search) |
1055 | - self.assertEqual(iface.Get(id), 'getme') |
1056 | - self.search_ids.append(id) |
1057 | - |
1058 | - # XXX Need tests for the Updated(), Deleted(), and Created() signals. |
1059 | - |
1060 | def test_get_features(self): |
1061 | obj = self.session_bus.get_object( |
1062 | 'com.Gwibber.Service', |
1063 | |
1064 | === added file 'gwibber/gwibber/tests/test_model.py' |
1065 | --- gwibber/gwibber/tests/test_model.py 1970-01-01 00:00:00 +0000 |
1066 | +++ gwibber/gwibber/tests/test_model.py 2012-09-19 17:22:19 +0000 |
1067 | @@ -0,0 +1,39 @@ |
1068 | +# Copyright (C) 2012 Canonical Ltd |
1069 | +# |
1070 | +# This program is free software: you can redistribute it and/or modify |
1071 | +# it under the terms of the GNU General Public License version 2 as |
1072 | +# published by the Free Software Foundation. |
1073 | +# |
1074 | +# This program is distributed in the hope that it will be useful, |
1075 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1076 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1077 | +# GNU General Public License for more details. |
1078 | +# |
1079 | +# You should have received a copy of the GNU General Public License |
1080 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1081 | + |
1082 | +"""Test the Dee.SharedModel that we use for communicating with our frontend.""" |
1083 | + |
1084 | +__all__ = [ |
1085 | + 'TestSharedModel', |
1086 | + ] |
1087 | + |
1088 | + |
1089 | +import unittest |
1090 | + |
1091 | +from gi.repository import Dee |
1092 | +from gwibber.utils.model import SharedModel |
1093 | + |
1094 | + |
1095 | +class TestSharedModel(unittest.TestCase): |
1096 | + """Test our Dee.SharedModel instance.""" |
1097 | + |
1098 | + def test_basic_properties(self): |
1099 | + self.assertIsInstance(SharedModel, Dee.SharedModel) |
1100 | + self.assertEqual(SharedModel.get_n_columns(), 39) |
1101 | + self.assertEqual(SharedModel.get_n_rows(), 0) |
1102 | + self.assertEqual(SharedModel.get_schema(), |
1103 | + ['aas', 's', 's', 's', 's', 'b', 's', 's', 's', 's', |
1104 | + 's', 's', 's', 's', 's', 's', 'd', 'b', 's', 's', |
1105 | + 's', 's', 's', 's', 's', 's', 's', 's', 's', 's', |
1106 | + 's', 's', 's', 's', 's', 's', 's', 's', 's']) |
1107 | |
1108 | === modified file 'gwibber/gwibber/tests/test_protocols.py' |
1109 | --- gwibber/gwibber/tests/test_protocols.py 2012-09-13 17:50:35 +0000 |
1110 | +++ gwibber/gwibber/tests/test_protocols.py 2012-09-19 17:22:19 +0000 |
1111 | @@ -23,10 +23,24 @@ |
1112 | import unittest |
1113 | import threading |
1114 | |
1115 | +from gi.repository import Dee |
1116 | + |
1117 | from gwibber.protocols.flickr import Flickr |
1118 | from gwibber.protocols.twitter import Twitter |
1119 | from gwibber.utils.base import Base |
1120 | from gwibber.utils.manager import ProtocolManager |
1121 | +from gwibber.utils.model import SharedModel, COLUMN_TYPES |
1122 | + |
1123 | + |
1124 | +try: |
1125 | + # Python 3.3 |
1126 | + from unittest import mock |
1127 | +except ImportError: |
1128 | + import mock |
1129 | + |
1130 | + |
1131 | +TestModel = Dee.SharedModel.new('com.Gwibber.TestSharedModel') |
1132 | +TestModel.set_schema_full(COLUMN_TYPES) |
1133 | |
1134 | |
1135 | class TestProtocolManager(unittest.TestCase): |
1136 | @@ -73,6 +87,9 @@ |
1137 | class TestProtocols(unittest.TestCase): |
1138 | """Test protocol implementations.""" |
1139 | |
1140 | + def setUp(self): |
1141 | + TestModel.clear() |
1142 | + |
1143 | def test_protocol_info(self): |
1144 | # Each protocol carries with it a number of protocol variables. These |
1145 | # are initialized to None in the base class. |
1146 | @@ -126,6 +143,84 @@ |
1147 | thread.join() |
1148 | self.assertEqual(my_protocol.result, 'one:two') |
1149 | |
1150 | + @mock.patch('gwibber.utils.base.SharedModel', TestModel) |
1151 | + def test_shared_model_successfully_mocked(self): |
1152 | + self.assertEqual(SharedModel.get_n_rows(), 0) |
1153 | + self.assertEqual(TestModel.get_n_rows(), 0) |
1154 | + base = Base(None) |
1155 | + base._publish('a', 'b', 'c', message='a', |
1156 | + from_me=True, likes=1, liked=True) |
1157 | + base._publish('a', 'b', 'c', message='b', |
1158 | + from_me=True, likes=1, liked=True) |
1159 | + base._publish('a', 'b', 'c', message='c', |
1160 | + from_me=True, likes=1, liked=True) |
1161 | + self.assertEqual(SharedModel.get_n_rows(), 0) |
1162 | + self.assertEqual(TestModel.get_n_rows(), 3) |
1163 | + |
1164 | + @mock.patch('gwibber.utils.base.SharedModel', TestModel) |
1165 | + def test_invalid_argument(self): |
1166 | + base = Base(None) |
1167 | + self.assertEqual(0, TestModel.get_n_rows()) |
1168 | + self.assertRaises(TypeError, base._publish, invalid_argument='not good') |
1169 | + |
1170 | + @mock.patch('gwibber.utils.base.SharedModel', TestModel) |
1171 | + @mock.patch('gwibber.utils.base._seen_messages', {}) |
1172 | + def test_one_message(self): |
1173 | + base = Base(None) |
1174 | + self.assertEqual(0, TestModel.get_n_rows()) |
1175 | + self.assertTrue(base._publish( |
1176 | + service='facebook', account_id='1/facebook', |
1177 | + message_id='1234', stream='messages', sender='fred', |
1178 | + sender_nick='freddy', from_me=True, timestamp='today', |
1179 | + message='hello, @jimmy', likes=10, liked=True)) |
1180 | + self.assertEqual(1, TestModel.get_n_rows()) |
1181 | + |
1182 | + @mock.patch('gwibber.utils.base.SharedModel', TestModel) |
1183 | + @mock.patch('gwibber.utils.base._seen_messages', {}) |
1184 | + def test_duplicate_messages_identified(self): |
1185 | + base = Base(None) |
1186 | + self.assertEqual(0, TestModel.get_n_rows()) |
1187 | + self.assertTrue(base._publish( |
1188 | + service='facebook', account_id='1/facebook', |
1189 | + message_id='1234', stream='messages', sender='fred', |
1190 | + sender_nick='freddy', from_me=True, timestamp='today', |
1191 | + message='hello, @jimmy', likes=10, liked=True)) |
1192 | + |
1193 | + self.assertTrue(base._publish( |
1194 | + service='twitter', account_id='2/twitter', |
1195 | + message_id='5678', stream='messages', sender='fred', |
1196 | + sender_nick='freddy', from_me=True, timestamp='today', |
1197 | + message='hello jimmy', likes=10, liked=False)) |
1198 | + |
1199 | + self.assertEqual(1, TestModel.get_n_rows()) |
1200 | + self.assertEqual(TestModel.get_row(TestModel.get_first_iter())[7], |
1201 | + 'hello, @jimmy') |
1202 | + self.assertEqual(TestModel.get_row(TestModel.get_first_iter())[0], |
1203 | + [['facebook', '1/facebook', '1234'], |
1204 | + ['twitter', '2/twitter', '5678']]) |
1205 | + |
1206 | + @mock.patch('gwibber.utils.base.SharedModel', TestModel) |
1207 | + @mock.patch('gwibber.utils.base._seen_messages', {}) |
1208 | + def test_similar_messages_allowed(self): |
1209 | + base = Base(None) |
1210 | + self.assertEqual(0, TestModel.get_n_rows()) |
1211 | + |
1212 | + self.assertTrue(base._publish( |
1213 | + service='facebook', account_id='1/facebook', |
1214 | + message_id='1234', stream='messages', sender='fred', |
1215 | + sender_nick='freddy', from_me=True, timestamp='today', |
1216 | + message='hello, @jimmy', likes=10, liked=True)) |
1217 | + |
1218 | + self.assertEqual(1, TestModel.get_n_rows()) |
1219 | + |
1220 | + self.assertTrue(base._publish( |
1221 | + service='facebook', account_id='1/facebook', |
1222 | + message_id='34567', stream='messages', sender='tedtholomew', |
1223 | + sender_nick='teddy', from_me=False, timestamp='today', |
1224 | + message='hello, @jimmy', likes=10, liked=True)) |
1225 | + |
1226 | + self.assertEqual(2, TestModel.get_n_rows()) |
1227 | + |
1228 | def test_basic_login(self): |
1229 | # Try to log in twice. The second login attempt returns False because |
1230 | # it's already logged in. |
1231 | |
1232 | === removed file 'gwibber/gwibber/tests/test_storage.py' |
1233 | --- gwibber/gwibber/tests/test_storage.py 2012-08-21 21:35:08 +0000 |
1234 | +++ gwibber/gwibber/tests/test_storage.py 1970-01-01 00:00:00 +0000 |
1235 | @@ -1,688 +0,0 @@ |
1236 | -# Copyright (C) 2012 Canonical Ltd |
1237 | -# |
1238 | -# This program is free software: you can redistribute it and/or modify |
1239 | -# it under the terms of the GNU General Public License version 2 as |
1240 | -# published by the Free Software Foundation. |
1241 | -# |
1242 | -# This program is distributed in the hope that it will be useful, |
1243 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1244 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1245 | -# GNU General Public License for more details. |
1246 | -# |
1247 | -# You should have received a copy of the GNU General Public License |
1248 | -# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1249 | - |
1250 | -"""Test stream storage.""" |
1251 | - |
1252 | -__all__ = [ |
1253 | - 'TestMessageStorage', |
1254 | - 'TestSearchStorage', |
1255 | - 'TestStreamStorage', |
1256 | - ] |
1257 | - |
1258 | - |
1259 | -import sqlite3 |
1260 | -import unittest |
1261 | - |
1262 | -from gwibber.storage.messages import MessageStorage |
1263 | -from gwibber.storage.searches import SearchStorage |
1264 | -from gwibber.storage.streams import StreamStorage |
1265 | - |
1266 | -import gwibber.storage.messages |
1267 | - |
1268 | -try: |
1269 | - # Python 3.3 |
1270 | - from unittest import mock |
1271 | -except ImportError: |
1272 | - import mock |
1273 | - |
1274 | - |
1275 | -class TestStreamStorage(unittest.TestCase): |
1276 | - """Test stream storage.""" |
1277 | - |
1278 | - def setUp(self): |
1279 | - self.db = sqlite3.connect(':memory:') |
1280 | - |
1281 | - def test_table_creation(self): |
1282 | - rows = self.db.execute('PRAGMA table_info(streams)').fetchall() |
1283 | - self.assertEqual(len(rows), 0) |
1284 | - StreamStorage(self.db) |
1285 | - rows = self.db.execute('PRAGMA table_info(streams)').fetchall() |
1286 | - # 5 rows because there are 5 columns in the table. |
1287 | - self.assertEqual(len(rows), 5) |
1288 | - |
1289 | - def test_table_already_exists(self): |
1290 | - StreamStorage(self.db) |
1291 | - StreamStorage(self.db) |
1292 | - rows = self.db.execute('PRAGMA table_info(streams)').fetchall() |
1293 | - self.assertEqual(len(rows), 5) |
1294 | - |
1295 | - def test_get_data(self): |
1296 | - storage = StreamStorage(self.db) |
1297 | - self.db.execute('INSERT INTO streams VALUES (?, ?, ?, ?, ?)', |
1298 | - (1, 'myname', 'myaccount', 'operation', 'somedata')) |
1299 | - data = storage.get_data(1) |
1300 | - self.assertEqual(data, 'somedata') |
1301 | - |
1302 | - def test_get_data_missing(self): |
1303 | - storage = StreamStorage(self.db) |
1304 | - self.db.execute('INSERT INTO streams VALUES (?, ?, ?, ?, ?)', |
1305 | - (1, 'myname', 'myaccount', 'operation', 'somedata')) |
1306 | - data = storage.get_data(2) |
1307 | - self.assertEqual(data, '') |
1308 | - |
1309 | - def test_list_all_data(self): |
1310 | - storage = StreamStorage(self.db) |
1311 | - with self.db: |
1312 | - for sample_data in [ |
1313 | - (1, 'myname', 'myaccount', 'operation', 'somedata'), |
1314 | - (2, 'myname', 'myaccount', 'operation', 'moredata'), |
1315 | - (3, 'myname', 'myaccount', 'operation', 'anotherdata'), |
1316 | - ]: |
1317 | - self.db.execute( |
1318 | - 'INSERT INTO streams VALUES (?, ?, ?, ?, ?)', |
1319 | - sample_data) |
1320 | - data = storage.list_all_data() |
1321 | - self.assertEqual(data, '[somedata, moredata, anotherdata]') |
1322 | - |
1323 | - def test_add(self): |
1324 | - storage = StreamStorage(self.db) |
1325 | - storage.add(1, 'bob', 'ant', 'noop', 'data') |
1326 | - rows = self.db.execute('SELECT * FROM streams').fetchall() |
1327 | - self.assertEqual(len(rows), 1) |
1328 | - self.assertEqual(rows[0], ('1', 'bob', 'ant', 'noop', 'data')) |
1329 | - |
1330 | - def test_remove(self): |
1331 | - storage = StreamStorage(self.db) |
1332 | - with self.db: |
1333 | - for sample_data in [ |
1334 | - (1, 'myname', 'myaccount', 'operation', 'somedata'), |
1335 | - (2, 'myname', 'myaccount', 'operation', 'moredata'), |
1336 | - (3, 'myname', 'myaccount', 'operation', 'anotherdata'), |
1337 | - ]: |
1338 | - self.db.execute( |
1339 | - 'INSERT INTO streams VALUES (?, ?, ?, ?, ?)', |
1340 | - sample_data) |
1341 | - storage.remove(2) |
1342 | - rows = self.db.execute('SELECT * FROM streams').fetchall() |
1343 | - self.assertEqual(len(rows), 2) |
1344 | - self.assertEqual([row[0] for row in rows], ['1', '3']) |
1345 | - |
1346 | - def test_remove_missing(self): |
1347 | - storage = StreamStorage(self.db) |
1348 | - with self.db: |
1349 | - for sample_data in [ |
1350 | - (1, 'myname', 'myaccount', 'operation', 'somedata'), |
1351 | - (2, 'myname', 'myaccount', 'operation', 'moredata'), |
1352 | - (3, 'myname', 'myaccount', 'operation', 'anotherdata'), |
1353 | - ]: |
1354 | - self.db.execute( |
1355 | - 'INSERT INTO streams VALUES (?, ?, ?, ?, ?)', |
1356 | - sample_data) |
1357 | - storage.remove(5) |
1358 | - rows = self.db.execute('SELECT * FROM streams').fetchall() |
1359 | - self.assertEqual(len(rows), 3) |
1360 | - self.assertEqual([row[0] for row in rows], ['1', '2', '3']) |
1361 | - |
1362 | - |
1363 | -class TestMessageStorage(unittest.TestCase): |
1364 | - """Test message storage.""" |
1365 | - |
1366 | - def setUp(self): |
1367 | - self.db = sqlite3.connect(':memory:') |
1368 | - |
1369 | - def test_table_creation(self): |
1370 | - rows = self.db.execute('PRAGMA table_info(messages)').fetchall() |
1371 | - self.assertEqual(len(rows), 0) |
1372 | - MessageStorage(self.db) |
1373 | - rows = self.db.execute('PRAGMA table_info(messages)').fetchall() |
1374 | - # 14 rows because there are 14 columns in the table. |
1375 | - self.assertEqual(len(rows), 14) |
1376 | - |
1377 | - def test_table_already_exists(self): |
1378 | - MessageStorage(self.db) |
1379 | - MessageStorage(self.db) |
1380 | - rows = self.db.execute('PRAGMA table_info(messages)').fetchall() |
1381 | - self.assertEqual(len(rows), 14) |
1382 | - |
1383 | - def test_get_messages_sent_not_all(self): |
1384 | - # Test conditional arm 1. |
1385 | - storage = MessageStorage(self.db) |
1386 | - with self.db: |
1387 | - for sample_data in [ |
1388 | - # id, mid, account, service, operation, transient |
1389 | - # stream, time, text, from_me, to_me, sender, recipient, data |
1390 | - (1, 1, 'ant', '', '', '0', '', 100, '', 1, 0, '', '', 'row1'), |
1391 | - (2, 2, 'ant', '', '', '0', '', 101, '', 1, 0, '', '', 'row2'), |
1392 | - (3, 3, 'ant', '', '', '0', '', 99, '', 1, 0, '', '', 'row3'), |
1393 | - (4, 4, 'ant', '', '', '1', '', 100, '', 1, 0, '', '', 'row4'), |
1394 | - (5, 5, 'ant', '', '', '0', '', 100, '', 0, 0, '', '', 'row5'), |
1395 | - (6, 6, 'bee', '', '', '0', '', 100, '', 1, 0, '', '', 'row6'), |
1396 | - ]: |
1397 | - self.db.execute(""" |
1398 | - INSERT INTO messages VALUES |
1399 | - (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) |
1400 | - """, sample_data) |
1401 | - # This query should return rows 1 and 2 only. It will not return row |
1402 | - # 3 because its time is < 100. It will not return row4 because its |
1403 | - # transient != '0'. It will not return row5 because its from_me != |
1404 | - # 1. It will not return row6 because its account != 'ant'. |
1405 | - answer = storage.get_messages( |
1406 | - 'sent', 'ant', 100, '0', '', 'time', 'ASC', 0) |
1407 | - self.assertEqual(answer, '[row1, row2]') |
1408 | - |
1409 | - def test_get_messages_sent_not_all_order_desc(self): |
1410 | - # Test conditional arm 1 with descending order. |
1411 | - storage = MessageStorage(self.db) |
1412 | - with self.db: |
1413 | - for sample_data in [ |
1414 | - # id, mid, account, service, operation, transient |
1415 | - # stream, time, text, from_me, to_me, sender, recipient, data |
1416 | - (1, 1, 'ant', '', '', '0', '', 100, '', 1, 0, '', '', 'row1'), |
1417 | - (2, 2, 'ant', '', '', '0', '', 101, '', 1, 0, '', '', 'row2'), |
1418 | - (3, 3, 'ant', '', '', '0', '', 99, '', 1, 0, '', '', 'row3'), |
1419 | - (4, 4, 'ant', '', '', '1', '', 100, '', 1, 0, '', '', 'row4'), |
1420 | - (5, 5, 'ant', '', '', '0', '', 100, '', 0, 0, '', '', 'row5'), |
1421 | - (6, 6, 'bee', '', '', '0', '', 100, '', 1, 0, '', '', 'row6'), |
1422 | - ]: |
1423 | - self.db.execute(""" |
1424 | - INSERT INTO messages VALUES |
1425 | - (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) |
1426 | - """, sample_data) |
1427 | - # This query should return rows 1 and 2 only. It will not return row |
1428 | - # 3 because its time is < 100. It will not return row4 because its |
1429 | - # transient != '0'. It will not return row5 because its from_me != |
1430 | - # 1. It will not return row6 because its account != 'ant'. |
1431 | - answer = storage.get_messages( |
1432 | - 'sent', 'ant', 100, '0', '', 'time', 'DESC', 0) |
1433 | - self.assertEqual(answer, '[row2, row1]') |
1434 | - |
1435 | - def test_get_messages_sent_not_all_with_limit(self): |
1436 | - # Test conditional arm 1 with a limit. |
1437 | - storage = MessageStorage(self.db) |
1438 | - with self.db: |
1439 | - for sample_data in [ |
1440 | - # id, mid, account, service, operation, transient |
1441 | - # stream, time, text, from_me, to_me, sender, recipient, data |
1442 | - (1, 1, 'ant', '', '', '0', '', 100, '', 1, 0, '', '', 'row1'), |
1443 | - (2, 2, 'ant', '', '', '0', '', 101, '', 1, 0, '', '', 'row2'), |
1444 | - (3, 3, 'ant', '', '', '0', '', 99, '', 1, 0, '', '', 'row3'), |
1445 | - (4, 4, 'ant', '', '', '1', '', 100, '', 1, 0, '', '', 'row4'), |
1446 | - (5, 5, 'ant', '', '', '0', '', 100, '', 0, 0, '', '', 'row5'), |
1447 | - (6, 6, 'bee', '', '', '0', '', 100, '', 1, 0, '', '', 'row6'), |
1448 | - ]: |
1449 | - self.db.execute(""" |
1450 | - INSERT INTO messages VALUES |
1451 | - (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) |
1452 | - """, sample_data) |
1453 | - # This query should return row 1. It will not return row 3 because |
1454 | - # its time is < 100. It will not return row4 because its transient != |
1455 | - # '0'. It will not return row5 because its from_me != 1. It will not |
1456 | - # return row6 because its account != 'ant'. |
1457 | - # |
1458 | - # In addition to all that, we'll get just row 1 due to LIMIT 1. |
1459 | - answer = storage.get_messages( |
1460 | - 'sent', 'ant', 100, '0', '', 'time', 'ASC', 1) |
1461 | - self.assertEqual(answer, '[row1]') |
1462 | - |
1463 | - def test_get_messages_sent_all(self): |
1464 | - # Test conditional arm 2. |
1465 | - storage = MessageStorage(self.db) |
1466 | - with self.db: |
1467 | - for sample_data in [ |
1468 | - # id, mid, account, service, operation, transient |
1469 | - # stream, time, text, from_me, to_me, sender, recipient, data |
1470 | - (1, 1, 'ant', '', '', '0', '', 100, '', 1, 0, '', '', 'row1'), |
1471 | - (2, 2, 'ant', '', '', '0', '', 101, '', 1, 0, '', '', 'row2'), |
1472 | - (3, 3, 'ant', '', '', '0', '', 99, '', 1, 0, '', '', 'row3'), |
1473 | - (4, 4, 'ant', '', '', '1', '', 100, '', 1, 0, '', '', 'row4'), |
1474 | - (5, 5, 'ant', '', '', '0', '', 100, '', 0, 0, '', '', 'row5'), |
1475 | - (6, 6, 'bee', '', '', '0', '', 100, '', 1, 0, '', '', 'row6'), |
1476 | - ]: |
1477 | - self.db.execute(""" |
1478 | - INSERT INTO messages VALUES |
1479 | - (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) |
1480 | - """, sample_data) |
1481 | - # This query should return rows 1, 2, and 6. It will not return row 3 |
1482 | - # because its time is < 100. It will not return row4 because its |
1483 | - # transient != '0'. It will not return row5 because its from_me != 1. |
1484 | - answer = storage.get_messages( |
1485 | - 'sent', 'all', 100, '0', '', 'data', 'ASC', 0) |
1486 | - self.assertEqual(answer, '[row1, row2, row6]') |
1487 | - |
1488 | - def test_get_messages_not_transient_not_all_not_recipient(self): |
1489 | - # Test conditional arm 3, both legs of the OR clause. |
1490 | - storage = MessageStorage(self.db) |
1491 | - with self.db: |
1492 | - for sample_data in [ |
1493 | - # id, mid, account, service, operation, transient |
1494 | - # stream, time, text, from_me, to_me, sender, recipient, data |
1495 | - (1, 1, 'ant', '', '', '0', '', 100, '', 1, 0, '', '', 'row1'), |
1496 | - (2, 2, 'ant', '', '', '0', '', 101, '', 1, 0, '', '', 'row2'), |
1497 | - (3, 3, 'ant', '', '', '1', '', 99, '', 1, 0, '', '', 'row3'), |
1498 | - (4, 4, 'ant', '', '', '1', '', 100, '', 1, 0, '', '', 'row4'), |
1499 | - (5, 5, 'ant', '', '', '0', '', 100, '', 0, 0, '', '', 'row5'), |
1500 | - (6, 6, 'bee', '', '', '0', '', 100, '', 1, 0, '', '', 'row6'), |
1501 | - (7, 7, 'twitter', '', '', '0', '', 100, '', 1, 0, '', 'bob', |
1502 | - 'row7'), |
1503 | - ]: |
1504 | - self.db.execute(""" |
1505 | - INSERT INTO messages VALUES |
1506 | - (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) |
1507 | - """, sample_data) |
1508 | - # This query should return rows 4 and 7. It will not return row3 |
1509 | - # because its time is < 100. It will not return any other rows |
1510 | - # because their transients are not '1' or their recipients are not |
1511 | - # 'bob'. |
1512 | - answer = storage.get_messages( |
1513 | - 'stream', 'twitter', 100, '1', 'bob', 'data', 'ASC', 0) |
1514 | - self.assertEqual(answer, '[row4, row7]') |
1515 | - |
1516 | - def test_get_messages_not_all_not_all(self): |
1517 | - # Test conditional arm 4. |
1518 | - storage = MessageStorage(self.db) |
1519 | - with self.db: |
1520 | - for sample_data in [ |
1521 | - # id, mid, account, service, operation, transient, |
1522 | - # stream, time, text, from_me, to_me, sender, recipient, data |
1523 | - (1, 1, 'ant', '', '', '0', '', 100, '', 1, 0, '', '', 'row1'), |
1524 | - (2, 2, 'ant', '', '', '0', '', 101, '', 1, 0, '', '', 'row2'), |
1525 | - (3, 3, 'ant', '', '', '1', '', 99, '', 1, 0, '', '', 'row3'), |
1526 | - (4, 4, 'ant', '', '', '1', '', 100, '', 1, 0, '', '', 'row4'), |
1527 | - (5, 5, 'ant', '', '', '0', 'zee', 100, '', 0, 0, '', '', |
1528 | - 'row5'), |
1529 | - (6, 6, 'bee', '', '', '0', '', 100, '', 1, 0, '', '', 'row6'), |
1530 | - ]: |
1531 | - self.db.execute(""" |
1532 | - INSERT INTO messages VALUES |
1533 | - (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) |
1534 | - """, sample_data) |
1535 | - # This query should return row 5. That is the only row where the |
1536 | - # stream, account, time, and transient match the query. |
1537 | - answer = storage.get_messages( |
1538 | - 'zee', 'ant', 100, '0', '0', 'data', 'ASC', 0) |
1539 | - self.assertEqual(answer, '[row5]') |
1540 | - |
1541 | - def test_get_messages_all_not_all(self): |
1542 | - # Test conditional arm 5. |
1543 | - storage = MessageStorage(self.db) |
1544 | - with self.db: |
1545 | - for sample_data in [ |
1546 | - # id, mid, account, service, operation, transient, |
1547 | - # stream, time, text, from_me, to_me, sender, recipient, data |
1548 | - (1, 1, 'ant', '', '', '0', '', 100, '', 1, 0, '', '', 'row1'), |
1549 | - (2, 2, 'ant', '', '', '0', '', 101, '', 1, 0, '', '', 'row2'), |
1550 | - (3, 3, 'ant', '', '', '1', '', 99, '', 1, 0, '', '', 'row3'), |
1551 | - (4, 4, 'ant', '', '', '1', '', 100, '', 1, 0, '', '', 'row4'), |
1552 | - (5, 5, 'ant', '', '', '0', 'zee', 100, '', 0, 0, '', '', |
1553 | - 'row5'), |
1554 | - (6, 6, 'bee', '', '', '0', '', 100, '', 1, 0, '', '', 'row6'), |
1555 | - ]: |
1556 | - self.db.execute(""" |
1557 | - INSERT INTO messages VALUES |
1558 | - (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) |
1559 | - """, sample_data) |
1560 | - # This query should return rows 1, 2 and 5. All three rows match |
1561 | - # account ant, as well as time and transient. Row 3 does not match |
1562 | - # the time, row 4 does not match the transient, row 6 does not match |
1563 | - # the account. |
1564 | - answer = storage.get_messages( |
1565 | - 'all', 'ant', 100, '0', '0', 'data', 'ASC', 0) |
1566 | - self.assertEqual(answer, '[row1, row2, row5]') |
1567 | - |
1568 | - def test_get_messages_not_all_all(self): |
1569 | - # Test conditional arm 6. |
1570 | - storage = MessageStorage(self.db) |
1571 | - with self.db: |
1572 | - for sample_data in [ |
1573 | - # id, mid, account, service, operation, transient, |
1574 | - # stream, time, text, from_me, to_me, sender, recipient, data |
1575 | - (1, 1, 'ant', '', '', '0', '', 100, '', 1, 0, '', '', 'row1'), |
1576 | - (2, 2, 'ant', '', '', '0', '', 101, '', 1, 0, '', '', 'row2'), |
1577 | - (3, 3, 'ant', '', '', '1', '', 99, '', 1, 0, '', '', 'row3'), |
1578 | - (4, 4, 'ant', '', '', '1', '', 100, '', 1, 0, '', '', 'row4'), |
1579 | - (5, 5, 'ant', '', '', '0', 'zee', 100, '', 0, 0, '', '', |
1580 | - 'row5'), |
1581 | - (6, 6, 'bee', '', '', '0', '', 100, '', 1, 0, '', '', 'row6'), |
1582 | - ]: |
1583 | - self.db.execute(""" |
1584 | - INSERT INTO messages VALUES |
1585 | - (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) |
1586 | - """, sample_data) |
1587 | - # This query should return row 5. No other row has a matching stream. |
1588 | - answer = storage.get_messages( |
1589 | - 'zee', 'all', 100, '0', '0', 'data', 'ASC', 0) |
1590 | - self.assertEqual(answer, '[row5]') |
1591 | - |
1592 | - def test_get_messages_else(self): |
1593 | - # Test conditional arm 7. |
1594 | - storage = MessageStorage(self.db) |
1595 | - with self.db: |
1596 | - for sample_data in [ |
1597 | - # id, mid, account, service, operation, transient, |
1598 | - # stream, time, text, from_me, to_me, sender, recipient, data |
1599 | - (1, 1, 'ant', '', '', '0', '', 100, '', 1, 0, '', '', 'row1'), |
1600 | - (2, 2, 'ant', '', '', '0', '', 101, '', 1, 0, '', '', 'row2'), |
1601 | - (3, 3, 'ant', '', '', '1', '', 99, '', 1, 0, '', '', 'row3'), |
1602 | - (4, 4, 'ant', '', '', '1', '', 100, '', 1, 0, '', '', 'row4'), |
1603 | - (5, 5, 'ant', '', '', '0', 'zee', 100, '', 0, 0, '', '', |
1604 | - 'row5'), |
1605 | - (6, 6, 'bee', '', '', '0', '', 100, '', 1, 0, '', '', 'row6'), |
1606 | - (7, 7, 'bee', '', '', '0', '', 99, '', 1, 0, '', '', 'row7'), |
1607 | - ]: |
1608 | - self.db.execute(""" |
1609 | - INSERT INTO messages VALUES |
1610 | - (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) |
1611 | - """, sample_data) |
1612 | - # This query should return rows 1, 2, 5, and 6. Those rows have a |
1613 | - # matching time and transient. Rows 3 and 4 don't match transient, |
1614 | - # and row 7 doesn't match time. |
1615 | - answer = storage.get_messages( |
1616 | - 'all', 'all', 100, '0', '0', 'data', 'ASC', 0) |
1617 | - self.assertEqual(answer, '[row1, row2, row5, row6]') |
1618 | - |
1619 | - def test_get_messages_home(self): |
1620 | - # Test conditional arm 7, but by using the alias of stream == 'home' |
1621 | - # to mean stream == 'all' and account == 'all. IOW, the account value |
1622 | - # is ignored. |
1623 | - storage = MessageStorage(self.db) |
1624 | - with self.db: |
1625 | - for sample_data in [ |
1626 | - # id, mid, account, service, operation, transient, |
1627 | - # stream, time, text, from_me, to_me, sender, recipient, data |
1628 | - (1, 1, 'ant', '', '', '0', '', 100, '', 1, 0, '', '', 'row1'), |
1629 | - (2, 2, 'ant', '', '', '0', '', 101, '', 1, 0, '', '', 'row2'), |
1630 | - (3, 3, 'ant', '', '', '1', '', 99, '', 1, 0, '', '', 'row3'), |
1631 | - (4, 4, 'ant', '', '', '1', '', 100, '', 1, 0, '', '', 'row4'), |
1632 | - (5, 5, 'ant', '', '', '0', 'zee', 100, '', 0, 0, '', '', |
1633 | - 'row5'), |
1634 | - (6, 6, 'bee', '', '', '0', '', 100, '', 1, 0, '', '', 'row6'), |
1635 | - (7, 7, 'bee', '', '', '0', '', 99, '', 1, 0, '', '', 'row7'), |
1636 | - ]: |
1637 | - self.db.execute(""" |
1638 | - INSERT INTO messages VALUES |
1639 | - (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) |
1640 | - """, sample_data) |
1641 | - # This query should return rows 1, 2, 5, and 6. Those rows have a |
1642 | - # matching time and transient. Rows 3 and 4 don't match transient, |
1643 | - # and row 7 doesn't match time. |
1644 | - answer = storage.get_messages( |
1645 | - 'home', 'cat', 100, '0', '0', 'data', 'ASC', 0) |
1646 | - self.assertEqual(answer, '[row1, row2, row5, row6]') |
1647 | - |
1648 | - def test_get_data(self): |
1649 | - storage = MessageStorage(self.db) |
1650 | - with self.db: |
1651 | - for sample_data in [ |
1652 | - # id, mid, account, service, operation, transient, |
1653 | - # stream, time, text, from_me, to_me, sender, recipient, data |
1654 | - (1, 1, 'ant', '', '', '0', '', 100, '', 1, 0, '', '', 'row1'), |
1655 | - (2, 2, 'ant', '', '', '0', '', 101, '', 1, 0, '', '', 'row2'), |
1656 | - ]: |
1657 | - self.db.execute(""" |
1658 | - INSERT INTO messages VALUES |
1659 | - (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) |
1660 | - """, sample_data) |
1661 | - self.assertEqual(storage.get_data(2), 'row2') |
1662 | - |
1663 | - def test_get_data_missing(self): |
1664 | - storage = MessageStorage(self.db) |
1665 | - with self.db: |
1666 | - for sample_data in [ |
1667 | - # id, mid, account, service, operation, transient, |
1668 | - # stream, time, text, from_me, to_me, sender, recipient, data |
1669 | - (1, 1, 'ant', '', '', '0', '', 100, '', 1, 0, '', '', 'row1'), |
1670 | - (2, 2, 'ant', '', '', '0', '', 101, '', 1, 0, '', '', 'row2'), |
1671 | - ]: |
1672 | - self.db.execute(""" |
1673 | - INSERT INTO messages VALUES |
1674 | - (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) |
1675 | - """, sample_data) |
1676 | - self.assertEqual(storage.get_data(3), '') |
1677 | - |
1678 | - def test_maintenance_no_accounts(self): |
1679 | - # Initialize the table. |
1680 | - MessageStorage(self.db) |
1681 | - # Calling maintenance() with no accounts should do nothing. |
1682 | - class FakeAccountManager: |
1683 | - def get_account(self, global_id): |
1684 | - return False |
1685 | - with self.db: |
1686 | - MessageStorage(self.db).maintenance(FakeAccountManager()) |
1687 | - results = self.db.execute('SELECT account FROM messages').fetchall() |
1688 | - self.assertEqual(len(results), 0) |
1689 | - |
1690 | - def test_maintenance_on_non_account(self): |
1691 | - # Initialize the table. |
1692 | - MessageStorage(self.db) |
1693 | - # Put some sample data into the messages table, but with an account |
1694 | - # that does not match the format expected by the maintenance() |
1695 | - # function, which it interprets as a missing account. Every row gets |
1696 | - # deleted. |
1697 | - with self.db: |
1698 | - for sample_data in [ |
1699 | - # id, mid, account, service, operation, transient, |
1700 | - # stream, time, text, from_me, to_me, sender, recipient, data |
1701 | - (1, 1, 'ant', '', '', '0', '', 100, '', 1, 0, '', '', 'row1'), |
1702 | - (2, 2, 'ant', '', '', '0', '', 101, '', 1, 0, '', '', 'row2'), |
1703 | - ]: |
1704 | - self.db.execute(""" |
1705 | - INSERT INTO messages VALUES |
1706 | - (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) |
1707 | - """, sample_data) |
1708 | - class FakeAccountManager: |
1709 | - def get_account(self, global_id): |
1710 | - return False |
1711 | - with self.db: |
1712 | - MessageStorage(self.db).maintenance(FakeAccountManager()) |
1713 | - results = self.db.execute('SELECT id FROM messages').fetchall() |
1714 | - self.assertEqual(len(results), 0) |
1715 | - |
1716 | - def test_maintenance_on_missing_account(self): |
1717 | - # Initialize the table. |
1718 | - MessageStorage(self.db) |
1719 | - # Put some sample data into the messages table, but with accounts |
1720 | - # we'll treat as both existing, and missing. |
1721 | - # deleted. |
1722 | - with self.db: |
1723 | - for sample_data in [ |
1724 | - # id, mid, account, service, operation, transient, |
1725 | - # stream, time, text, from_me, to_me, sender, recipient, data |
1726 | - (1, 1, 'ant/foo', '', '', '0', '', 100, '', 1, 0, '', '', ''), |
1727 | - (2, 2, 'ant/foo', '', '', '0', '', 101, '', 1, 0, '', '', ''), |
1728 | - (3, 3, 'bee/foo', '', '', '0', '', 100, '', 1, 0, '', '', ''), |
1729 | - (4, 4, 'bee/foo', '', '', '0', '', 101, '', 1, 0, '', '', ''), |
1730 | - ]: |
1731 | - self.db.execute(""" |
1732 | - INSERT INTO messages VALUES |
1733 | - (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) |
1734 | - """, sample_data) |
1735 | - class FakeAccountManager: |
1736 | - def get_account(self, global_id): |
1737 | - return global_id == 'bee' |
1738 | - with self.db: |
1739 | - MessageStorage(self.db).maintenance(FakeAccountManager()) |
1740 | - results = self.db.execute('SELECT id FROM messages').fetchall() |
1741 | - self.assertEqual(sorted(row[0] for row in results), ['3', '4']) |
1742 | - |
1743 | - @mock.patch.dict(gwibber.storage.messages.__dict__, dict(MAX_COUNT=3)) |
1744 | - def test_maintenance(self): |
1745 | - # Initialize the table. |
1746 | - MessageStorage(self.db) |
1747 | - # A bunch of messages in various accounts are in the table. We're |
1748 | - # maxed out at three entries per account. The 'ant' account has two |
1749 | - # entries, the 'bee' account has three, the 'cat' account has four, |
1750 | - # and the 'dog' account has five. After maintenance() is run, no |
1751 | - # account has more than 3 entries, and all of them are the latest |
1752 | - # ones added. |
1753 | - with self.db: |
1754 | - for sample_data in [ |
1755 | - # id, mid, account, service, operation, transient, |
1756 | - # stream, time, text, from_me, to_me, sender, recipient, data |
1757 | - (1, 1, 'ant/foo', '', 'receive', '0', 'messages', |
1758 | - 100, '', 1, 0, '', '', ''), |
1759 | - (2, 2, 'ant/foo', '', 'receive', '0', 'messages', |
1760 | - 101, '', 1, 0, '', '', ''), |
1761 | - (3, 3, 'bee/foo', '', 'receive', '0', 'messages', |
1762 | - 100, '', 1, 0, '', '', ''), |
1763 | - (4, 4, 'bee/foo', '', 'receive', '0', 'messages', |
1764 | - 101, '', 1, 0, '', '', ''), |
1765 | - (5, 5, 'bee/foo', '', 'receive', '0', 'messages', |
1766 | - 102, '', 1, 0, '', '', ''), |
1767 | - (6, 6, 'cat/foo', '', 'receive', '0', 'messages', |
1768 | - 100, '', 1, 0, '', '', ''), |
1769 | - (7, 7, 'cat/foo', '', 'receive', '0', 'messages', |
1770 | - 101, '', 1, 0, '', '', ''), |
1771 | - (8, 8, 'cat/foo', '', 'receive', '0', 'messages', |
1772 | - 102, '', 1, 0, '', '', ''), |
1773 | - (9, 9, 'cat/foo', '', 'receive', '0', 'messages', |
1774 | - 103, '', 1, 0, '', '', ''), |
1775 | - (10, 10, 'dog/foo', '', 'receive', '0', 'messages', |
1776 | - 100, '', 1, 0, '', '', ''), |
1777 | - (11, 11, 'dog/foo', '', 'receive', '0', 'messages', |
1778 | - 101, '', 1, 0, '', '', ''), |
1779 | - (12, 12, 'dog/foo', '', 'receive', '0', 'messages', |
1780 | - 102, '', 1, 0, '', '', ''), |
1781 | - (13, 13, 'dog/foo', '', 'receive', '0', 'messages', |
1782 | - 103, '', 1, 0, '', '', ''), |
1783 | - (14, 14, 'dog/foo', '', 'receive', '0', 'messages', |
1784 | - 104, '', 1, 0, '', '', ''), |
1785 | - ]: |
1786 | - self.db.execute(""" |
1787 | - INSERT INTO messages VALUES |
1788 | - (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) |
1789 | - """, sample_data) |
1790 | - class FakeAccountManager: |
1791 | - def get_account(self, global_id): |
1792 | - return True |
1793 | - with self.db: |
1794 | - MessageStorage(self.db).maintenance(FakeAccountManager()) |
1795 | - results = self.db.execute( |
1796 | - 'SELECT id, account FROM messages').fetchall() |
1797 | - ids_by_account = {} |
1798 | - for id, account in results: |
1799 | - global_id, slash, rest = account.partition('/') |
1800 | - ids_by_account.setdefault(global_id, []).append(int(id)) |
1801 | - # There are fewer 'ant' messages than the maximum, so all are kept. |
1802 | - self.assertEqual(ids_by_account['ant'], [1, 2]) |
1803 | - # There exactly the max number of 'bee' messages, so all are kept. |
1804 | - self.assertEqual(ids_by_account['bee'], [3, 4, 5]) |
1805 | - # There is one too many 'cat' messages, so the earliest one (i.e. with |
1806 | - # id = 6, time = 100) is deleted. The later ones are kept. |
1807 | - self.assertEqual(ids_by_account['cat'], [7, 8, 9]) |
1808 | - # There are two too many 'dog' messages, so the earliest ones |
1809 | - # (i.e. with ids 10 and 11, times 100 and 101) are deleted. The later |
1810 | - # ones are kept. |
1811 | - self.assertEqual(ids_by_account['dog'], [12, 13, 14]) |
1812 | - |
1813 | - |
1814 | -class TestSearchStorage(unittest.TestCase): |
1815 | - """Test the search storage.""" |
1816 | - |
1817 | - def setUp(self): |
1818 | - self.db = sqlite3.connect(':memory:') |
1819 | - |
1820 | - def test_table_creation(self): |
1821 | - rows = self.db.execute('PRAGMA table_info(searches)').fetchall() |
1822 | - self.assertEqual(len(rows), 0) |
1823 | - SearchStorage(self.db) |
1824 | - rows = self.db.execute('PRAGMA table_info(searches)').fetchall() |
1825 | - # 4 rows because there are 4 columns in the table. |
1826 | - self.assertEqual(len(rows), 4) |
1827 | - |
1828 | - def test_table_already_exists(self): |
1829 | - SearchStorage(self.db) |
1830 | - SearchStorage(self.db) |
1831 | - rows = self.db.execute('PRAGMA table_info(searches)').fetchall() |
1832 | - self.assertEqual(len(rows), 4) |
1833 | - |
1834 | - def test_get_data(self): |
1835 | - storage = SearchStorage(self.db) |
1836 | - with self.db: |
1837 | - for sample_data in [ |
1838 | - # id, name, query, data |
1839 | - (1, 'anne', '', 'one'), |
1840 | - (2, 'bart', '', 'two'), |
1841 | - (3, 'cate', '', 'three'), |
1842 | - (4, 'dave', '', 'four'), |
1843 | - ]: |
1844 | - self.db.execute(""" |
1845 | - INSERT INTO searches VALUES (?, ?, ?, ?) |
1846 | - """, sample_data) |
1847 | - data = storage.get_data(3) |
1848 | - self.assertEqual(data, 'three') |
1849 | - |
1850 | - def test_get_data_missing(self): |
1851 | - storage = SearchStorage(self.db) |
1852 | - with self.db: |
1853 | - for sample_data in [ |
1854 | - # id, name, query, data |
1855 | - (1, 'anne', '', 'one'), |
1856 | - (2, 'bart', '', 'two'), |
1857 | - (3, 'cate', '', 'three'), |
1858 | - (4, 'dave', '', 'four'), |
1859 | - ]: |
1860 | - self.db.execute(""" |
1861 | - INSERT INTO searches VALUES (?, ?, ?, ?) |
1862 | - """, sample_data) |
1863 | - data = storage.get_data(10) |
1864 | - self.assertEqual(data, '') |
1865 | - |
1866 | - def test_list_all_data(self): |
1867 | - storage = SearchStorage(self.db) |
1868 | - with self.db: |
1869 | - for sample_data in [ |
1870 | - # id, name, query, data |
1871 | - (1, 'anne', '', 'one'), |
1872 | - (2, 'bart', '', 'two'), |
1873 | - (3, 'cate', '', 'three'), |
1874 | - (4, 'dave', '', 'four'), |
1875 | - ]: |
1876 | - self.db.execute(""" |
1877 | - INSERT INTO searches VALUES (?, ?, ?, ?) |
1878 | - """, sample_data) |
1879 | - data = storage.list_all_data() |
1880 | - self.assertEqual(data, '[one, two, three, four]') |
1881 | - |
1882 | - def test_add(self): |
1883 | - storage = SearchStorage(self.db) |
1884 | - storage.add(1, 'bob', 'myquery', 'mydata') |
1885 | - rows = self.db.execute('SELECT * FROM searches').fetchall() |
1886 | - self.assertEqual(len(rows), 1) |
1887 | - id, name, query, data = rows[0] |
1888 | - self.assertEqual(id, '1') |
1889 | - self.assertEqual(name, 'bob') |
1890 | - self.assertEqual(query, 'myquery') |
1891 | - self.assertEqual(data, 'mydata') |
1892 | - |
1893 | - def test_remove(self): |
1894 | - storage = SearchStorage(self.db) |
1895 | - with self.db: |
1896 | - for sample_data in [ |
1897 | - (1, 'anne', 'myquery', 'one'), |
1898 | - (2, 'bart', 'myquery', 'two'), |
1899 | - (3, 'cate', 'myquery', 'three'), |
1900 | - ]: |
1901 | - self.db.execute( |
1902 | - 'INSERT INTO searches VALUES (?, ?, ?, ?)', |
1903 | - sample_data) |
1904 | - storage.remove(2) |
1905 | - rows = self.db.execute('SELECT * FROM searches').fetchall() |
1906 | - self.assertEqual(len(rows), 2) |
1907 | - self.assertEqual([row[0] for row in rows], ['1', '3']) |
1908 | - |
1909 | - def test_remove_missing(self): |
1910 | - storage = SearchStorage(self.db) |
1911 | - with self.db: |
1912 | - for sample_data in [ |
1913 | - (1, 'anne', 'myquery', 'one'), |
1914 | - (2, 'bart', 'myquery', 'two'), |
1915 | - (3, 'cate', 'myquery', 'three'), |
1916 | - ]: |
1917 | - self.db.execute( |
1918 | - 'INSERT INTO searches VALUES (?, ?, ?, ?)', |
1919 | - sample_data) |
1920 | - storage.remove(5) |
1921 | - rows = self.db.execute('SELECT * FROM searches').fetchall() |
1922 | - self.assertEqual(len(rows), 3) |
1923 | - self.assertEqual([row[0] for row in rows], ['1', '2', '3']) |
1924 | |
1925 | === modified file 'gwibber/gwibber/utils/account.py' |
1926 | --- gwibber/gwibber/utils/account.py 2012-09-13 17:50:35 +0000 |
1927 | +++ gwibber/gwibber/utils/account.py 2012-09-19 17:22:19 +0000 |
1928 | @@ -29,7 +29,7 @@ |
1929 | |
1930 | from gwibber.errors import UnsupportedProtocolError |
1931 | from gwibber.utils.manager import protocol_manager |
1932 | - |
1933 | +from gwibber.utils.model import SharedModel, COLUMN_NAMES |
1934 | |
1935 | log = logging.getLogger('gwibber.accounts') |
1936 | |
1937 | @@ -37,9 +37,8 @@ |
1938 | class AccountManager: |
1939 | """Manage the accounts that we know about.""" |
1940 | |
1941 | - def __init__(self, refresh_callback, streams): |
1942 | + def __init__(self, refresh_callback): |
1943 | self._callback = refresh_callback |
1944 | - self._streams = streams |
1945 | self._accounts = {} |
1946 | # Ask libaccounts for a manager of the microblogging services. |
1947 | # Connect callbacks to the manager so that we can react when accounts |
1948 | @@ -71,9 +70,11 @@ |
1949 | except KeyError: |
1950 | log.debug('Attempting to delete unknown account: {}', account_id) |
1951 | return |
1952 | - for stream in json.loads(self._streams.List()): |
1953 | - if stream['account'] == account['id']: |
1954 | - self._streams.Delete(stream['id']) |
1955 | + |
1956 | + for row in SharedModel: |
1957 | + if account_id in row[COLUMN_NAMES.index('message_ids')]: |
1958 | + SharedModel.remove(row) |
1959 | + |
1960 | self._callback() |
1961 | |
1962 | def add_new_account(self, account_service): |
1963 | |
1964 | === modified file 'gwibber/gwibber/utils/base.py' |
1965 | --- gwibber/gwibber/utils/base.py 2012-09-13 17:50:35 +0000 |
1966 | +++ gwibber/gwibber/utils/base.py 2012-09-19 17:22:19 +0000 |
1967 | @@ -20,8 +20,34 @@ |
1968 | ] |
1969 | |
1970 | |
1971 | -from threading import Thread |
1972 | - |
1973 | +import string |
1974 | + |
1975 | +from threading import Thread, Lock |
1976 | + |
1977 | +from gwibber.utils.model import SharedModel, COLUMN_INDICES |
1978 | +from gwibber.utils.model import SENDER, MESSAGE, IDS |
1979 | + |
1980 | + |
1981 | +IGNORED = string.punctuation + ' ' |
1982 | + |
1983 | +def strip(message): |
1984 | + """Return a string suitable for fuzzy comparisons. |
1985 | + |
1986 | + This is used to identify highly similar but not quite identical duplicate |
1987 | + messages, so that we don't display those duplicates to the user. |
1988 | + """ |
1989 | + message = message.replace('http://', '') |
1990 | + message = message.replace('https://', '') |
1991 | + message = message.replace('gwibber:/', '') |
1992 | + return ''.join(char for char in message if char not in IGNORED) |
1993 | + |
1994 | + |
1995 | +# This dict maps messages stripped by the above function to the |
1996 | +# DeeModelIters that represent their rows in the Dee.SharedModel. |
1997 | +# It's used for convenience in finding duplicates. |
1998 | +_seen_messages = {} |
1999 | + |
2000 | +_publish_lock = Lock() |
2001 | |
2002 | class Base: |
2003 | class info: |
2004 | @@ -53,6 +79,44 @@ |
2005 | thread.daemon = True |
2006 | thread.start() |
2007 | |
2008 | + def _publish(self, service, account_id, message_id, **kwargs): |
2009 | + """Publish fresh data into the stream, without duplicating anything. |
2010 | + |
2011 | + Returns True if the message was appended, or if the message was |
2012 | + already present, returns False if unable to append new messagee. |
2013 | + """ |
2014 | + args = [''] * len(COLUMN_INDICES) |
2015 | + # See gwibber/utils/model.py for an explanation of this unique column. |
2016 | + args[IDS] = [[service, account_id, message_id]] |
2017 | + |
2018 | + # The basic idea is that you call _publish() with keyword |
2019 | + # arguments only, making the calling code much more explicit, |
2020 | + # and then this converts it from an unordered dict into the |
2021 | + # ordered list required by SharedModel.append(). |
2022 | + bad_kwargs = [] |
2023 | + for column_name, value in kwargs.items(): |
2024 | + index = COLUMN_INDICES.get(column_name) |
2025 | + if index is None: |
2026 | + bad_kwargs.append(column_name) |
2027 | + else: |
2028 | + args[index] = value |
2029 | + if len(bad_kwargs) > 0: |
2030 | + raise TypeError('Unexpected keyword arguments: {}'.format( |
2031 | + ', '.join(bad_kwargs))) |
2032 | + |
2033 | + with _publish_lock: |
2034 | + # Don't let duplicate messages into the model, but do |
2035 | + # record the unique message ids of each duplicate message. |
2036 | + stripped = strip(args[SENDER] + args[MESSAGE]) |
2037 | + row_iter = _seen_messages.get(stripped) |
2038 | + if row_iter is None: |
2039 | + _seen_messages[stripped] = SharedModel.append(*args) |
2040 | + else: |
2041 | + row = SharedModel.get_row(row_iter) |
2042 | + row[IDS] = row[IDS] + args[IDS] |
2043 | + |
2044 | + return stripped in _seen_messages |
2045 | + |
2046 | def _login(self): |
2047 | """Prevent redundant login attempts.""" |
2048 | # The first time the user logs in, we expect old_token to be None. |
2049 | |
2050 | === added file 'gwibber/gwibber/utils/model.py' |
2051 | --- gwibber/gwibber/utils/model.py 1970-01-01 00:00:00 +0000 |
2052 | +++ gwibber/gwibber/utils/model.py 2012-09-19 17:22:19 +0000 |
2053 | @@ -0,0 +1,109 @@ |
2054 | +# Copyright (C) 2012 Canonical Ltd |
2055 | +# |
2056 | +# This program is free software: you can redistribute it and/or modify |
2057 | +# it under the terms of the GNU General Public License version 2 as |
2058 | +# published by the Free Software Foundation. |
2059 | +# |
2060 | +# This program is distributed in the hope that it will be useful, |
2061 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
2062 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
2063 | +# GNU General Public License for more details. |
2064 | +# |
2065 | +# You should have received a copy of the GNU General Public License |
2066 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
2067 | + |
2068 | +"""The Dee.SharedModel interface layer. |
2069 | + |
2070 | +Dee.SharedModel is comparable to a Gtk.ListStore, except that it |
2071 | +shares its state between processes using DBus. When gwibber-service |
2072 | +downloads new messages from a website, it inserts those messages into |
2073 | +this SharedModel instance, which then triggers a callback in the Vala |
2074 | +frontend, which knows to display the new messages there. |
2075 | +""" |
2076 | + |
2077 | + |
2078 | +__all__ = [ |
2079 | + 'SharedModel', |
2080 | + 'COLUMN_NAMES', |
2081 | + 'COLUMN_TYPES', |
2082 | + 'COLUMN_INDICES', |
2083 | + 'SENDER', |
2084 | + 'MESSAGE', |
2085 | + 'IDS', |
2086 | + ] |
2087 | + |
2088 | + |
2089 | +from gi.repository import Dee |
2090 | + |
2091 | + |
2092 | +# Most of this schema is very straightforward, but the 'message_ids' |
2093 | +# column needs a bit of explanation: |
2094 | +# |
2095 | +# It is a two-dimensional array (ie, an array of arrays). Each inner |
2096 | +# array contains three elements: the name of the protocol, the |
2097 | +# account_id (like '6/flickr' or '3/facebook'), followed by the |
2098 | +# message_id for that particular service. |
2099 | +# |
2100 | +# Then, there will be one of these triples present for every service |
2101 | +# on which the message exists. So for example, if the user posts the |
2102 | +# same message to both facebook and twitter, that message will appear |
2103 | +# as a single row in this schema, and the 'service' column will look |
2104 | +# something like this: |
2105 | +# |
2106 | +# [ |
2107 | +# ['facebook', '2/facebook', '12345'], |
2108 | +# ['twitter', '3/twitter', '987654'], |
2109 | +# ] |
2110 | +SCHEMA = ( |
2111 | + ('message_ids', 'aas'), |
2112 | + ('stream', 's'), |
2113 | + ('transient', 's'), |
2114 | + ('sender', 's'), |
2115 | + ('sender_nick', 's'), |
2116 | + ('from_me', 'b'), |
2117 | + ('timestamp', 's'), |
2118 | + ('message', 's'), |
2119 | + ('html', 's'), |
2120 | + ('icon_uri', 's'), |
2121 | + ('url', 's'), |
2122 | + ('source', 's'), |
2123 | + ('timestring', 's'), |
2124 | + ('reply_nick', 's'), |
2125 | + ('reply_name', 's'), |
2126 | + ('reply_url', 's'), |
2127 | + ('likes', 'd'), |
2128 | + ('liked', 'b'), |
2129 | + ('retweet_nick', 's'), |
2130 | + ('retweet_name', 's'), |
2131 | + ('retweet_id', 's'), |
2132 | + ('link_picture', 's'), |
2133 | + ('link_name', 's'), |
2134 | + ('link_url', 's'), |
2135 | + ('link_desc', 's'), |
2136 | + ('link_caption', 's'), |
2137 | + ('link_icon', 's'), |
2138 | + ('img_url', 's'), |
2139 | + ('img_src', 's'), |
2140 | + ('img_thumb', 's'), |
2141 | + ('img_name', 's'), |
2142 | + ('video_pic', 's'), |
2143 | + ('video_src', 's'), |
2144 | + ('video_url', 's'), |
2145 | + ('video_name', 's'), |
2146 | + ('comments', 's'), |
2147 | + ('recipient', 's'), |
2148 | + ('recipient_nick', 's'), |
2149 | + ('recipient_icon', 's'), |
2150 | + ) |
2151 | + |
2152 | + |
2153 | +COLUMN_NAMES, COLUMN_TYPES = zip(*SCHEMA) |
2154 | + |
2155 | +COLUMN_INDICES = {name: i for i, name in enumerate(COLUMN_NAMES)} |
2156 | +SENDER = COLUMN_INDICES['sender'] |
2157 | +MESSAGE = COLUMN_INDICES['message'] |
2158 | +IDS = COLUMN_INDICES['message_ids'] |
2159 | + |
2160 | + |
2161 | +SharedModel = Dee.SharedModel.new('com.Gwibber.Streams') |
2162 | +SharedModel.set_schema_full(COLUMN_TYPES) |
Also, is Dee threadsafe? Because if it isn't, then we're going to have to give it a threading.Lock instance so that no two threads try to publish at the same time.