Merge lp:~jderose/dmedia/filestore-ui into lp:dmedia

Proposed by Jason Gerard DeRose
Status: Merged
Merged at revision: 345
Proposed branch: lp:~jderose/dmedia/filestore-ui
Merge into: lp:dmedia
Diff against target: 5276 lines (+517/-4500)
32 files modified
dmedia-service (+29/-10)
dmedia/backends/__init__.py (+0/-34)
dmedia/backends/s3.py (+0/-184)
dmedia/backends/tests/__init__.py (+0/-24)
dmedia/backends/tests/test_s3.py (+0/-103)
dmedia/backends/tests/test_torrent.py (+0/-69)
dmedia/backends/torrent.py (+0/-70)
dmedia/core.py (+5/-6)
dmedia/gst/__init__.py (+0/-25)
dmedia/gst/tests/__init__.py (+0/-24)
dmedia/gst/tests/test_transcoder.py (+0/-294)
dmedia/gst/transcoder.py (+0/-238)
dmedia/gtk/client.py (+0/-238)
dmedia/gtk/firstrun.py (+0/-279)
dmedia/gtk/menu.py (+0/-408)
dmedia/gtk/service.py (+0/-302)
dmedia/gtk/tests/test_client.py (+0/-284)
dmedia/gtk/tests/test_firstrun.py (+0/-35)
dmedia/gtk/tests/test_widgets.py (+0/-142)
dmedia/gtk/widgets.py (+0/-225)
dmedia/service/tests/test_api.py (+0/-110)
dmedia/service/tests/test_udisks.py (+183/-0)
dmedia/service/udisks.py (+299/-0)
dmedia/webui/__init__.py (+0/-27)
dmedia/webui/js.py (+0/-552)
dmedia/webui/tests/__init__.py (+0/-24)
dmedia/webui/tests/test_basejs.py (+0/-69)
dmedia/webui/tests/test_browserjs.py (+0/-38)
dmedia/webui/tests/test_couchjs.py (+0/-97)
dmedia/webui/tests/test_js.py (+0/-491)
dmedia/webui/tests/test_uploaderjs.py (+0/-90)
setup.py (+1/-8)
To merge this branch: bzr merge lp:~jderose/dmedia/filestore-ui
Reviewer Review Type Date Requested Status
David Jordan Approve
Review via email: mp+100967@code.launchpad.net

Description of the change

This mostly deletes a bunch of stale Python2 files that weren't being used anymore.

It also fixes setup.py so that doctests and unittests are run for every module (this was changed temporarily quite a while ago during the big refactor).

Last, this real change in this merge is to revamp how dmedia-service uses UDisks. We now do an initial scan of all mounted partitions to find out about any existing FileStore... this fixes the problem of dmedia only properly detected removable drives if you inserted the drive *after* dmedia started.

To post a comment you must log in.
Revision history for this message
David Jordan (dmj726) wrote :

The UDisks change looks really good, and we need to be dunning tests. This looks good.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'dmedia-service'
2--- dmedia-service 2012-03-25 22:39:44 +0000
3+++ dmedia-service 2012-04-05 13:22:39 +0000
4@@ -41,7 +41,7 @@
5
6 import dmedia
7 from dmedia.core import Core, start_file_server
8-from dmedia.service.dbus import UDisks
9+from dmedia.service.udisks import UDisks
10 from dmedia.service.avahi import Avahi
11
12
13@@ -75,8 +75,6 @@
14 if self.env_s is None:
15 self.usercouch = usercouch.UserCouch(dmedia.get_dmedia_dir())
16 env = self.usercouch.bootstrap()
17- interval = 5 * 60 * 1000 # Autocompact every 5 minutes
18- self._timeout_id = GObject.timeout_add(interval, self._autocompact)
19 else:
20 env = json.loads(self.env_s)
21 self.udisks = UDisks()
22@@ -86,8 +84,9 @@
23 self.core.add_filestore('/home')
24 self.udisks.connect('store_added', self.on_store_added)
25 self.udisks.connect('store_removed', self.on_store_removed)
26- self.udisks.monitor()
27-
28+ self.udisks.connect('card_added', self.on_card_added)
29+ self.udisks.connect('card_removed', self.on_card_removed)
30+
31 def start_httpd(self):
32 (self.httpd, self.port) = start_file_server(self.core.env)
33 self.avahi = Avahi(self.core.env, self.port)
34@@ -95,8 +94,15 @@
35
36 def run(self):
37 self.start_core()
38+ GObject.timeout_add(500, self.on_idle)
39+ self.mainloop.run()
40+
41+ def on_idle(self):
42+ self.udisks.monitor()
43 self.start_httpd()
44- self.mainloop.run()
45+ interval = 5 * 60 * 1000 # Autocompact every 5 minutes
46+ self._timeout_id = GObject.timeout_add(interval, self._autocompact)
47+ return False # Don't repeat idle call
48
49 def _autocompact(self):
50 log.info('UserCouch.autocompact()...')
51@@ -117,20 +123,32 @@
52 self.httpd.join()
53 self.mainloop.quit()
54
55- def on_store_added(self, udisks, obj, parentdir, partition, drive):
56- log.info('UDisks store_added: %r', parentdir)
57+ def on_store_added(self, udisks, obj, parentdir, store_id):
58 try:
59 self.AddFileStore(parentdir)
60 except Exception:
61 log.exception('Could not add FileStore %r', parentdir)
62
63- def on_store_removed(self, udisks, obj, parentdir):
64- log.info('UDisks store_removed: %r', parentdir)
65+ def on_store_removed(self, udisks, obj, parentdir, store_id):
66 try:
67 self.RemoveFileStore(parentdir)
68 except Exception:
69 log.exception('Could not remove FileStore %r', parentdir)
70
71+ def on_card_added(self, udisks, obj, mount):
72+ self.CardAdded(obj, mount)
73+
74+ def on_card_removed(self, udisks, obj, mount):
75+ self.CardRemoved(obj, mount)
76+
77+ @dbus.service.signal(IFACE, signature='ss')
78+ def CardAdded(self, obj, mount):
79+ pass
80+
81+ @dbus.service.signal(IFACE, signature='ss')
82+ def CardRemoved(self, obj, mount):
83+ pass
84+
85 @dbus.service.method(IFACE, in_signature='', out_signature='s')
86 def Version(self):
87 """
88@@ -151,6 +169,7 @@
89 """
90 Return dmedia env as JSON string.
91 """
92+ log.info('GetEnv')
93 return self.env_s
94
95 @dbus.service.method(IFACE, in_signature='', out_signature='s')
96
97=== removed directory 'dmedia/backends'
98=== removed file 'dmedia/backends/__init__.py'
99--- dmedia/backends/__init__.py 2011-04-25 09:03:19 +0000
100+++ dmedia/backends/__init__.py 1970-01-01 00:00:00 +0000
101@@ -1,34 +0,0 @@
102-# Authors:
103-# Jason Gerard DeRose <jderose@novacut.com>
104-#
105-# dmedia: distributed media library
106-# Copyright (C) 2011 Jason Gerard DeRose <jderose@novacut.com>
107-#
108-# This file is part of `dmedia`.
109-#
110-# `dmedia` is free software: you can redistribute it and/or modify it under the
111-# terms of the GNU Affero General Public License as published by the Free
112-# Software Foundation, either version 3 of the License, or (at your option) any
113-# later version.
114-#
115-# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY
116-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
117-# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
118-# details.
119-#
120-# You should have received a copy of the GNU Affero General Public License along
121-# with `dmedia`. If not, see <http://www.gnu.org/licenses/>.
122-
123-"""
124-Plugins for transfer backends for uploading and downloading files.
125-"""
126-
127-try:
128- from . import s3
129-except ImportError:
130- pass
131-
132-try:
133- from . import torrent
134-except ImportError:
135- pass
136
137=== removed file 'dmedia/backends/s3.py'
138--- dmedia/backends/s3.py 2011-09-04 04:04:17 +0000
139+++ dmedia/backends/s3.py 1970-01-01 00:00:00 +0000
140@@ -1,184 +0,0 @@
141-# Authors:
142-# Jason Gerard DeRose <jderose@novacut.com>
143-#
144-# dmedia: distributed media library
145-# Copyright (C) 2011 Jason Gerard DeRose <jderose@novacut.com>
146-#
147-# This file is part of `dmedia`.
148-#
149-# `dmedia` is free software: you can redistribute it and/or modify it under the
150-# terms of the GNU Affero General Public License as published by the Free
151-# Software Foundation, either version 3 of the License, or (at your option) any
152-# later version.
153-#
154-# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY
155-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
156-# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
157-# details.
158-#
159-# You should have received a copy of the GNU Affero General Public License along
160-# with `dmedia`. If not, see <http://www.gnu.org/licenses/>.
161-
162-"""
163-Upload to and download from Amazon S3 using ``boto``.
164-
165-For documentation on ``boto``, see:
166-
167- http://code.google.com/p/boto/
168-"""
169-
170-import os
171-import logging
172-
173-# FIXME: how do you use gnomekeyring from PyGI?
174-#import gnomekeyring
175-gnomekeyring = None
176-from boto.s3.connection import S3Connection
177-from boto.s3.bucket import Bucket
178-from boto.s3.key import Key
179-
180-from dmedia.constants import LEAF_SIZE
181-from dmedia import transfers
182-
183-
184-log = logging.getLogger()
185-
186-
187-def keyring_name(bucket):
188- """
189- Keyring name (description) in which to store S3 credentials.
190-
191- For example:
192-
193- >>> keyring_name('novacut')
194- 'dmedia/s3/novacut'
195-
196- """
197- return 'dmedia/s3/' + bucket
198-
199-
200-def keyring_attrs(bucket):
201- """
202- Keyring attributes for item storing S3 credentials.
203-
204- For example:
205-
206- >>> keyring_attrs('novacut')
207- {'bucket': 'novacut', 'dmedia': 's3'}
208-
209- """
210- return {
211- 'dmedia': 's3',
212- 'bucket': bucket,
213- }
214-
215-
216-def save_credentials(bucket, keyid, secret, keyring=gnomekeyring):
217- keyring.item_create_sync(
218- None,
219- gnomekeyring.ITEM_GENERIC_SECRET,
220- keyring_name(bucket),
221- keyring_attrs(bucket),
222- ':'.join([keyid, secret]),
223- True,
224- )
225-
226-
227-def load_credentials(bucket, keyring=gnomekeyring):
228- raise KeyError # FIXME gnomekeyring
229- try:
230- items = keyring.find_items_sync(
231- gnomekeyring.ITEM_GENERIC_SECRET,
232- keyring_attrs(bucket),
233- )
234- (keyid, secret) = items[0].secret.split(':')
235- return (keyid, secret)
236- except gnomekeyring.NoMatchError:
237- raise KeyError
238-
239-
240-class S3Backend(transfers.TransferBackend):
241- """
242- Backend for uploading to and downloading from Amazon S3 using ``boto``.
243- """
244- def setup(self):
245- self.bucketname = self.store['bucket']
246- try:
247- (self.keyid, self.secret) = load_credentials(self.bucketname)
248- except KeyError:
249- pass
250- self._bucket = None
251-
252- @property
253- def bucket(self):
254- """
255- Lazily create the ``boto.s3.bucket.Bucket`` instance.
256- """
257- if self._bucket is None:
258- conn = S3Connection(self.keyid, self.secret)
259- self._bucket = conn.get_bucket(self.bucketname)
260- return self._bucket
261-
262- def boto_callback(self, completed, total):
263- self.progress(completed)
264-
265- def upload(self, doc, leaves, fs):
266- """
267- Upload the file with *doc* metadata from the filestore *fs*.
268-
269- :param doc: the CouchDB document of file to upload (a ``dict``)
270- :param fs: a `FileStore` instance from which the file will be read
271- """
272- chash = doc['_id']
273- ext = doc.get('ext')
274- key = self.key(chash, ext)
275- log.info('Uploading %r to S3 bucket %r...', key, self.bucketname)
276-
277- k = Key(self.bucket)
278- k.key = key
279- headers = {}
280- if doc.get('content_type'):
281- headers['Content-Type'] = doc['content_type']
282- if doc.get('name'):
283- headers['Content-Disposition'] = (
284- 'attachment; filename="{}"'.format(doc['name'])
285- )
286-
287- if doc.get('content_type') in ('video/ogg', 'audio/ogg'):
288- dur = doc.get('meta', {}).get('duration')
289- if isinstance(dur, int) and dur >= 0:
290- headers['x-amz-meta-content-duration'] = str(dur)
291- fp = fs.open(chash, ext)
292- k.set_contents_from_file(fp,
293- headers=headers,
294- cb=self.boto_callback,
295- num_cb=max(5, doc['bytes'] / LEAF_SIZE),
296- policy='public-read',
297- )
298- log.info('Uploaded %r to S3 bucket %r', key, self.bucketname)
299- return {'copies': self.copies, 'policy': 'public-read'}
300-
301- def download(self, doc, leaves, fs):
302- """
303- Download the file with *doc* metadata into the filestore *fs*.
304-
305- :param doc: the CouchDB document of file to download (a ``dict``)
306- :param fs: a `FileStore` instance into which the file will be written
307- """
308- chash = doc['_id']
309- ext = doc.get('ext')
310- key = self.key(chash, ext)
311- log.info('Downloading %r from S3 bucket %r...', key, self.bucketname)
312-
313- k = self.bucket.get_key(self.key(chash, ext))
314- tmp_fp = fs.allocate_for_transfer(doc['bytes'], chash, ext)
315- k.get_file(tmp_fp,
316- cb=self.boto_callback,
317- num_cb=max(5, doc['bytes'] / LEAF_SIZE),
318- )
319- tmp_fp.close()
320- log.info('Downloaded %r from S3 bucket %r', key, self.bucketname)
321-
322-
323-transfers.register_uploader('s3', S3Backend)
324-transfers.register_downloader('s3', S3Backend)
325
326=== removed directory 'dmedia/backends/tests'
327=== removed file 'dmedia/backends/tests/__init__.py'
328--- dmedia/backends/tests/__init__.py 2011-04-23 07:29:32 +0000
329+++ dmedia/backends/tests/__init__.py 1970-01-01 00:00:00 +0000
330@@ -1,24 +0,0 @@
331-# Authors:
332-# Jason Gerard DeRose <jderose@novacut.com>
333-#
334-# dmedia: distributed media library
335-# Copyright (C) 2011 Jason Gerard DeRose <jderose@novacut.com>
336-#
337-# This file is part of `dmedia`.
338-#
339-# `dmedia` is free software: you can redistribute it and/or modify it under the
340-# terms of the GNU Affero General Public License as published by the Free
341-# Software Foundation, either version 3 of the License, or (at your option) any
342-# later version.
343-#
344-# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY
345-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
346-# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
347-# details.
348-#
349-# You should have received a copy of the GNU Affero General Public License along
350-# with `dmedia`. If not, see <http://www.gnu.org/licenses/>.
351-
352-"""
353-Unit tests for the `dmedia.backends` package.
354-"""
355
356=== removed file 'dmedia/backends/tests/test_s3.py'
357--- dmedia/backends/tests/test_s3.py 2011-09-04 04:04:17 +0000
358+++ dmedia/backends/tests/test_s3.py 1970-01-01 00:00:00 +0000
359@@ -1,103 +0,0 @@
360-# Authors:
361-# Jason Gerard DeRose <jderose@novacut.com>
362-#
363-# dmedia: distributed media library
364-# Copyright (C) 2011 Jason Gerard DeRose <jderose@novacut.com>
365-#
366-# This file is part of `dmedia`.
367-#
368-# `dmedia` is free software: you can redistribute it and/or modify it under the
369-# terms of the GNU Affero General Public License as published by the Free
370-# Software Foundation, either version 3 of the License, or (at your option) any
371-# later version.
372-#
373-# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY
374-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
375-# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
376-# details.
377-#
378-# You should have received a copy of the GNU Affero General Public License along
379-# with `dmedia`. If not, see <http://www.gnu.org/licenses/>.
380-
381-"""
382-Unit tests for the `dmedia.backends.s3` module.
383-"""
384-
385-from unittest import TestCase
386-
387-# FIXME: how do you use gnomekeyring from PyGI?
388-#import gnomekeyring
389-
390-from dmedia.backends import s3
391-
392-
393-class DummyItem:
394- secret = 'bar:baz'
395-
396-
397-class DummyKeyring(object):
398- def item_create_sync(self, *args):
399- self._create = args
400-
401- def find_items_sync(self, *args):
402- self._find = args
403- return [DummyItem()]
404-
405-
406-class TestFunctions(TestCase):
407- def test_keyring_name(self):
408- f = s3.keyring_name
409- self.assertEqual(f('whatever'), 'dmedia/s3/whatever')
410-
411- def test_keyring_attrs(self):
412- f = s3.keyring_attrs
413- self.assertEqual(f('whatever'), {'bucket': 'whatever', 'dmedia': 's3'})
414-
415- def test_save_credentials(self):
416- self.skipTest('gnomekeyring')
417- f = s3.save_credentials
418- k = DummyKeyring()
419- self.assertIsNone(f('foo', 'bar', 'baz', k))
420- self.assertEqual(
421- k._create,
422- (
423- None,
424- gnomekeyring.ITEM_GENERIC_SECRET,
425- 'dmedia/s3/foo',
426- {'bucket': 'foo', 'dmedia': 's3'},
427- 'bar:baz',
428- True,
429- )
430- )
431-
432- def test_load_credentials(self):
433- self.skipTest('gnomekeyring')
434- f = s3.load_credentials
435- k = DummyKeyring()
436- self.assertEqual(f('foo', k), ('bar', 'baz'))
437- self.assertEqual(
438- k._find,
439- (
440- gnomekeyring.ITEM_GENERIC_SECRET,
441- {'bucket': 'foo', 'dmedia': 's3'},
442- )
443- )
444-
445-
446-
447-class TestS3Backend(TestCase):
448- klass = s3.S3Backend
449-
450- def test_init(self):
451- inst = self.klass({'_id': 'foo', 'bucket': 'bar'})
452- self.assertEqual(inst.bucketname, 'bar')
453- self.assertEqual(inst._bucket, None)
454-
455- def test_repr(self):
456- inst = self.klass({'_id': 'foo', 'bucket': 'bar'})
457- self.assertEqual(repr(inst), "S3Backend('foo')")
458-
459- def test_bucket(self):
460- inst = self.klass({'_id': 'foo', 'bucket': 'bar'})
461- inst._bucket = 'whatever'
462- self.assertEqual(inst.bucket, 'whatever')
463
464=== removed file 'dmedia/backends/tests/test_torrent.py'
465--- dmedia/backends/tests/test_torrent.py 2011-04-25 09:03:19 +0000
466+++ dmedia/backends/tests/test_torrent.py 1970-01-01 00:00:00 +0000
467@@ -1,69 +0,0 @@
468-# Authors:
469-# Jason Gerard DeRose <jderose@novacut.com>
470-#
471-# dmedia: distributed media library
472-# Copyright (C) 2011 Jason Gerard DeRose <jderose@novacut.com>
473-#
474-# This file is part of `dmedia`.
475-#
476-# `dmedia` is free software: you can redistribute it and/or modify it under the
477-# terms of the GNU Affero General Public License as published by the Free
478-# Software Foundation, either version 3 of the License, or (at your option) any
479-# later version.
480-#
481-# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY
482-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
483-# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
484-# details.
485-#
486-# You should have received a copy of the GNU Affero General Public License along
487-# with `dmedia`. If not, see <http://www.gnu.org/licenses/>.
488-
489-"""
490-Unit tests for the `dmedia.backends.torrent` module.
491-"""
492-
493-from unittest import TestCase
494-import httplib
495-
496-from dmedia.backends import torrent
497-
498-
499-class TestTorrentBackend(TestCase):
500- klass = torrent.TorrentBackend
501-
502- def test_init(self):
503- url = 'https://foo.s3.amazonaws.com/'
504- inst = self.klass({'url': url})
505- self.assertEqual(inst.url, url)
506- self.assertEqual(inst.basepath, '/')
507- self.assertEqual(
508- inst.t,
509- ('https', 'foo.s3.amazonaws.com', '/', '', '', '')
510- )
511- self.assertIsInstance(inst.conn, httplib.HTTPSConnection)
512-
513- url = 'http://example.com/bar'
514- inst = self.klass({'url': url})
515- self.assertEqual(inst.url, url)
516- self.assertEqual(inst.basepath, '/bar/')
517- self.assertEqual(
518- inst.t,
519- ('http', 'example.com', '/bar', '', '', '')
520- )
521- self.assertIsInstance(inst.conn, httplib.HTTPConnection)
522- self.assertNotIsInstance(inst.conn, httplib.HTTPSConnection)
523-
524- with self.assertRaises(ValueError) as cm:
525- inst = self.klass({'url': 'ftp://example.com/'})
526- self.assertEqual(
527- str(cm.exception),
528- "url scheme must be http or https; got 'ftp://example.com/'"
529- )
530-
531- with self.assertRaises(ValueError) as cm:
532- inst = self.klass({'url': 'http:example.com/bar'})
533- self.assertEqual(
534- str(cm.exception),
535- "bad url: 'http:example.com/bar'"
536- )
537
538=== removed file 'dmedia/backends/torrent.py'
539--- dmedia/backends/torrent.py 2011-04-25 09:03:19 +0000
540+++ dmedia/backends/torrent.py 1970-01-01 00:00:00 +0000
541@@ -1,70 +0,0 @@
542-# Authors:
543-# Jason Gerard DeRose <jderose@novacut.com>
544-#
545-# dmedia: distributed media library
546-# Copyright (C) 2011 Jason Gerard DeRose <jderose@novacut.com>
547-#
548-# This file is part of `dmedia`.
549-#
550-# `dmedia` is free software: you can redistribute it and/or modify it under the
551-# terms of the GNU Affero General Public License as published by the Free
552-# Software Foundation, either version 3 of the License, or (at your option) any
553-# later version.
554-#
555-# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY
556-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
557-# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
558-# details.
559-#
560-# You should have received a copy of the GNU Affero General Public License along
561-# with `dmedia`. If not, see <http://www.gnu.org/licenses/>.
562-
563-"""
564-Download with BitTorrent.
565-"""
566-
567-import logging
568-import time
569-from os import path
570-
571-import libtorrent
572-
573-from dmedia.constants import LEAF_SIZE
574-from dmedia.transfers import HTTPBaseBackend, register_downloader, http_conn
575-
576-
577-log = logging.getLogger()
578-
579-
580-class TorrentBackend(HTTPBaseBackend):
581- """
582- Backend for BitTorrent downloads using `libtorrent`.
583- """
584-
585- def download(self, doc, leaves, fs):
586- chash = doc['_id']
587- ext = doc.get('ext')
588- url = self.basepath + self.key(chash, ext) + '?torrent'
589- data = self.get(url)
590-
591- tmp = fs.tmp(chash, ext, create=True)
592- session = libtorrent.session()
593- session.listen_on(6881, 6891)
594-
595- info = libtorrent.torrent_info(libtorrent.bdecode(data))
596-
597- torrent = session.add_torrent({
598- 'ti': info,
599- 'save_path': path.dirname(tmp),
600- })
601-
602- while not torrent.is_seed():
603- s = torrent.status()
604- self.progress(s.total_payload_download)
605- time.sleep(2)
606-
607- session.remove_torrent(torrent)
608- time.sleep(2)
609-
610-
611-register_downloader('torrent', TorrentBackend)
612
613=== modified file 'dmedia/core.py'
614--- dmedia/core.py 2012-01-23 17:56:15 +0000
615+++ dmedia/core.py 2012-04-05 13:22:39 +0000
616@@ -98,7 +98,6 @@
617 class Base:
618 def __init__(self, env):
619 self.env = env
620- self.db = Database(schema.DB_NAME, env)
621 self.logdb = Database('dmedia_log', env)
622
623 def log(self, doc):
624@@ -107,9 +106,10 @@
625 return self.logdb.post(doc, batch='ok')
626
627
628-class Core(Base):
629+class Core:
630 def __init__(self, env, get_parentdir_info=None, bootstrap=True):
631- super().__init__(env)
632+ self.env = env
633+ self.db = Database(schema.DB_NAME, env)
634 self.stores = LocalStores()
635 self._get_parentdir_info = get_parentdir_info
636 if bootstrap:
637@@ -117,7 +117,6 @@
638
639 def _bootstrap(self):
640 self.db.ensure()
641- self.logdb.ensure()
642 init_views(self.db)
643 self._init_local()
644 self._init_stores()
645@@ -169,8 +168,8 @@
646 doc['connected'] = time.time()
647 doc['connected_to'] = self.machine_id
648 doc['statvfs'] = fs.statvfs()._asdict()
649- if callable(self._get_parentdir_info):
650- doc.update(self._get_parentdir_info(parentdir))
651+ #if callable(self._get_parentdir_info):
652+ # doc.update(self._get_parentdir_info(parentdir))
653 self.db.save(doc)
654 log.info('FileStore %r at %r', fs.id, fs.parentdir)
655 return fs
656
657=== removed directory 'dmedia/gst'
658=== removed file 'dmedia/gst/__init__.py'
659--- dmedia/gst/__init__.py 2011-09-16 03:22:56 +0000
660+++ dmedia/gst/__init__.py 1970-01-01 00:00:00 +0000
661@@ -1,25 +0,0 @@
662-# Authors:
663-# Jason Gerard DeRose <jderose@novacut.com>
664-#
665-# dmedia: distributed media library
666-# Copyright (C) 2010 Jason Gerard DeRose <jderose@novacut.com>
667-#
668-# This file is part of `dmedia`.
669-#
670-# `dmedia` is free software: you can redistribute it and/or modify it under the
671-# terms of the GNU Affero General Public License as published by the Free
672-# Software Foundation, either version 3 of the License, or (at your option) any
673-# later version.
674-#
675-# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY
676-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
677-# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
678-# details.
679-#
680-# You should have received a copy of the GNU Affero General Public License along
681-# with `dmedia`. If not, see <http://www.gnu.org/licenses/>.
682-
683-"""
684-GStreamer PyGI backend components.
685-"""
686-
687
688=== removed directory 'dmedia/gst/tests'
689=== removed file 'dmedia/gst/tests/__init__.py'
690--- dmedia/gst/tests/__init__.py 2011-09-16 03:22:56 +0000
691+++ dmedia/gst/tests/__init__.py 1970-01-01 00:00:00 +0000
692@@ -1,24 +0,0 @@
693-# Authors:
694-# Jason Gerard DeRose <jderose@novacut.com>
695-#
696-# dmedia: distributed media library
697-# Copyright (C) 2010 Jason Gerard DeRose <jderose@novacut.com>
698-#
699-# This file is part of `dmedia`.
700-#
701-# `dmedia` is free software: you can redistribute it and/or modify it under the
702-# terms of the GNU Affero General Public License as published by the Free
703-# Software Foundation, either version 3 of the License, or (at your option) any
704-# later version.
705-#
706-# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY
707-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
708-# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
709-# details.
710-#
711-# You should have received a copy of the GNU Affero General Public License along
712-# with `dmedia`. If not, see <http://www.gnu.org/licenses/>.
713-
714-"""
715-Unit tests for the `dmedia.gst` subpackage.
716-"""
717
718=== removed file 'dmedia/gst/tests/test_transcoder.py'
719--- dmedia/gst/tests/test_transcoder.py 2011-09-16 03:22:56 +0000
720+++ dmedia/gst/tests/test_transcoder.py 1970-01-01 00:00:00 +0000
721@@ -1,294 +0,0 @@
722-# Authors:
723-# Jason Gerard DeRose <jderose@novacut.com>
724-#
725-# dmedia: distributed media library
726-# Copyright (C) 2011 Jason Gerard DeRose <jderose@novacut.com>
727-#
728-# This file is part of `dmedia`.
729-#
730-# `dmedia` is free software: you can redistribute it and/or modify it under the
731-# terms of the GNU Affero General Public License as published by the Free
732-# Software Foundation, either version 3 of the License, or (at your option) any
733-# later version.
734-#
735-# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY
736-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
737-# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
738-# details.
739-#
740-# You should have received a copy of the GNU Affero General Public License along
741-# with `dmedia`. If not, see <http://www.gnu.org/licenses/>.
742-
743-"""
744-Unit tests for `dmedia.transcoder` module.
745-"""
746-
747-from unittest import TestCase
748-
749-from gi.repository import Gst
750-
751-from dmedia import transcoder
752-from dmedia.constants import TYPE_ERROR
753-from dmedia.filestore import FileStore
754-from .helpers import sample_mov, mov_hash, TempDir, raises
755-
756-
757-class test_functions(TestCase):
758- def test_caps_string(self):
759- f = transcoder.caps_string
760- self.assertEqual(
761- f('audio/x-raw-float', {}),
762- 'audio/x-raw-float'
763- )
764- self.assertEqual(
765- f('audio/x-raw-float', {'rate': 44100}),
766- 'audio/x-raw-float, rate=44100'
767- )
768- self.assertEqual(
769- f('audio/x-raw-float', {'rate': 44100, 'channels': 1}),
770- 'audio/x-raw-float, channels=1, rate=44100'
771- )
772-
773-
774-class test_TranscodBin(TestCase):
775- klass = transcoder.TranscodeBin
776-
777- def test_init(self):
778- d = {
779- 'enc': 'vorbisenc',
780- 'props': {
781- 'quality': 0.5,
782- },
783- }
784- inst = self.klass(d)
785- self.assertTrue(inst._d is d)
786-
787- self.assertTrue(inst._q1.get_parent() is inst)
788- self.assertTrue(isinstance(inst._q1, Gst.Element))
789- self.assertEqual(inst._q1.get_factory().get_name(), 'queue')
790-
791- self.assertTrue(inst._enc.get_parent() is inst)
792- self.assertTrue(isinstance(inst._enc, Gst.Element))
793- self.assertEqual(inst._enc.get_factory().get_name(), 'vorbisenc')
794- self.assertEqual(inst._enc.get_property('quality'), 0.5)
795-
796- self.assertTrue(inst._q2.get_parent() is inst)
797- self.assertTrue(isinstance(inst._q2, Gst.Element))
798- self.assertEqual(inst._q2.get_factory().get_name(), 'queue')
799-
800- d = {'enc': 'vorbisenc'}
801- inst = self.klass(d)
802- self.assertTrue(inst._d is d)
803-
804- self.assertTrue(inst._q1.get_parent() is inst)
805- self.assertTrue(isinstance(inst._q1, Gst.Element))
806- self.assertEqual(inst._q1.get_factory().get_name(), 'queue')
807-
808- self.assertTrue(inst._enc.get_parent() is inst)
809- self.assertTrue(isinstance(inst._enc, Gst.Element))
810- self.assertEqual(inst._enc.get_factory().get_name(), 'vorbisenc')
811- self.assertNotEqual(inst._enc.get_property('quality'), 0.5)
812-
813- self.assertTrue(inst._q2.get_parent() is inst)
814- self.assertTrue(isinstance(inst._q2, Gst.Element))
815- self.assertEqual(inst._q2.get_factory().get_name(), 'queue')
816-
817- def test_repr(self):
818- d = {
819- 'enc': 'vorbisenc',
820- 'props': {
821- 'quality': 0.5,
822- },
823- }
824-
825- inst = self.klass(d)
826- self.assertEqual(
827- repr(inst),
828- 'TranscodeBin(%r)' % (d,)
829- )
830-
831- class FooBar(self.klass):
832- pass
833- inst = FooBar(d)
834- self.assertEqual(
835- repr(inst),
836- 'FooBar(%r)' % (d,)
837- )
838-
839- def test_make(self):
840- d = {'enc': 'vorbisenc'}
841- inst = self.klass(d)
842-
843- enc = inst._make('theoraenc')
844- self.assertTrue(enc.get_parent() is inst)
845- self.assertTrue(isinstance(enc, Gst.Element))
846- self.assertEqual(enc.get_factory().get_name(), 'theoraenc')
847- self.assertEqual(enc.get_property('quality'), 48)
848- self.assertEqual(enc.get_property('keyframe-force'), 64)
849-
850- enc = inst._make('theoraenc', {'quality': 50, 'keyframe-force': 32})
851- self.assertTrue(enc.get_parent() is inst)
852- self.assertTrue(isinstance(enc, Gst.Element))
853- self.assertEqual(enc.get_factory().get_name(), 'theoraenc')
854- self.assertEqual(enc.get_property('quality'), 50)
855- self.assertEqual(enc.get_property('keyframe-force'), 32)
856-
857-
858-class test_AudioTranscoder(TestCase):
859- klass = transcoder.AudioTranscoder
860-
861- def test_init(self):
862- d = {
863- 'enc': 'vorbisenc',
864- 'props': {
865- 'quality': 0.5,
866- },
867- }
868- inst = self.klass(d)
869- self.assertTrue(isinstance(inst._enc, Gst.Element))
870- self.assertEqual(inst._enc.get_factory().get_name(), 'vorbisenc')
871- self.assertEqual(inst._enc.get_property('quality'), 0.5)
872-
873- d = {
874- 'enc': 'vorbisenc',
875- 'caps': {'rate': 44100},
876- 'props': {'quality': 0.25},
877- }
878- inst = self.klass(d)
879- self.assertTrue(isinstance(inst._enc, Gst.Element))
880- self.assertEqual(inst._enc.get_factory().get_name(), 'vorbisenc')
881- self.assertEqual(inst._enc.get_property('quality'), 0.25)
882-
883-
884-class test_VideoTranscoder(TestCase):
885- klass = transcoder.VideoTranscoder
886-
887- def test_init(self):
888- d = {
889- 'enc': 'theoraenc',
890- 'props': {
891- 'quality': 50,
892- 'keyframe-force': 32,
893- },
894- }
895- inst = self.klass(d)
896- self.assertTrue(isinstance(inst._enc, Gst.Element))
897- self.assertEqual(inst._enc.get_factory().get_name(), 'theoraenc')
898- self.assertEqual(inst._enc.get_property('quality'), 50)
899-
900- d = {
901- 'enc': 'theoraenc',
902- 'caps': {'width': 800, 'height': 450},
903- 'props': {
904- 'quality': 50,
905- 'keyframe-force': 32,
906- },
907- }
908- inst = self.klass(d)
909- self.assertTrue(isinstance(inst._enc, Gst.Element))
910- self.assertEqual(inst._enc.get_factory().get_name(), 'theoraenc')
911- self.assertEqual(inst._enc.get_property('quality'), 50)
912-
913-
914-class test_Transcoder(TestCase):
915- klass = transcoder.Transcoder
916-
917- def setUp(self):
918- self.tmp = TempDir()
919- self.fs = FileStore(self.tmp.path)
920- self.fs.import_file(open(sample_mov, 'rb'), 'mov')
921-
922- def test_init(self):
923- job = {
924- 'src': {'id': mov_hash, 'ext': 'mov'},
925- 'mux': 'oggmux',
926- 'ext': 'ogv',
927- }
928-
929- e = raises(TypeError, self.klass, 17, self.fs)
930- self.assertEqual(
931- str(e),
932- TYPE_ERROR % ('job', dict, int, 17)
933- )
934- e = raises(TypeError, self.klass, job, 18)
935- self.assertEqual(
936- str(e),
937- TYPE_ERROR % ('fs', FileStore, int, 18)
938- )
939-
940- inst = self.klass(job, self.fs)
941- self.assertTrue(inst.job is job)
942- self.assertTrue(inst.fs is self.fs)
943-
944- self.assertTrue(isinstance(inst.dst_fp, file))
945- self.assertEqual(inst.dst_fp.mode, 'r+b')
946- self.assertTrue(
947- inst.dst_fp.name.startswith(self.tmp.join('.dmedia', 'writes'))
948- )
949- self.assertTrue(inst.dst_fp.name.endswith('.ogv'))
950-
951- self.assertTrue(isinstance(inst.src, Gst.Element))
952- self.assertTrue(inst.src.get_parent() is inst.pipeline)
953- self.assertEqual(inst.src.get_factory().get_name(), 'filesrc')
954- self.assertEqual(
955- inst.src.get_property('location'),
956- self.fs.path(mov_hash, 'mov')
957- )
958-
959- self.assertTrue(isinstance(inst.dec, Gst.Element))
960- self.assertTrue(inst.dec.get_parent() is inst.pipeline)
961- self.assertEqual(inst.dec.get_factory().get_name(), 'decodebin2')
962-
963- self.assertTrue(isinstance(inst.mux, Gst.Element))
964- self.assertTrue(inst.mux.get_parent() is inst.pipeline)
965- self.assertEqual(inst.mux.get_factory().get_name(), 'oggmux')
966-
967- self.assertTrue(isinstance(inst.sink, Gst.Element))
968- self.assertTrue(inst.sink.get_parent() is inst.pipeline)
969- self.assertEqual(inst.sink.get_factory().get_name(), 'fdsink')
970- self.assertEqual(inst.sink.get_property('fd'), inst.dst_fp.fileno())
971-
972- def test_theora450(self):
973- self.skipTest('fix me gi.repository.Gst')
974- job = {
975- 'src': {'id': mov_hash, 'ext': 'mov'},
976- 'mux': 'oggmux',
977- 'video': {
978- 'enc': 'theoraenc',
979- 'caps': {'width': 800, 'height': 450},
980- },
981- }
982- inst = self.klass(job, self.fs)
983- inst.run()
984-
985- def test_flac(self):
986- self.skipTest('fix me gi.repository.Gst')
987- job = {
988- 'src': {'id': mov_hash, 'ext': 'mov'},
989- 'mux': 'oggmux',
990- 'audio': {
991- 'enc': 'flacenc',
992- 'caps': {'rate': 44100},
993- },
994- }
995- inst = self.klass(job, self.fs)
996- inst.run()
997-
998- def test_theora360_vorbis(self):
999- self.skipTest('fix me gi.repository.Gst')
1000- job = {
1001- 'src': {'id': mov_hash, 'ext': 'mov'},
1002- 'mux': 'oggmux',
1003- 'video': {
1004- 'enc': 'theoraenc',
1005- 'props': {'quality': 40},
1006- 'caps': {'width': 800, 'height': 450},
1007- },
1008- 'audio': {
1009- 'enc': 'vorbisenc',
1010- 'props': {'quality': 0.4},
1011- 'caps': {'rate': 44100},
1012- },
1013- }
1014- inst = self.klass(job, self.fs)
1015- inst.run()
1016
1017=== removed file 'dmedia/gst/transcoder.py'
1018--- dmedia/gst/transcoder.py 2011-09-16 03:22:56 +0000
1019+++ dmedia/gst/transcoder.py 1970-01-01 00:00:00 +0000
1020@@ -1,238 +0,0 @@
1021-# Authors:
1022-# Jason Gerard DeRose <jderose@novacut.com>
1023-#
1024-# dmedia: distributed media library
1025-# Copyright (C) 2011 Jason Gerard DeRose <jderose@novacut.com>
1026-#
1027-# This file is part of `dmedia`.
1028-#
1029-# `dmedia` is free software: you can redistribute it and/or modify it under the
1030-# terms of the GNU Affero General Public License as published by the Free
1031-# Software Foundation, either version 3 of the License, or (at your option) any
1032-# later version.
1033-#
1034-# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY
1035-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
1036-# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
1037-# details.
1038-#
1039-# You should have received a copy of the GNU Affero General Public License along
1040-# with `dmedia`. If not, see <http://www.gnu.org/licenses/>.
1041-
1042-"""
1043-GStreamer-based transcoder.
1044-"""
1045-
1046-import logging
1047-
1048-from gi.repository import GObject, Gst
1049-
1050-from .constants import TYPE_ERROR
1051-from .filestore import FileStore
1052-
1053-
1054-log = logging.getLogger()
1055-GObject.threads_init()
1056-Gst.init(None)
1057-
1058-
1059-def caps_string(mime, caps):
1060- """
1061- Build a GStreamer caps string.
1062-
1063- For example:
1064-
1065- >>> caps_string('video/x-raw-yuv', {'width': 800, 'height': 450})
1066- 'video/x-raw-yuv, height=450, width=800'
1067- """
1068- accum = [mime]
1069- for key in sorted(caps):
1070- accum.append('%s=%s' % (key, caps[key]))
1071- return ', '.join(accum)
1072-
1073-
1074-
1075-class TranscodeBin(Gst.Bin):
1076- """
1077- Base class for `AudioTranscoder` and `VideoTranscoder`.
1078- """
1079- def __init__(self, d):
1080- super(TranscodeBin, self).__init__()
1081- self._d = d
1082- self._q1 = self._make('queue')
1083- self._enc = self._make(d['enc'], d.get('props'))
1084- self._q2 = self._make('queue')
1085- self._enc.link(self._q2)
1086- self.add_pad(
1087- Gst.GhostPad.new('sink', self._q1.get_pad('sink'))
1088- )
1089- self.add_pad(
1090- Gst.GhostPad.new('src', self._q2.get_pad('src'))
1091- )
1092-
1093- def __repr__(self):
1094- return '%s(%r)' % (self.__class__.__name__, self._d)
1095-
1096- def _make(self, name, props=None):
1097- """
1098- Create gst element, set properties, and add to this bin.
1099- """
1100- element = Gst.ElementFactory.make(name, None)
1101- if props:
1102- for (key, value) in props.iteritems():
1103- element.set_property(key, value)
1104- self.add(element)
1105- return element
1106-
1107-
1108-class AudioTranscoder(TranscodeBin):
1109- def __init__(self, d):
1110- super(AudioTranscoder, self).__init__(d)
1111-
1112- # Create processing elements:
1113- self._conv = self._make('audioconvert')
1114- self._rsp = self._make('audioresample', {'quality': 10})
1115- self._rate = self._make('audiorate')
1116-
1117- # Link elements:
1118- self._q1.link(self._conv)
1119- self._conv.link(self._rsp)
1120- if d.get('caps'):
1121- # FIXME: There is probably a better way to do this, but the caps API
1122- # has always been a bit of a mystery to me. --jderose
1123- if d['enc'] == 'vorbisenc':
1124- mime = 'audio/x-raw-float'
1125- else:
1126- mime = 'audio/x-raw-int'
1127- caps = Gst.caps_from_string(
1128- caps_string(mime, d['caps'])
1129- )
1130- self._rsp.link_filtered(self._rate, caps)
1131- else:
1132- self._rsp.link(self._rate)
1133- self._rate.link(self._enc)
1134-
1135-
1136-class VideoTranscoder(TranscodeBin):
1137- def __init__(self, d):
1138- super(VideoTranscoder, self).__init__(d)
1139-
1140- # Create processing elements:
1141- self._scale = self._make('ffvideoscale', {'method': 10})
1142- self._q = self._make('queue')
1143-
1144- # Link elements:
1145- self._q1.link(self._scale)
1146- if d.get('caps'):
1147- caps = Gst.caps_from_string(
1148- caps_string('video/x-raw-yuv', d['caps'])
1149- )
1150- self._scale.link_filtered(self._q, caps)
1151- else:
1152- self._scale.link(self._q)
1153- self._q.link(self._enc)
1154-
1155-
1156-class Transcoder(object):
1157- def __init__(self, job, fs):
1158- """
1159- Initialize.
1160-
1161- :param job: a ``dict`` describing the transcode to perform.
1162- :param fs: a `FileStore` instance in which to store transcoded file
1163- """
1164- if not isinstance(job, dict):
1165- raise TypeError(
1166- TYPE_ERROR % ('job', dict, type(job), job)
1167- )
1168- if not isinstance(fs, FileStore):
1169- raise TypeError(
1170- TYPE_ERROR % ('fs', FileStore, type(fs), fs)
1171- )
1172- self.job = job
1173- self.fs = fs
1174-
1175- src = job['src']
1176- src_filename = self.fs.path(src['id'], src.get('ext'))
1177- self.dst_fp = self.fs.allocate_for_write(job.get('ext'))
1178-
1179- self.mainloop = GObject.MainLoop()
1180- self.pipeline = Gst.Pipeline()
1181-
1182- # Create bus and connect several handlers
1183- self.bus = self.pipeline.get_bus()
1184- self.bus.add_signal_watch()
1185- self.bus.connect('message::eos', self.on_eos)
1186- self.bus.connect('message::error', self.on_error)
1187-
1188- # Create elements
1189- self.src = Gst.ElementFactory.make('filesrc', None)
1190- self.dec = Gst.ElementFactory.make('decodebin2', None)
1191- self.mux = Gst.ElementFactory.make(job['mux'], None)
1192- self.sink = Gst.ElementFactory.make('fdsink', None)
1193-
1194- # Set properties
1195- self.src.set_property('location', src_filename)
1196- self.sink.set_property('fd', self.dst_fp.fileno())
1197-
1198- # Connect handler for 'new-decoded-pad' signal
1199- self.dec.connect('new-decoded-pad', self.on_new_decoded_pad)
1200-
1201- # Add elements to pipeline
1202- for el in (self.src, self.dec, self.mux, self.sink):
1203- self.pipeline.add(el)
1204-
1205- # Link *some* elements
1206- # This is completed in self.on_new_decoded_pad()
1207- self.src.link(self.dec)
1208- self.mux.link(self.sink)
1209-
1210- self.audio = None
1211- self.video = None
1212- self.tup = None
1213-
1214- def run(self):
1215- self.pipeline.set_state(Gst.State.PLAYING)
1216- self.mainloop.run()
1217- return self.tup
1218-
1219- def kill(self):
1220- self.pipeline.set_state(Gst.State.NULL)
1221- self.pipeline.get_state()
1222- self.mainloop.quit()
1223-
1224- def link_pad(self, pad, name, key):
1225- if key in self.job:
1226- klass = {'audio': AudioTranscoder, 'video': VideoTranscoder}[key]
1227- el = klass(self.job[key])
1228- else:
1229- el = Gst.ElementFactory.make('fakesink', None)
1230- self.pipeline.add(el)
1231- log.info('Linking pad %r with %r', name, el)
1232- pad.link(el.get_pad('sink'))
1233- if key in self.job:
1234- el.link(self.mux)
1235- el.set_state(Gst.State.PLAYING)
1236- return el
1237-
1238- def on_new_decoded_pad(self, element, pad, last):
1239- name = pad.get_caps().to_string()
1240- log.debug('new decoded pad: %r', name)
1241- if name.startswith('audio/'):
1242- assert self.audio is None
1243- self.audio = self.link_pad(pad, name, 'audio')
1244- elif name.startswith('video/'):
1245- assert self.video is None
1246- self.video = self.link_pad(pad, name, 'video')
1247-
1248- def on_eos(self, bus, msg):
1249- log.info('eos')
1250- self.kill()
1251- self.dst_fp.close()
1252- fp = open(self.dst_fp.name, 'rb')
1253- self.tup = self.fs.tmp_hash_move(fp, self.job.get('ext'))
1254-
1255- def on_error(self, bus, msg):
1256- error = msg.parse_error()[1]
1257- log.error(error)
1258- self.kill()
1259
1260=== removed file 'dmedia/gtk/client.py'
1261--- dmedia/gtk/client.py 2011-04-10 09:55:27 +0000
1262+++ dmedia/gtk/client.py 1970-01-01 00:00:00 +0000
1263@@ -1,238 +0,0 @@
1264-# Authors:
1265-# Jason Gerard DeRose <jderose@novacut.com>
1266-#
1267-# dmedia: distributed media library
1268-# Copyright (C) 2010 Jason Gerard DeRose <jderose@novacut.com>
1269-#
1270-# This file is part of `dmedia`.
1271-#
1272-# `dmedia` is free software: you can redistribute it and/or modify it under the
1273-# terms of the GNU Affero General Public License as published by the Free
1274-# Software Foundation, either version 3 of the License, or (at your option) any
1275-# later version.
1276-#
1277-# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY
1278-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
1279-# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
1280-# details.
1281-#
1282-# You should have received a copy of the GNU Affero General Public License along
1283-# with `dmedia`. If not, see <http://www.gnu.org/licenses/>.
1284-
1285-"""
1286-Convenience wrapper for Python applications talking to dmedia dbus service.
1287-"""
1288-
1289-import dbus
1290-import dbus.mainloop.glib
1291-from gi.repository import GObject
1292-from gi.repository.GObject import TYPE_PYOBJECT
1293-from dmedia.constants import IMPORT_BUS, IMPORT_IFACE, EXTENSIONS
1294-
1295-
1296-# We need mainloop integration to test signals:
1297-dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
1298-
1299-
1300-class Client(GObject.GObject):
1301- """
1302- Simple and Pythonic way to control dmedia dbus service.
1303-
1304- For Python applications, this client provides several advantages over
1305- strait dbus because it:
1306-
1307- 1. Lazily starts the dmedia service the first time you call a dbus method
1308- or connect a signal handler
1309-
1310- 2. More Pythonic API, including default argument values where they make
1311- sense
1312-
1313- 3. Can use convenient GObject signals
1314-
1315- Controlling import operations
1316- =============================
1317-
1318- The dmedia service can have multiple import operations running at once.
1319- Import jobs are identified by the path of the directory being imported.
1320-
1321- For example, use `Client.list_imports()` to get the list of currently running
1322- imports:
1323-
1324- >>> from dmedia.gtkui.client import Client
1325- >>> client = Client() #doctest: +SKIP
1326- >>> client.list_imports() #doctest: +SKIP
1327- []
1328-
1329- Start an import operation using `Client.start_import()`, after which you
1330- will see it in the list of running imports:
1331-
1332- >>> client.start_import('/media/EOS_DIGITAL') #doctest: +SKIP
1333- 'started'
1334- >>> client.list_imports() #doctest: +SKIP
1335- ['/media/EOS_DIGITAL']
1336-
1337- If you try to import a path for which an import operation is already in
1338- progress, `Client.start_import()` will return the status string
1339- ``'already_running'``:
1340-
1341- >>> client.start_import('/media/EOS_DIGITAL') #doctest: +SKIP
1342- 'already_running'
1343-
1344- Stop an import operation using `Client.stop_import()`, after which there
1345- will be no running imports:
1346-
1347- >>> client.stop_import('/media/EOS_DIGITAL') #doctest: +SKIP
1348- 'stopped'
1349- >>> client.list_imports() #doctest: +SKIP
1350- []
1351-
1352- If you try to stop an import operation that doesn't exist,
1353- `Client.stop_import()` will return the status string ``'not_running'``:
1354-
1355- >>> client.stop_import('/media/EOS_DIGITAL') #doctest: +SKIP
1356- 'not_running'
1357-
1358- Finally, you can shutdown the dmedia service with `Client.kill()`:
1359-
1360- >>> client.kill() #doctest: +SKIP
1361-
1362- """
1363-
1364- __gsignals__ = {
1365- 'batch_started': (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE,
1366- [TYPE_PYOBJECT]
1367- ),
1368- 'batch_finished': (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE,
1369- [TYPE_PYOBJECT, TYPE_PYOBJECT]
1370- ),
1371- 'import_started': (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE,
1372- [TYPE_PYOBJECT, TYPE_PYOBJECT]
1373- ),
1374- 'import_count': (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE,
1375- [TYPE_PYOBJECT, TYPE_PYOBJECT, TYPE_PYOBJECT]
1376- ),
1377- 'import_progress': (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE,
1378- [TYPE_PYOBJECT, TYPE_PYOBJECT, TYPE_PYOBJECT, TYPE_PYOBJECT,
1379- TYPE_PYOBJECT]
1380- ),
1381- 'import_finished': (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE,
1382- [TYPE_PYOBJECT, TYPE_PYOBJECT, TYPE_PYOBJECT]
1383- ),
1384- }
1385-
1386- def __init__(self, bus=None):
1387- super(Client, self).__init__()
1388- self._bus = (IMPORT_BUS if bus is None else bus)
1389- self._conn = dbus.SessionBus()
1390- self._proxy = None
1391-
1392- @property
1393- def proxy(self):
1394- """
1395- Lazily create proxy object so dmedia service starts only when needed.
1396- """
1397- if self._proxy is None:
1398- self._proxy = self._conn.get_object(self._bus, '/')
1399- self._connect_signals()
1400- return self._proxy
1401-
1402- def _call(self, name, *args):
1403- method = self.proxy.get_dbus_method(name, dbus_interface=IMPORT_IFACE)
1404- return method(*args)
1405-
1406- def _connect_signals(self):
1407- self.proxy.connect_to_signal(
1408- 'BatchStarted', self._on_BatchStarted, IMPORT_IFACE
1409- )
1410- self.proxy.connect_to_signal(
1411- 'BatchFinished', self._on_BatchFinished, IMPORT_IFACE
1412- )
1413- self.proxy.connect_to_signal(
1414- 'ImportStarted', self._on_ImportStarted, IMPORT_IFACE
1415- )
1416- self.proxy.connect_to_signal(
1417- 'ImportCount', self._on_ImportCount, IMPORT_IFACE
1418- )
1419- self.proxy.connect_to_signal(
1420- 'ImportProgress', self._on_ImportProgress, IMPORT_IFACE
1421- )
1422- self.proxy.connect_to_signal(
1423- 'ImportFinished', self._on_ImportFinished, IMPORT_IFACE
1424- )
1425-
1426- def _on_BatchStarted(self, batch_id):
1427- self.emit('batch_started', batch_id)
1428-
1429- def _on_BatchFinished(self, batch_id, stats):
1430- self.emit('batch_finished', batch_id, stats)
1431-
1432- def _on_ImportStarted(self, base, import_id):
1433- self.emit('import_started', base, import_id)
1434-
1435- def _on_ImportCount(self, base, import_id, total):
1436- self.emit('import_count', base, import_id, total)
1437-
1438- def _on_ImportProgress(self, base, import_id, completed, total, info):
1439- self.emit('import_progress', base, import_id, completed, total, info)
1440-
1441- def _on_ImportFinished(self, base, import_id, stats):
1442- self.emit('import_finished', base, import_id, stats)
1443-
1444- def connect(self, *args, **kw):
1445- super(Client, self).connect(*args, **kw)
1446- if self._proxy is None:
1447- self.proxy
1448-
1449- def kill(self):
1450- """
1451- Shutdown the dmedia daemon.
1452- """
1453- self._call('Kill')
1454- self._proxy = None
1455-
1456- def version(self):
1457- """
1458- Return version number of running dmedia daemon.
1459- """
1460- return self._call('Version')
1461-
1462- def get_extensions(self, types):
1463- """
1464- Get a list of extensions based on broad categories in *types*.
1465-
1466- Currently recognized categories include ``'video'``, ``'audio'``,
1467- ``'images'``, and ``'all'``. You can safely include categories that
1468- don't yet exist.
1469-
1470- :param types: A list of general categories, e.g. ``['video', 'audio']``
1471- """
1472- return self._call('GetExtensions', types)
1473-
1474- def start_import(self, base, extract=True):
1475- """
1476- Start import of card mounted at *base*.
1477-
1478- If *extract* is ``True`` (the default), metadata will be extracted and
1479- thumbnails generated.
1480-
1481- :param base: File-system path from which to import, e.g.
1482- ``'/media/EOS_DIGITAL'``
1483- :param extract: If ``True``, perform metadata extraction, thumbnail
1484- generation; default is ``True``.
1485- """
1486- return self._call('StartImport', base, extract)
1487-
1488- def stop_import(self, base):
1489- """
1490- Start import of card mounted at *base*.
1491-
1492- :param base: File-system path from which to import, e.g.
1493- ``'/media/EOS_DIGITAL'``
1494- """
1495- return self._call('StopImport', base)
1496-
1497- def list_imports(self):
1498- """
1499- Return list of currently running imports.
1500- """
1501- return self._call('ListImports')
1502
1503=== removed file 'dmedia/gtk/firstrun.py'
1504--- dmedia/gtk/firstrun.py 2011-03-28 11:25:27 +0000
1505+++ dmedia/gtk/firstrun.py 1970-01-01 00:00:00 +0000
1506@@ -1,279 +0,0 @@
1507-#!/usr/bin/env python
1508-
1509-# Authors:
1510-# David Green <david4dev@gmail.com>
1511-# Jason Gerard DeRose <jderose@novacut.com>
1512-#
1513-# dmedia: distributed media library
1514-# Copyright (C) 2010 Jason Gerard DeRose <jderose@novacut.com>
1515-#
1516-# This file is part of `dmedia`.
1517-#
1518-# `dmedia` is free software: you can redistribute it and/or modify it under the
1519-# terms of the GNU Affero General Public License as published by the Free
1520-# Software Foundation, either version 3 of the License, or (at your option) any
1521-# later version.
1522-#
1523-# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY
1524-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
1525-# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
1526-# details.
1527-#
1528-# You should have received a copy of the GNU Affero General Public License along
1529-# with `dmedia`. If not, see <http://www.gnu.org/licenses/>.
1530-
1531-import os
1532-from os import path
1533-from xml.sax import saxutils
1534-from gettext import gettext as _
1535-
1536-from gi.repository import Gtk, Pango, GConf
1537-
1538-
1539-NO_DIALOG = '/apps/dmedia/dont-show-import-firstrun'
1540-conf = GConf.Client.get_default()
1541-
1542-
1543-TITLE = _('DMedia Importer')
1544-
1545-ICON_SIZE = Gtk.IconSize.LARGE_TOOLBAR
1546-PADDING = 5
1547-
1548-
1549-class EasyBox(object):
1550- """
1551- A more convenient box borrowed from TymeLapse.
1552- """
1553- def _pack(self, widget, expand=False, padding=PADDING, end=False):
1554- method = (self.pack_start if not end else self.pack_end)
1555- method(widget, expand, expand, padding)
1556- return widget
1557-
1558-
1559-class VBox(Gtk.VBox, EasyBox):
1560- pass
1561-
1562-
1563-class HBox(Gtk.HBox, EasyBox):
1564- pass
1565-
1566-
1567-class Label(Gtk.Label):
1568- """
1569- A more convenient label borrowed from TymeLapse.
1570- """
1571- def __init__(self, text, *tags):
1572- super(Label, self).__init__()
1573- self.set_alignment(0, 0.5)
1574- self.set_padding(5, 5)
1575- self._text = text
1576- self._tags = set(tags)
1577- self._update()
1578- #self.set_selectable(True)
1579-
1580- def _add_tags(self, *tags):
1581- self._tags.update(tags)
1582- self._update()
1583-
1584- def _remove_tags(self, *tags):
1585- for tag in tags:
1586- if tag in self._tags:
1587- self._tags.remove(tag)
1588- self._update()
1589-
1590- def _set_text(self, text):
1591- self._text = text
1592- self._update()
1593-
1594- def _update(self):
1595- text = ('' if self._text is None else self._text)
1596- if text and self._tags:
1597- m = saxutils.escape(text)
1598- for tag in self._tags:
1599- m = '<%(tag)s>%(m)s</%(tag)s>' % dict(tag=tag, m=m)
1600- self.set_markup(m)
1601- else:
1602- self.set_text(text)
1603-
1604-
1605-class Button(Gtk.Button):
1606- def __init__(self, stock=None, text=None):
1607- super(Button, self).__init__()
1608- hbox = HBox()
1609- self.add(hbox)
1610- self._image = Gtk.Image()
1611- self._label = Label(None)
1612- hbox._pack(self._image)
1613- hbox._pack(self._label, expand=True)
1614- if stock is not None:
1615- self._set_stock(stock)
1616- if text is not None:
1617- self._set_text(text)
1618-
1619- def _set_stock(self, stock, size=ICON_SIZE):
1620- self._image.set_from_stock(stock, size)
1621-
1622- def _set_text(self, text):
1623- self._label.set_text(text)
1624-
1625-
1626-class FolderChooser(Button):
1627- def __init__(self):
1628- super(FolderChooser, self).__init__(stock=Gtk.STOCK_OPEN)
1629- self._label.set_ellipsize(Pango.EllipsizeMode.START)
1630- self._title = _('Choose folder to import...')
1631- self.connect('clicked', self._on_clicked)
1632- self._set_value(os.environ['HOME'])
1633-
1634- def _set_value(self, value):
1635- self._value = path.abspath(value)
1636- self._label._set_text(self._value)
1637-
1638- def _on_clicked(self, button):
1639- dialog = Gtk.FileChooserDialog(
1640- title=self._title,
1641- action=Gtk.FileChooserAction.SELECT_FOLDER,
1642- buttons=(
1643- Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
1644- Gtk.STOCK_OPEN, Gtk.ResponseType.OK,
1645- ),
1646- )
1647- dialog.set_current_folder(self._value)
1648- response = dialog.run()
1649- if response == Gtk.ResponseType.OK:
1650- self._set_value(dialog.get_filename())
1651- dialog.destroy()
1652-
1653-
1654-def okay_button():
1655- return Button(Gtk.STOCK_OK, _('OK, Import Files'))
1656-
1657-
1658-class ImportDialog(Gtk.Window):
1659- def __init__(self):
1660- super(ImportDialog, self).__init__()
1661- self.set_default_size(425, 200)
1662- self.set_title(TITLE)
1663- try:
1664- self.set_icon_from_file('/usr/share/pixmaps/dmedia.svg')
1665- except:
1666- pass
1667- self.connect('destroy', Gtk.main_quit)
1668-
1669- self._value = None
1670-
1671- hbox = HBox()
1672- self.add(hbox)
1673-
1674- vbox = VBox()
1675- hbox._pack(vbox, expand=True)
1676-
1677- vbox._pack(Label(_('Choose Folder:'), 'b'))
1678- self._folder = FolderChooser()
1679- vbox._pack(self._folder)
1680-
1681- self._button = okay_button()
1682- vbox._pack(self._button, end=True)
1683- self._button.connect('clicked', self._on_clicked)
1684-
1685- self.show_all()
1686-
1687- def run(self):
1688- Gtk.main()
1689- return self._value
1690-
1691- def _on_clicked(self, button):
1692- self._value = self._folder._value
1693- Gtk.main_quit()
1694-
1695-
1696-class FirstRunGUI(Gtk.Window):
1697- """
1698- Promt use first time dmedia importer is run.
1699-
1700- For example:
1701-
1702- >>> from dmedia.gtkui.firstrun import FirstRunGUI
1703- >>> run = FirstRunGUI.run_if_first_run('/media/EOS_DIGITAL') #doctest: +SKIP
1704- """
1705-
1706- def __init__(self):
1707- super(FirstRunGUI, self).__init__()
1708- self.set_default_size(425, 200)
1709-
1710- self.set_title(TITLE)
1711- try:
1712- self.set_icon_from_file('/usr/share/pixmaps/dmedia.svg')
1713- except:
1714- pass
1715-
1716- self.ok_was_pressed = False
1717-
1718- self.add_content()
1719-
1720- self.connect_signals()
1721-
1722- def add_content(self):
1723- vbox = VBox()
1724- self.add(vbox)
1725-
1726- label1 = Label(_('Welcome to the dmedia importer!'), 'big')
1727- label1.set_alignment(0.5, 0.5)
1728- vbox._pack(label1)
1729-
1730- label2 = Label(_('It will import all files in the following folder:'))
1731- vbox._pack(label2)
1732-
1733- self.folder = Label(None, 'b')
1734- self.folder.set_ellipsize(Pango.EllipsizeMode.START)
1735- vbox._pack(self.folder)
1736-
1737- self.dont_show_again = Gtk.CheckButton(label=_("Don't show this again"))
1738- self.dont_show_again.set_active(True)
1739-
1740- self.ok = okay_button()
1741-
1742- hbox = HBox()
1743- hbox._pack(self.ok, expand=True)
1744- hbox._pack(self.dont_show_again)
1745-
1746- vbox._pack(hbox, end=True)
1747-
1748-
1749- def connect_signals(self):
1750- self.connect("delete_event", self.delete_event)
1751- self.connect("destroy", self.destroy)
1752- self.ok.connect("clicked", self.on_ok)
1753-
1754-
1755- def destroy(self, widget, data=None):
1756- Gtk.main_quit()
1757-
1758-
1759- def delete_event(self, widget, event, data=None):
1760- return False
1761-
1762-
1763- def on_ok(self, widget):
1764- dont_show_again = self.dont_show_again.get_active()
1765- val = 0
1766- if dont_show_again:
1767- val = 1
1768- conf.set_bool(NO_DIALOG, val)
1769- self.ok_was_pressed = True
1770- self.destroy(widget)
1771-
1772- def go(self, folder):
1773- self.folder._set_text(folder)
1774- self.show_all()
1775- Gtk.main()
1776- return self.ok_was_pressed
1777-
1778- @classmethod
1779- def run_if_first_run(cls, base, unset=False):
1780- if unset:
1781- conf.unset(NO_DIALOG)
1782- if conf.get_bool(NO_DIALOG):
1783- return True
1784- app = cls()
1785- return app.go(base)
1786
1787=== removed file 'dmedia/gtk/menu.py'
1788--- dmedia/gtk/menu.py 2011-04-17 03:10:38 +0000
1789+++ dmedia/gtk/menu.py 1970-01-01 00:00:00 +0000
1790@@ -1,408 +0,0 @@
1791-# Authors:
1792-# David Green <david4dev@gmail.com>
1793-#
1794-# dmedia: distributed media library
1795-# Copyright (C) 2010 Jason Gerard DeRose <jderose@novacut.com>
1796-#
1797-# This file is part of `dmedia`.
1798-#
1799-# `dmedia` is free software: you can redistribute it and/or modify it under the
1800-# terms of the GNU Affero General Public License as published by the Free
1801-# Software Foundation, either version 3 of the License, or (at your option) any
1802-# later version.
1803-#
1804-# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY
1805-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
1806-# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
1807-# details.
1808-#
1809-# You should have received a copy of the GNU Affero General Public License along
1810-# with `dmedia`. If not, see <http://www.gnu.org/licenses/>.
1811-
1812-#TODO: separators, keyboard acceleration
1813-
1814-"""
1815-Menu declaration.
1816-"""
1817-
1818-
1819-from gi.repository import Gtk
1820-
1821-ACTIONS = {
1822- "close" : Gtk.main_quit
1823-}
1824-
1825-
1826-#this could potentially be put into a .json file
1827-MENU = [
1828- {
1829- "label" : "_File",
1830- "type" : "menu",
1831- "items" : [
1832- {
1833- "label" : "_Close",
1834- "type" : "action",
1835- "action" : "close"
1836- }
1837- ]
1838- }
1839-]
1840-
1841-#A menu for testing and demonstration
1842-TEST_MENU = [
1843-{
1844- "label" : "_File",
1845- "type" : "menu",
1846- "items" : [
1847- {
1848- "label" : "_Close",
1849- "type" : "action",
1850- "action" : "close"
1851- },
1852- {
1853- "label" : "_Close",
1854- "type" : "action",
1855- "action" : "close"
1856- },
1857- {
1858- "type" : "custom",
1859- "widget" : Gtk.SeparatorMenuItem()
1860- },
1861- {
1862- "label" : "_Close",
1863- "type" : "menu",
1864- "items" : [{
1865- "label" : "_Close",
1866- "type" : "action",
1867- "action" : "close"
1868- },
1869- {
1870- "label" : "_Close",
1871- "type" : "action",
1872- "action" : "close"
1873- },
1874- {
1875- "type" : "custom",
1876- "widget" : Gtk.SeparatorMenuItem()
1877- },
1878- {
1879- "label" : "_Close",
1880- "type" : "menu",
1881- "items" : [{
1882- "label" : "_Close",
1883- "type" : "action",
1884- "action" : "close"
1885- },
1886- {
1887- "label" : "_Close",
1888- "type" : "action",
1889- "action" : "close"
1890- },
1891- {
1892- "type" : "custom",
1893- "widget" : Gtk.SeparatorMenuItem()
1894- },
1895- {
1896- "label" : "_Close",
1897- "type" : "action",
1898- "action" : "close"
1899- }]
1900- },
1901- {
1902- "label" : "_Close",
1903- "type" : "action",
1904- "action" : "close"
1905- }]
1906- },
1907- {
1908- "label" : "_Close",
1909- "type" : "action",
1910- "action" : "close"
1911- }
1912- ]
1913- },
1914- {
1915- "label" : "_Edit",
1916- "type" : "menu",
1917- "items" : [
1918- {
1919- "label" : "_Close",
1920- "type" : "action",
1921- "action" : "close"
1922- },
1923- {
1924- "label" : "_Close",
1925- "type" : "action",
1926- "action" : "close"
1927- },
1928- {
1929- "type" : "custom",
1930- "widget" : Gtk.SeparatorMenuItem()
1931- },
1932- {
1933- "label" : "_Close",
1934- "type" : "menu",
1935- "items" : [{
1936- "label" : "_Close",
1937- "type" : "action",
1938- "action" : "close"
1939- },
1940- {
1941- "label" : "_Close",
1942- "type" : "action",
1943- "action" : "close"
1944- },
1945- {
1946- "type" : "custom",
1947- "widget" : Gtk.SeparatorMenuItem()
1948- },
1949- {
1950- "label" : "_Close",
1951- "type" : "menu",
1952- "items" : [{
1953- "label" : "_Close",
1954- "type" : "action",
1955- "action" : "close"
1956- },
1957- {
1958- "label" : "_Close",
1959- "type" : "action",
1960- "action" : "close"
1961- },
1962- {
1963- "type" : "custom",
1964- "widget" : Gtk.SeparatorMenuItem()
1965- },
1966- {
1967- "label" : "_Close",
1968- "type" : "action",
1969- "action" : "close"
1970- }]
1971- },
1972- {
1973- "label" : "_Close",
1974- "type" : "action",
1975- "action" : "close"
1976- }]
1977- },
1978- {
1979- "label" : "_Close",
1980- "type" : "action",
1981- "action" : "close"
1982- }
1983- ]
1984- },
1985- {
1986- "label" : "_View",
1987- "type" : "menu",
1988- "items" : [
1989- {
1990- "label" : "_Close",
1991- "type" : "action",
1992- "action" : "close"
1993- },
1994- {
1995- "label" : "_Close",
1996- "type" : "action",
1997- "action" : "close"
1998- },
1999- {
2000- "type" : "custom",
2001- "widget" : Gtk.SeparatorMenuItem()
2002- },
2003- {
2004- "label" : "_Close",
2005- "type" : "menu",
2006- "items" : [{
2007- "label" : "_Close",
2008- "type" : "action",
2009- "action" : "close"
2010- },
2011- {
2012- "label" : "_Close",
2013- "type" : "action",
2014- "action" : "close"
2015- },
2016- {
2017- "type" : "custom",
2018- "widget" : Gtk.SeparatorMenuItem()
2019- },
2020- {
2021- "label" : "_Close",
2022- "type" : "menu",
2023- "items" : [{
2024- "label" : "_Close",
2025- "type" : "action",
2026- "action" : "close"
2027- },
2028- {
2029- "label" : "_Close",
2030- "type" : "action",
2031- "action" : "close"
2032- },
2033- {
2034- "type" : "custom",
2035- "widget" : Gtk.SeparatorMenuItem()
2036- },
2037- {
2038- "label" : "_Close",
2039- "type" : "action",
2040- "action" : "close"
2041- }]
2042- },
2043- {
2044- "label" : "_Close",
2045- "type" : "action",
2046- "action" : "close"
2047- }]
2048- },
2049- {
2050- "label" : "_Close",
2051- "type" : "action",
2052- "action" : "close"
2053- }
2054- ]
2055- },
2056- {
2057- "label" : "_Tools",
2058- "type" : "menu",
2059- "items" : [
2060- {
2061- "label" : "_Close",
2062- "type" : "action",
2063- "action" : "close"
2064- },
2065- {
2066- "label" : "_Close",
2067- "type" : "action",
2068- "action" : "close"
2069- },
2070- {
2071- "type" : "custom",
2072- "widget" : Gtk.SeparatorMenuItem()
2073- },
2074- {
2075- "label" : "_Close",
2076- "type" : "menu",
2077- "items" : [{
2078- "label" : "_Close",
2079- "type" : "action",
2080- "action" : "close"
2081- },
2082- {
2083- "label" : "_Close",
2084- "type" : "action",
2085- "action" : "close"
2086- },
2087- {
2088- "type" : "custom",
2089- "widget" : Gtk.SeparatorMenuItem()
2090- },
2091- {
2092- "label" : "_Close",
2093- "type" : "menu",
2094- "items" : [{
2095- "label" : "_Close",
2096- "type" : "action",
2097- "action" : "close"
2098- },
2099- {
2100- "label" : "_Close",
2101- "type" : "action",
2102- "action" : "close"
2103- },
2104- {
2105- "type" : "custom",
2106- "widget" : Gtk.SeparatorMenuItem()
2107- },
2108- {
2109- "label" : "_Close",
2110- "type" : "action",
2111- "action" : "close"
2112- }]
2113- },
2114- {
2115- "label" : "_Close",
2116- "type" : "action",
2117- "action" : "close"
2118- }]
2119- },
2120- {
2121- "label" : "_Close",
2122- "type" : "action",
2123- "action" : "close"
2124- }
2125- ]
2126- },
2127- {
2128- "label" : "Et_c",
2129- "type" : "menu",
2130- "items" : [
2131- {
2132- "label" : "_Close",
2133- "type" : "action",
2134- "action" : "close"
2135- },
2136- {
2137- "label" : "_Close",
2138- "type" : "action",
2139- "action" : "close"
2140- },
2141- {
2142- "type" : "custom",
2143- "widget" : Gtk.SeparatorMenuItem()
2144- },
2145- {
2146- "label" : "_Close",
2147- "type" : "menu",
2148- "items" : [{
2149- "label" : "_Close",
2150- "type" : "action",
2151- "action" : "close"
2152- },
2153- {
2154- "label" : "_Close",
2155- "type" : "action",
2156- "action" : "close"
2157- },
2158- {
2159- "type" : "custom",
2160- "widget" : Gtk.SeparatorMenuItem()
2161- },
2162- {
2163- "label" : "_Close",
2164- "type" : "menu",
2165- "items" : [{
2166- "label" : "_Close",
2167- "type" : "action",
2168- "action" : "close"
2169- },
2170- {
2171- "label" : "_Close",
2172- "type" : "action",
2173- "action" : "close"
2174- },
2175- {
2176- "type" : "custom",
2177- "widget" : Gtk.SeparatorMenuItem()
2178- },
2179- {
2180- "label" : "_Close",
2181- "type" : "action",
2182- "action" : "close"
2183- }]
2184- },
2185- {
2186- "label" : "_Close",
2187- "type" : "action",
2188- "action" : "close"
2189- }]
2190- },
2191- {
2192- "label" : "_Close",
2193- "type" : "action",
2194- "action" : "close"
2195- }
2196- ]
2197- }
2198-]
2199
2200=== removed file 'dmedia/gtk/service.py'
2201--- dmedia/gtk/service.py 2011-09-04 00:53:41 +0000
2202+++ dmedia/gtk/service.py 1970-01-01 00:00:00 +0000
2203@@ -1,302 +0,0 @@
2204-# Authors:
2205-# Jason Gerard DeRose <jderose@novacut.com>
2206-# Manish SInha <mail@manishsinha.net>
2207-# David Green <david4dev@gmail.com>
2208-#
2209-# dmedia: distributed media library
2210-# Copyright (C) 2010 Jason Gerard DeRose <jderose@novacut.com>
2211-#
2212-# This file is part of `dmedia`.
2213-#
2214-# `dmedia` is free software: you can redistribute it and/or modify it under the
2215-# terms of the GNU Affero General Public License as published by the Free
2216-# Software Foundation, either version 3 of the License, or (at your option) any
2217-# later version.
2218-#
2219-# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY
2220-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
2221-# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
2222-# details.
2223-#
2224-# You should have received a copy of the GNU Affero General Public License along
2225-# with `dmedia`. If not, see <http://www.gnu.org/licenses/>.
2226-
2227-"""
2228-D-Bus service implementing Pro File Import UX.
2229-
2230-For details, please see:
2231-
2232- https://wiki.ubuntu.com/AyatanaDmediaLovefest
2233-"""
2234-
2235-from os import path
2236-from gettext import gettext as _
2237-import logging
2238-from subprocess import check_call
2239-
2240-import dbus
2241-import dbus.service
2242-import dbus.mainloop.glib
2243-
2244-from dmedia import __version__
2245-from dmedia.constants import IMPORT_BUS, IMPORT_IFACE, EXT_MAP
2246-from dmedia.importer import ImportManager
2247-
2248-from .util import NotifyManager, Timer, import_started, batch_finished
2249-
2250-
2251-try:
2252- from gi.repository import Notify
2253- Notify.init('dmedia')
2254-except ImportError:
2255- Notify = None
2256-
2257-try:
2258- from gi.repository import AppIndicator3
2259- from gi.repository import Gtk
2260-except ImportError:
2261- AppIndicator3 = None
2262-
2263-log = logging.getLogger()
2264-
2265-
2266-ICON = 'indicator-rendermenu'
2267-ICON_ATT = 'indicator-rendermenu-att'
2268-
2269-
2270-class DMedia(dbus.service.Object):
2271- __signals = frozenset([
2272- 'BatchStarted',
2273- 'BatchFinished',
2274- 'ImportStarted',
2275- 'ImportCount',
2276- 'ImportProgress',
2277- 'ImportFinished',
2278- ])
2279-
2280- def __init__(self, env, bus, killfunc):
2281- assert callable(killfunc)
2282- self._env = env
2283- self._bus = bus
2284- self._killfunc = killfunc
2285- self._bus = bus
2286- self._no_gui = env.get('no_gui', False)
2287- log.info('Starting service on %r', self._bus)
2288- self._conn = dbus.SessionBus()
2289- super(DMedia, self).__init__(self._conn, object_path='/')
2290- self._busname = dbus.service.BusName(self._bus, self._conn)
2291-
2292- if self._no_gui or Notify is None:
2293- self._notify = None
2294- else:
2295- log.info('Using `Notify`')
2296- self._notify = NotifyManager()
2297-
2298- if self._no_gui or AppIndicator3 is None:
2299- self._indicator = None
2300- else:
2301- log.info('Using `AppIndicator3`')
2302- self._indicator = AppIndicator3.Indicator.new('rendermenu', ICON,
2303- AppIndicator3.IndicatorCategory.APPLICATION_STATUS
2304- )
2305- self._timer = Timer(2, self._on_timer)
2306- self._indicator.set_attention_icon(ICON_ATT)
2307- self._menu = Gtk.Menu()
2308-
2309- self._current = Gtk.MenuItem()
2310- self._menu.append(self._current)
2311-
2312- sep = Gtk.SeparatorMenuItem()
2313- self._menu.append(sep)
2314-
2315- futon = Gtk.MenuItem()
2316- futon.set_label(_('Browse DB in Futon'))
2317- futon.connect('activate', self._on_futon)
2318- self._menu.append(futon)
2319-
2320- quit = Gtk.MenuItem()
2321- quit.set_label(_('Shutdown dmedia'))
2322- quit.connect('activate', self._on_quit)
2323- self._menu.append(quit)
2324-
2325- self._menu.show_all()
2326- self._current.hide()
2327- self._indicator.set_menu(self._menu)
2328- self._indicator.set_status(AppIndicator3.IndicatorStatus.ACTIVE)
2329-
2330- self._manager = None
2331-
2332- @property
2333- def manager(self):
2334- if self._manager is None:
2335- self._manager = ImportManager(self._env, self._on_signal)
2336- return self._manager
2337-
2338- def _on_signal(self, signal, args):
2339- if signal in self.__signals:
2340- method = getattr(self, signal)
2341- method(*args)
2342-
2343- def _on_timer(self):
2344- if self._manager is None:
2345- return
2346- text = _('File {completed} of {total}').format(
2347- **self._manager.get_batch_progress()
2348- )
2349- self._current.set_label(text)
2350-# self._indicator.set_menu(self._menu)
2351-
2352- def _on_quit(self, menuitem):
2353- self.Kill()
2354-
2355- def _on_futon(self, menuitem):
2356- log.info('Opening dmedia database in Futon..')
2357- try:
2358- uri = self.metastore.get_auth_uri() + '/_utils'
2359- check_call(['/usr/bin/xdg-open', uri])
2360- log.info('Opened Futon')
2361- except Exception:
2362- log.exception('Could not open dmedia database in Futon')
2363-
2364- @dbus.service.signal(IMPORT_IFACE, signature='s')
2365- def BatchStarted(self, batch_id):
2366- """
2367- Fired at transition from idle to at least one active import.
2368-
2369- For pro file import UX, the RenderMenu should be set to STATUS_ATTENTION
2370- when this signal is received.
2371- """
2372- if self._notify:
2373- self._batch = []
2374- if self._indicator:
2375- self._indicator.set_status(AppIndicator3.IndicatorStatus.ATTENTION)
2376- self._current.show()
2377- self._current.set_label(_('Searching for files...'))
2378- self._indicator.set_menu(self._menu)
2379- self._timer.start()
2380-
2381- @dbus.service.signal(IMPORT_IFACE, signature='sa{sx}')
2382- def BatchFinished(self, batch_id, stats):
2383- """
2384- Fired at transition from at least one active import to idle.
2385-
2386- *stats* will be the combined stats of all imports in this batch.
2387-
2388- For pro file import UX, the RenderMenu should be set back to
2389- STATUS_ACTIVE, and the NotifyOSD with the aggregate import stats should
2390- be displayed when this signal is received.
2391- """
2392- if self._indicator:
2393- self._indicator.set_status(AppIndicator3.IndicatorStatus.ACTIVE)
2394- if self._notify is None:
2395- return
2396- (summary, body) = batch_finished(stats)
2397- self._notify.replace(summary, body, 'notification-device-eject')
2398- self._timer.stop()
2399- self._current.hide()
2400-
2401- @dbus.service.signal(IMPORT_IFACE, signature='ss')
2402- def ImportStarted(self, base, import_id):
2403- """
2404- Fired when card is inserted.
2405-
2406- For pro file import UX, the "Searching for new files" NotifyOSD should
2407- be displayed when this signal is received. If a previous notification
2408- is still visible, the two should be merge and the summary conspicuously
2409- changed to be very clear that both cards were detected.
2410- """
2411- if self._notify is None:
2412- return
2413- self._batch.append(base)
2414- (summary, body) = import_started(self._batch)
2415- # FIXME: use correct icon depending on whether card reader is corrected
2416- # via FireWire or USB
2417- self._notify.replace(summary, body, 'notification-device-usb')
2418-
2419- @dbus.service.signal(IMPORT_IFACE, signature='ssx')
2420- def ImportCount(self, base, import_id, total):
2421- pass
2422-
2423- @dbus.service.signal(IMPORT_IFACE, signature='ssiia{ss}')
2424- def ImportProgress(self, base, import_id, completed, total, info):
2425- pass
2426-
2427- @dbus.service.signal(IMPORT_IFACE, signature='ssa{sx}')
2428- def ImportFinished(self, base, import_id, stats):
2429- pass
2430-
2431- @dbus.service.method(IMPORT_IFACE, in_signature='', out_signature='')
2432- def Kill(self):
2433- """
2434- Kill the dmedia service process.
2435- """
2436- log.info('Killing service...')
2437- if self._manager is not None:
2438- self._manager.kill()
2439- if callable(self._killfunc):
2440- log.info('Calling killfunc()')
2441- self._killfunc()
2442-
2443- @dbus.service.method(IMPORT_IFACE, in_signature='', out_signature='s')
2444- def Version(self):
2445- """
2446- Return dmedia version.
2447- """
2448- return __version__
2449-
2450- @dbus.service.method(IMPORT_IFACE, in_signature='as', out_signature='as')
2451- def GetExtensions(self, types):
2452- """
2453- Get a list of extensions based on broad categories in *types*.
2454-
2455- Currently recognized categories include ``'video'``, ``'audio'``,
2456- ``'images'``, and ``'all'``. You can safely include categories that
2457- don't yet exist.
2458-
2459- :param types: A list of general categories, e.g. ``['video', 'audio']``
2460- """
2461- extensions = set()
2462- for key in types:
2463- if key in EXT_MAP:
2464- extensions.update(EXT_MAP[key])
2465- return sorted(extensions)
2466-
2467- @dbus.service.method(IMPORT_IFACE, in_signature='sb', out_signature='s')
2468- def StartImport(self, base, extract):
2469- """
2470- Start import of card mounted at *base*.
2471-
2472- If *extract* is ``True``, metadata will be extracted and thumbnails
2473- generated.
2474-
2475- :param base: File-system path from which to import, e.g.
2476- ``'/media/EOS_DIGITAL'``
2477- :param extract: If ``True``, perform metadata extraction, thumbnail
2478- generation
2479- """
2480- base = unicode(base)
2481- if path.abspath(base) != base:
2482- return 'not_abspath'
2483- if not path.isdir(base):
2484- return 'not_a_dir'
2485- if self.manager.start_import(base, extract):
2486- return 'started'
2487- return 'already_running'
2488-
2489- @dbus.service.method(IMPORT_IFACE, in_signature='s', out_signature='s')
2490- def StopImport(self, base):
2491- """
2492- In running, stop the import of directory or file at *base*.
2493- """
2494- if self.manager.kill_job(base):
2495- return 'stopped'
2496- return 'not_running'
2497-
2498- @dbus.service.method(IMPORT_IFACE, in_signature='', out_signature='as')
2499- def ListImports(self):
2500- """
2501- Return list of currently running imports.
2502- """
2503- if self._manager is None:
2504- return []
2505- return self.manager.list_jobs()
2506
2507=== removed file 'dmedia/gtk/tests/test_client.py'
2508--- dmedia/gtk/tests/test_client.py 2011-09-06 19:38:32 +0000
2509+++ dmedia/gtk/tests/test_client.py 1970-01-01 00:00:00 +0000
2510@@ -1,284 +0,0 @@
2511-# Authors:
2512-# Jason Gerard DeRose <jderose@novacut.com>
2513-#
2514-# dmedia: distributed media library
2515-# Copyright (C) 2010 Jason Gerard DeRose <jderose@novacut.com>
2516-#
2517-# This file is part of `dmedia`.
2518-#
2519-# `dmedia` is free software: you can redistribute it and/or modify it under the
2520-# terms of the GNU Affero General Public License as published by the Free
2521-# Software Foundation, either version 3 of the License, or (at your option) any
2522-# later version.
2523-#
2524-# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY
2525-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
2526-# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
2527-# details.
2528-#
2529-# You should have received a copy of the GNU Affero General Public License along
2530-# with `dmedia`. If not, see <http://www.gnu.org/licenses/>.
2531-
2532-"""
2533-Unit tests for `dmedia.client` module.
2534-"""
2535-
2536-import os
2537-from os import path
2538-from subprocess import Popen
2539-import time
2540-import json
2541-
2542-import dbus
2543-from dbus.proxies import ProxyObject
2544-from gi.repository import GObject
2545-
2546-import dmedia
2547-from dmedia.constants import VIDEO, AUDIO, IMAGE, EXTENSIONS
2548-from dmedia.gtkui import client, service
2549-
2550-from dmedia.tests.helpers import TempDir, random_bus, prep_import_source
2551-from dmedia.tests.helpers import sample_mov, sample_thm
2552-from dmedia.tests.helpers import mov_hash, thm_hash
2553-from dmedia.tests.couch import CouchCase
2554-
2555-
2556-tree = path.dirname(path.dirname(path.abspath(dmedia.__file__)))
2557-assert path.isfile(path.join(tree, 'setup.py'))
2558-script = path.join(tree, 'dmedia-importer-service')
2559-assert path.isfile(script)
2560-
2561-
2562-class CaptureCallback(object):
2563- def __init__(self, signal, messages):
2564- self.signal = signal
2565- self.messages = messages
2566- self.callback = None
2567-
2568- def __call__(self, *args):
2569- self.messages.append(
2570- (self.signal,) + args
2571- )
2572- if callable(self.callback):
2573- self.callback()
2574-
2575-
2576-class SignalCapture(object):
2577- def __init__(self, obj, *signals):
2578- self.obj = obj
2579- self.messages = []
2580- self.handlers = {}
2581- for name in signals:
2582- callback = CaptureCallback(name, self.messages)
2583- obj.connect(name, callback)
2584- self.handlers[name] = callback
2585-
2586-
2587-class TestClient(CouchCase):
2588- klass = client.Client
2589-
2590- def setUp(self):
2591- """
2592- Launch dmedia dbus service using a random bus name.
2593-
2594- This will launch dmedia-service with a random bus name like this:
2595-
2596- dmedia-service --bus org.test3ISHAWZVSWVN5I5S.DMedia
2597-
2598- How do people usually unit test dbus services? This works, but not sure
2599- if there is a better idiom in common use. --jderose
2600- """
2601- self.skipTest('intermittent DBus failure')
2602- super(TestClient, self).setUp()
2603- self.bus = random_bus()
2604- cmd = [script, '--no-gui',
2605- '--bus', self.bus,
2606- '--env', json.dumps(self.env),
2607- ]
2608- self.service = Popen(cmd)
2609- time.sleep(1) # Give dmedia-service time to start
2610-
2611- def tearDown(self):
2612- super(TestClient, self).tearDown()
2613- try:
2614- self.service.terminate()
2615- self.service.wait()
2616- except OSError:
2617- pass
2618- finally:
2619- self.service = None
2620-
2621- def new(self):
2622- return self.klass(bus=self.bus)
2623-
2624- def test_init(self):
2625- # Test with no bus
2626- inst = self.klass()
2627- self.assertEqual(inst._bus, 'org.freedesktop.DMediaImporter')
2628- self.assertTrue(isinstance(inst._conn, dbus.SessionBus))
2629- self.assertTrue(inst._proxy is None)
2630-
2631- # Test with random bus
2632- inst = self.new()
2633- self.assertEqual(inst._bus, self.bus)
2634- self.assertTrue(isinstance(inst._conn, dbus.SessionBus))
2635- self.assertTrue(inst._proxy is None)
2636-
2637- # Test the proxy property
2638- p = inst.proxy
2639- self.assertTrue(isinstance(p, ProxyObject))
2640- self.assertTrue(p is inst._proxy)
2641- self.assertTrue(p is inst.proxy)
2642-
2643- # Test version()
2644- self.assertEqual(inst.version(), dmedia.__version__)
2645-
2646- # Test get_extensions()
2647- self.assertEqual(inst.get_extensions(['video']), sorted(VIDEO))
2648- self.assertEqual(inst.get_extensions(['audio']), sorted(AUDIO))
2649- self.assertEqual(inst.get_extensions(['image']), sorted(IMAGE))
2650- self.assertEqual(inst.get_extensions(['all']), sorted(EXTENSIONS))
2651- self.assertEqual(
2652- inst.get_extensions(['video', 'audio']),
2653- sorted(VIDEO + AUDIO)
2654- )
2655- self.assertEqual(
2656- inst.get_extensions(['video', 'image']),
2657- sorted(VIDEO + IMAGE)
2658- )
2659- self.assertEqual(
2660- inst.get_extensions(['audio', 'image']),
2661- sorted(AUDIO + IMAGE)
2662- )
2663- self.assertEqual(
2664- inst.get_extensions(['video', 'audio', 'image']),
2665- sorted(EXTENSIONS)
2666- )
2667- self.assertEqual(
2668- inst.get_extensions(['video', 'audio', 'image', 'all']),
2669- sorted(EXTENSIONS)
2670- )
2671- self.assertEqual(inst.get_extensions(['foo', 'bar']), [])
2672-
2673- def test_connect(self):
2674- def callback(*args):
2675- pass
2676-
2677- inst = self.new()
2678- self.assertEqual(inst._proxy, None)
2679- inst.connect('import_started', callback)
2680- self.assertTrue(isinstance(inst._proxy, ProxyObject))
2681- self.assertTrue(inst._proxy is inst.proxy)
2682-
2683- def test_kill(self):
2684- inst = self.new()
2685- self.assertEqual(self.service.poll(), None)
2686- inst.kill()
2687- self.assertTrue(inst._proxy is None)
2688- time.sleep(1) # Give dmedia-service time to shutdown
2689- self.assertEqual(self.service.poll(), 0)
2690-
2691- def test_import(self):
2692- inst = self.new()
2693- signals = SignalCapture(inst,
2694- 'batch_started',
2695- 'import_started',
2696- 'import_count',
2697- 'import_progress',
2698- 'import_finished',
2699- 'batch_finished',
2700- )
2701- mainloop = GObject.MainLoop()
2702- signals.handlers['batch_finished'].callback = mainloop.quit
2703-
2704- tmp = TempDir()
2705- base = tmp.path
2706-
2707- # Test with relative path
2708- self.assertEqual(
2709- inst.start_import('some/relative/path'),
2710- 'not_abspath'
2711- )
2712- self.assertEqual(
2713- inst.start_import('/media/EOS_DIGITAL/../../etc/ssh'),
2714- 'not_abspath'
2715- )
2716-
2717- # Test with non-dir
2718- nope = tmp.join('memory_card')
2719- self.assertEqual(inst.start_import(nope), 'not_a_dir')
2720- nope = tmp.touch('memory_card')
2721- self.assertEqual(inst.start_import(nope), 'not_a_dir')
2722- os.unlink(nope)
2723-
2724- # Test a real import
2725- (src1, src2, dup1) = prep_import_source(tmp)
2726- mov_size = path.getsize(sample_mov)
2727- thm_size = path.getsize(sample_thm)
2728- self.assertEqual(inst.list_imports(), [])
2729- self.assertEqual(inst.stop_import(base), 'not_running')
2730- self.assertEqual(inst.start_import(base), 'started')
2731- self.assertEqual(inst.start_import(base), 'already_running')
2732- self.assertEqual(inst.list_imports(), [base])
2733-
2734- # mainloop.quit() gets called at 'batch_finished' signal
2735- mainloop.run()
2736-
2737- self.assertEqual(inst.list_imports(), [])
2738- self.assertEqual(inst.stop_import(base), 'not_running')
2739-
2740- self.assertEqual(len(signals.messages), 8)
2741- batch_id = signals.messages[0][2]
2742- self.assertEqual(
2743- signals.messages[0],
2744- ('batch_started', inst, batch_id)
2745- )
2746- import_id = signals.messages[1][3]
2747- self.assertEqual(
2748- signals.messages[1],
2749- ('import_started', inst, base, import_id)
2750- )
2751- self.assertEqual(
2752- signals.messages[2],
2753- ('import_count', inst, base, import_id, 3)
2754- )
2755- self.assertEqual(
2756- signals.messages[3],
2757- ('import_progress', inst, base, import_id, 1, 3,
2758- dict(action='imported', src=src1)
2759- )
2760- )
2761- self.assertEqual(
2762- signals.messages[4],
2763- ('import_progress', inst, base, import_id, 2, 3,
2764- dict(action='imported', src=src2)
2765- )
2766- )
2767- self.assertEqual(
2768- signals.messages[5],
2769- ('import_progress', inst, base, import_id, 3, 3,
2770- dict(action='skipped', src=dup1)
2771- )
2772- )
2773- self.assertEqual(
2774- signals.messages[6],
2775- ('import_finished', inst, base, import_id,
2776- dict(
2777- imported=2,
2778- imported_bytes=(mov_size + thm_size),
2779- skipped=1,
2780- skipped_bytes=mov_size,
2781- )
2782- )
2783- )
2784- self.assertEqual(
2785- signals.messages[7],
2786- ('batch_finished', inst, batch_id,
2787- dict(
2788- imported=2,
2789- imported_bytes=(mov_size + thm_size),
2790- skipped=1,
2791- skipped_bytes=mov_size,
2792- )
2793- )
2794- )
2795
2796=== removed file 'dmedia/gtk/tests/test_firstrun.py'
2797--- dmedia/gtk/tests/test_firstrun.py 2011-03-27 08:01:30 +0000
2798+++ dmedia/gtk/tests/test_firstrun.py 1970-01-01 00:00:00 +0000
2799@@ -1,35 +0,0 @@
2800-# Authors:
2801-# Jason Gerard DeRose <jderose@novacut.com>
2802-#
2803-# dmedia: distributed media library
2804-# Copyright (C) 2010 Jason Gerard DeRose <jderose@novacut.com>
2805-#
2806-# This file is part of `dmedia`.
2807-#
2808-# `dmedia` is free software: you can redistribute it and/or modify it under the
2809-# terms of the GNU Affero General Public License as published by the Free
2810-# Software Foundation, either version 3 of the License, or (at your option) any
2811-# later version.
2812-#
2813-# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY
2814-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
2815-# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
2816-# details.
2817-#
2818-# You should have received a copy of the GNU Affero General Public License along
2819-# with `dmedia`. If not, see <http://www.gnu.org/licenses/>.
2820-
2821-"""
2822-Unit tests for `dmedia.firstrun` module.
2823-"""
2824-
2825-from unittest import TestCase
2826-
2827-from dmedia.gtkui import firstrun
2828-
2829-
2830-class test_FirstRunGUI(TestCase):
2831- klass = firstrun.FirstRunGUI
2832-
2833- def test_init(self):
2834- inst = self.klass()
2835
2836=== removed file 'dmedia/gtk/tests/test_widgets.py'
2837--- dmedia/gtk/tests/test_widgets.py 2011-08-31 18:17:37 +0000
2838+++ dmedia/gtk/tests/test_widgets.py 1970-01-01 00:00:00 +0000
2839@@ -1,142 +0,0 @@
2840-# Authors:
2841-# Jason Gerard DeRose <jderose@novacut.com>
2842-#
2843-# dmedia: distributed media library
2844-# Copyright (C) 2010 Jason Gerard DeRose <jderose@novacut.com>
2845-#
2846-# This file is part of `dmedia`.
2847-#
2848-# `dmedia` is free software: you can redistribute it and/or modify it under the
2849-# terms of the GNU Affero General Public License as published by the Free
2850-# Software Foundation, either version 3 of the License, or (at your option) any
2851-# later version.
2852-#
2853-# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY
2854-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
2855-# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
2856-# details.
2857-#
2858-# You should have received a copy of the GNU Affero General Public License along
2859-# with `dmedia`. If not, see <http://www.gnu.org/licenses/>.
2860-
2861-"""
2862-Unit tests for `dmedia.gtkui.widgets` module.
2863-"""
2864-
2865-from unittest import TestCase
2866-
2867-
2868-from dmedia.schema import random_id
2869-from dmedia.gtkui import widgets
2870-
2871-
2872-# Test oauth tokens - don't relpace with tokens you actually use!
2873-# Also, don't actually use these!
2874-tokens = {
2875- 'consumer_key': 'cVMqVGDNkC',
2876- 'consumer_secret': 'lhpRuaFaeI',
2877- 'token': 'NBGPaRrdXK',
2878- 'token_secret': 'SGtppOobin'
2879-}
2880-
2881-
2882-class DummyRequest(object):
2883- def __init__(self, uri):
2884- self._uri = uri
2885-
2886- def get_uri(self):
2887- return self._uri
2888-
2889-
2890-class DummyPolicy(object):
2891- def __init__(self):
2892- self._calls = []
2893-
2894- def ignore(self):
2895- self._calls.append('ignore')
2896-
2897- def use(self):
2898- self._calls.append('use')
2899-
2900- def download(self):
2901- self._calls.append('download')
2902-
2903-
2904-class SignalCollector(object):
2905- def __init__(self, couchview):
2906- self._couchview = couchview
2907- couchview.connect('play', self.on_play)
2908- couchview.connect('open', self.on_open)
2909- self._sigs = []
2910-
2911- def on_play(self, cv, *args):
2912- assert cv is self._couchview
2913- self._sigs.append(('play',) + args)
2914-
2915- def on_open(self, cv, *args):
2916- assert cv is self._couchview
2917- self._sigs.append(('open',) + args)
2918-
2919-
2920-class TestCouchView(TestCase):
2921- klass = widgets.CouchView
2922-
2923- def test_init(self):
2924- url = 'http://localhost:40705/dmedia/'
2925- netloc = 'localhost:40705'
2926-
2927- # Test with no oauth tokens provided:
2928- inst = self.klass(url)
2929- self.assertEqual(inst._couch_url, url)
2930- self.assertEqual(inst._couch_netloc, netloc)
2931- self.assertIsNone(inst._oauth)
2932-
2933- # Test with oauth tokens:
2934- inst = self.klass(url, oauth_tokens=tokens)
2935- self.assertEqual(inst._couch_url, url)
2936- self.assertEqual(inst._couch_netloc, netloc)
2937- self.assertIs(inst._oauth, tokens)
2938-
2939- def test_on_nav_policy_decision(self):
2940- # Method signature:
2941- # CouchView_on_nav_policy_decision(view, frame, request, nav, policy)
2942-
2943- url = 'http://localhost:40705/dmedia/'
2944- inst = self.klass(url)
2945- s = SignalCollector(inst)
2946- p = DummyPolicy()
2947-
2948- # Test a requset to desktopcouch
2949- r = DummyRequest('http://localhost:40705/foo/bar/baz')
2950- self.assertFalse(
2951- inst._on_nav_policy_decision(None, None, r, None, p)
2952- )
2953- self.assertEqual(p._calls, [])
2954- self.assertEqual(s._sigs, [])
2955-
2956- # Test a play:foo URI
2957- play = 'play:' + random_id() + '?start=17&end=69'
2958- r = DummyRequest(play)
2959- self.assertTrue(
2960- inst._on_nav_policy_decision(None, None, r, None, p)
2961- )
2962- self.assertEqual(p._calls, ['ignore'])
2963- self.assertEqual(s._sigs, [('play', play)])
2964-
2965- # Test opening an external URL
2966- lp = 'https://launchpad.net/dmedia'
2967- r = DummyRequest(lp)
2968- self.assertTrue(
2969- inst._on_nav_policy_decision(None, None, r, None, p)
2970- )
2971- self.assertEqual(p._calls, ['ignore', 'ignore'])
2972- self.assertEqual(s._sigs, [('play', play), ('open', lp)])
2973-
2974- # Test a URI that will just be ignored, not emit a signal
2975- nope = 'ftp://example.com'
2976- r = DummyRequest(nope)
2977- self.assertTrue(
2978- inst._on_nav_policy_decision(None, None, r, None, p)
2979- )
2980- self.assertEqual(p._calls, ['ignore', 'ignore', 'ignore'])
2981- self.assertEqual(s._sigs, [('play', play), ('open', lp)])
2982
2983=== removed file 'dmedia/gtk/widgets.py'
2984--- dmedia/gtk/widgets.py 2011-08-31 18:17:37 +0000
2985+++ dmedia/gtk/widgets.py 1970-01-01 00:00:00 +0000
2986@@ -1,225 +0,0 @@
2987-# Authors:
2988-# Jason Gerard DeRose <jderose@novacut.com>
2989-# David Green <david4dev@gmail.com>
2990-#
2991-# dmedia: distributed media library
2992-# Copyright (C) 2010 Jason Gerard DeRose <jderose@novacut.com>
2993-#
2994-# This file is part of `dmedia`.
2995-#
2996-# `dmedia` is free software: you can redistribute it and/or modify it under the
2997-# terms of the GNU Affero General Public License as published by the Free
2998-# Software Foundation, either version 3 of the License, or (at your option) any
2999-# later version.
3000-#
3001-# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY
3002-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
3003-# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
3004-# details.
3005-#
3006-# You should have received a copy of the GNU Affero General Public License along
3007-# with `dmedia`. If not, see <http://www.gnu.org/licenses/>.
3008-
3009-"""
3010-Custom dmedia GTK widgets, currently just `CouchView` and `BrowserMenu`.
3011-"""
3012-
3013-from urlparse import urlparse, parse_qsl
3014-
3015-from microfiber import _oauth_header
3016-from gi.repository import GObject, WebKit, Gtk
3017-
3018-from .menu import MENU, ACTIONS
3019-from gettext import gettext as _
3020-
3021-
3022-class CouchView(WebKit.WebView):
3023- """
3024- Transparently sign desktopcouch requests with OAuth.
3025-
3026- desktopcouch uses OAuth to authenticate HTTP requests to CouchDB. Well,
3027- technically it can also use basic auth, but if you do this, Stuart Langridge
3028- will be very cross with you!
3029-
3030- This class wraps a ``gi.repository.WebKit.WebView`` so that you can have a
3031- single web app that:
3032-
3033- 1. Can run in a browser and talk to a remote CouchDB over HTTPS with
3034- basic auth
3035-
3036- 2. Can also run in embedded WebKit and talk to the local desktopcouch
3037- over HTTP with OAuth
3038-
3039- Being able to do this sort of thing transparently is a big reason why dmedia
3040- and Novacut are designed the way they are.
3041-
3042- For some background, see:
3043-
3044- https://bugs.launchpad.net/dmedia/+bug/677697
3045-
3046- http://oauth.net/
3047-
3048- Special thanks to Stuart Langridge for the example code that helped get this
3049- working.
3050- """
3051-
3052- __gsignals__ = {
3053- 'play': (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE,
3054- [GObject.TYPE_PYOBJECT]
3055- ),
3056- 'open': (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE,
3057- [GObject.TYPE_PYOBJECT]
3058- ),
3059- }
3060-
3061- def __init__(self, couch_url, oauth_tokens=None):
3062- super(CouchView, self).__init__()
3063- self._couch_url = couch_url
3064- self._couch_netloc = urlparse(couch_url).netloc
3065- self.connect('resource-request-starting', self._on_resource_request)
3066- self.connect('navigation-policy-decision-requested',
3067- self._on_nav_policy_decision
3068- )
3069- self._oauth = oauth_tokens
3070-
3071- def _on_nav_policy_decision(self, view, frame, request, nav, policy):
3072- """
3073- Handle user trying to Navigate away from current page.
3074-
3075- Note that this will be called before `CouchView._on_resource_request()`.
3076-
3077- The *policy* arg is a ``WebPolicyDecision`` instance. To handle the
3078- decision, call one of:
3079-
3080- * ``WebPolicyDecision.ignore()``
3081- * ``WebPolicyDecision.use()``
3082- * ``WebPolicyDecision.download()``
3083-
3084- And then return ``True``.
3085-
3086- Otherwise, return ``False`` or ``None`` to have the WebKit default
3087- behavior apply.
3088- """
3089- uri = request.get_uri()
3090- u = urlparse(uri)
3091- if u.netloc == self._couch_netloc and u.scheme in ('http', 'https'):
3092- return False
3093- if uri.startswith('play:'):
3094- self.emit('play', uri)
3095- elif u.netloc != self._couch_netloc and u.scheme in ('http', 'https'):
3096- self.emit('open', uri)
3097- policy.ignore()
3098- return True
3099-
3100- def _on_resource_request(self, view, frame, resource, request, response):
3101- """
3102- When appropriate, OAuth-sign the request prior to it being sent.
3103-
3104- This will only sign requests on the same host and port in the URL passed
3105- to `CouchView.__init__()`.
3106- """
3107- uri = request.get_uri()
3108- u = urlparse(uri)
3109- if u.scheme not in ('http', 'https'): # Ignore data:foo requests
3110- return
3111- if u.netloc != self._couch_netloc: # Dont sign requests to broader net!
3112- return
3113- if not self._oauth: # OAuth info wasn't provided
3114- return
3115- query = dict(parse_qsl(u.query))
3116- # Handle bloody CouchDB having foo.html?dbname URLs, that is
3117- # a querystring which isn't of the form foo=bar
3118- if u.query and not query:
3119- query = {u.query: ''}
3120- baseurl = ''.join([u.scheme, '://', u.netloc, u.path])
3121- method = request.props.message.props.method
3122- h = _oauth_header(self._oauth, method, baseurl, query)
3123- for key in h:
3124- request.props.message.props.request_headers.append(k, h[k])
3125-
3126-
3127-class BrowserMenu(Gtk.MenuBar):
3128- """
3129- The BrowserMenu class creates a menubar for dmedia-gtk, the dmedia
3130- media browser.
3131-
3132- The menu argument specifies the layout of the menu as a list of menubar
3133- items. Each item is a dictionary. There are 2 main types of item: action and
3134- menu.
3135-
3136- Actions are menu items that do something when clicked. The dictionary
3137- for an action looks like this:
3138- {
3139- "label" : "text to display",
3140- "type" : "action",
3141- "action" : "action id"
3142- }
3143- The label is the text to display (eg. "Close"). The type tells BrowserMenu
3144- that this item is an action not a menu. The action is a string that is looked
3145- up in the actions dictionary and refers to a callback function that is executed
3146- when this menu item is clicked.
3147-
3148- Menus are submenus of the menubar. These can hold other menus and actions.
3149- The dictionary for a menu looks like this:
3150- {
3151- "label" : "text to display",
3152- "type" : "menu",
3153- "items" : [item_1, item_2 ... item_n]
3154- }
3155- The label is the text to display (eg. "File"). The type tells BrowserMenu
3156- that this item is a menu not an action. "items" is a list of other items
3157- that appear in this menu.
3158-
3159- The actions argument is a dictionary of action IDs (strings) and callback
3160- functions.
3161- {
3162- "action1" : lambda *a: ... ,
3163- "action2" : my_object.method,
3164- "action3" : some_function
3165- }
3166-
3167- If menu or actions are not specified the default will be MENU and
3168- ACTIONS repectively which are defined in menu.py.
3169-
3170- In addition to the main 2 types of menu item, there is a "custom"
3171- item that allows for any gtk widget to be put in the menu as long
3172- as gtk itself allows for this.
3173-
3174- The dictionary for a custom item looks like this:
3175- {
3176- "type" : "custom",
3177- "widget" : gtk_widget
3178- }
3179- """
3180- def __init__(self, menu=MENU, actions=ACTIONS):
3181- super(BrowserMenu, self).__init__()
3182- self.show()
3183- self.menu = menu
3184- self.actions = actions
3185- self.add_items_to_menu(self, *self.make_menu(self.menu))
3186-
3187- def add_items_to_menu(self, menu, *items):
3188- for item in items:
3189- menu.append(item)
3190-
3191- def make_menu(self, menu):
3192- items = []
3193- for i in menu:
3194- if i["type"] == "custom":
3195- items.append(i["widget"]) #allows for custom widgets, eg. separators
3196- else:
3197- item = Gtk.MenuItem()
3198- item.show()
3199- item.set_property("use-underline", True) #allow keyboard nav
3200- item.set_label(_(i["label"]))
3201- if i["type"] == "menu":
3202- submenu = Gtk.Menu()
3203- submenu.show()
3204- self.add_items_to_menu(submenu, *self.make_menu(i["items"]))
3205- item.set_submenu(submenu)
3206- elif i["type"] == "action":
3207- item.connect("activate", self.actions[i["action"]])
3208- items.append(item)
3209- return items
3210-
3211-
3212
3213=== removed file 'dmedia/service/tests/test_api.py'
3214--- dmedia/service/tests/test_api.py 2011-09-16 03:35:25 +0000
3215+++ dmedia/service/tests/test_api.py 1970-01-01 00:00:00 +0000
3216@@ -1,110 +0,0 @@
3217-# Authors:
3218-# Jason Gerard DeRose <jderose@novacut.com>
3219-#
3220-# dmedia: distributed media library
3221-# Copyright (C) 2011 Jason Gerard DeRose <jderose@novacut.com>
3222-#
3223-# This file is part of `dmedia`.
3224-#
3225-# `dmedia` is free software: you can redistribute it and/or modify it under the
3226-# terms of the GNU Affero General Public License as published by the Free
3227-# Software Foundation, either version 3 of the License, or (at your option) any
3228-# later version.
3229-#
3230-# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY
3231-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
3232-# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
3233-# details.
3234-#
3235-# You should have received a copy of the GNU Affero General Public License along
3236-# with `dmedia`. If not, see <http://www.gnu.org/licenses/>.
3237-
3238-"""
3239-Unit tests for `dmedia.api` module.
3240-"""
3241-
3242-import os
3243-from os import path
3244-from subprocess import Popen
3245-import json
3246-import time
3247-
3248-from microfiber import NotFound
3249-
3250-import dmedia
3251-from dmedia.abstractcouch import get_db
3252-from dmedia import api
3253-
3254-from .couch import CouchCase
3255-from .helpers import random_bus
3256-
3257-
3258-tree = path.dirname(path.dirname(path.abspath(dmedia.__file__)))
3259-assert path.isfile(path.join(tree, 'setup.py'))
3260-script = path.join(tree, 'dmedia-service')
3261-assert path.isfile(script)
3262-
3263-
3264-class TestDMedia(CouchCase):
3265- klass = api.DMedia
3266-
3267- def setUp(self):
3268- """
3269- Launch dmedia dbus service using a random bus name.
3270-
3271- This will launch dmedia-service with a random bus name like this:
3272-
3273- dmedia-service --bus org.test3ISHAWZVSWVN5I5S.DMedia
3274-
3275- How do people usually unit test dbus services? This works, but not sure
3276- if there is a better idiom in common use. --jderose
3277- """
3278- super(TestDMedia, self).setUp()
3279- self.bus = random_bus()
3280- cmd = [script,
3281- '--bus', self.bus,
3282- '--env', json.dumps(self.env),
3283- ]
3284- self.service = Popen(cmd)
3285- time.sleep(1) # Give dmedia-service time to start
3286-
3287- def tearDown(self):
3288- super(TestDMedia, self).tearDown()
3289- try:
3290- self.service.terminate()
3291- self.service.wait()
3292- except OSError:
3293- pass
3294- finally:
3295- self.service = None
3296-
3297- def test_all(self):
3298- inst = self.klass(self.bus)
3299-
3300- # DMedia.Version()
3301- self.assertEqual(inst.version(), dmedia.__version__)
3302-
3303- # DMedia.GetEnv()
3304- env = inst.get_env()
3305- self.assertEqual(env['oauth'], self.env['oauth'])
3306- self.assertEqual(env['url'], self.env['url'])
3307- self.assertEqual(env['dbname'], self.env['dbname'])
3308-
3309- # DMedia.HasApp()
3310- db = get_db(self.env)
3311- with self.assertRaises(NotFound) as cm:
3312- db.get('app')
3313- self.assertTrue(inst.has_app())
3314- self.assertTrue(db.get('app')['_rev'].startswith('1-'))
3315- self.assertTrue(inst.has_app())
3316- self.assertTrue(db.get('app')['_rev'].startswith('1-'))
3317-
3318- # DMedia.ListTransfers()
3319- self.assertEqual(inst.list_transfers(), [])
3320-
3321- # DMedia.Kill()
3322- self.assertIsNone(self.service.poll(), None)
3323- inst.kill()
3324- self.assertTrue(inst._proxy is None)
3325- time.sleep(1) # Give dmedia-service time to shutdown
3326- self.assertEqual(self.service.poll(), 0)
3327
3328=== added file 'dmedia/service/tests/test_udisks.py'
3329--- dmedia/service/tests/test_udisks.py 1970-01-01 00:00:00 +0000
3330+++ dmedia/service/tests/test_udisks.py 2012-04-05 13:22:39 +0000
3331@@ -0,0 +1,183 @@
3332+# dmedia: dmedia hashing protocol and file layout
3333+# Copyright (C) 2012 Novacut Inc
3334+#
3335+# This file is part of `dmedia`.
3336+#
3337+# `dmedia` is free software: you can redistribute it and/or modify it under
3338+# the terms of the GNU Affero General Public License as published by the Free
3339+# Software Foundation, either version 3 of the License, or (at your option) any
3340+# later version.
3341+#
3342+# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY
3343+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
3344+# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
3345+# details.
3346+#
3347+# You should have received a copy of the GNU Affero General Public License along
3348+# with `dmedia`. If not, see <http://www.gnu.org/licenses/>.
3349+#
3350+# Authors:
3351+# Jason Gerard DeRose <jderose@novacut.com>
3352+
3353+"""
3354+Unit tests for `dmedia.service.udisks`.
3355+"""
3356+
3357+from unittest import TestCase
3358+import os
3359+from os import path
3360+import json
3361+
3362+from microfiber import random_id
3363+
3364+from dmedia.tests.base import TempDir
3365+from dmedia.service import udisks
3366+
3367+
3368+class TestFunctions(TestCase):
3369+ def test_major_minor(self):
3370+ tmp = TempDir()
3371+ st_dev = os.stat(tmp.dir).st_dev
3372+ major = os.major(st_dev)
3373+ minor = os.minor(st_dev)
3374+
3375+ # Test on the tmp dir:
3376+ self.assertEqual(udisks.major_minor(tmp.dir), (major, minor))
3377+
3378+ # Test on a subdir
3379+ subdir = tmp.makedirs('subdir')
3380+ self.assertEqual(udisks.major_minor(subdir), (major, minor))
3381+
3382+ # Test on a file
3383+ some_file = tmp.touch('some_file')
3384+ self.assertEqual(udisks.major_minor(some_file), (major, minor))
3385+
3386+ # Test on a non-existant path
3387+ nope = tmp.join('nope')
3388+ with self.assertRaises(OSError) as cm:
3389+ udisks.major_minor(nope)
3390+ self.assertEqual(cm.exception.errno, 2)
3391+
3392+ def test_usable_mount(self):
3393+ self.assertEqual(udisks.usable_mount(['/media/foo']), '/media/foo')
3394+ self.assertEqual(udisks.usable_mount(['/srv/bar']), '/srv/bar')
3395+
3396+ self.assertIsNone(udisks.usable_mount(['/media']))
3397+ self.assertIsNone(udisks.usable_mount(['/srv']))
3398+ self.assertIsNone(udisks.usable_mount(['/srv', '/media']))
3399+
3400+ self.assertEqual(
3401+ udisks.usable_mount(['/media/foo', '/srv/bar']),
3402+ '/media/foo'
3403+ )
3404+ self.assertEqual(
3405+ udisks.usable_mount(['/srv/bar', '/media/foo']),
3406+ '/srv/bar'
3407+ )
3408+ self.assertEqual(
3409+ udisks.usable_mount(['/', '/home', '/home/user', '/tmp', '/media/foo']),
3410+ '/media/foo'
3411+ )
3412+
3413+ def test_partition_info(self):
3414+ d = {
3415+ 'DeviceSize': 16130244608,
3416+ 'IdType': 'vfat',
3417+ 'IdVersion': 'FAT32',
3418+ 'IdLabel': 'H4N_SD',
3419+ 'PartitionNumber': 1,
3420+ 'IdUuid': '89E3-CE4D',
3421+ }
3422+ self.assertEqual(udisks.partition_info(d),
3423+ {
3424+ 'mount': None,
3425+ 'info': {
3426+ 'bytes': 16130244608,
3427+ 'filesystem': 'vfat',
3428+ 'filesystem_version': 'FAT32',
3429+ 'label': 'H4N_SD',
3430+ 'number': 1,
3431+ 'size': '16.1 GB',
3432+ 'uuid': '89E3-CE4D'
3433+ },
3434+ }
3435+ )
3436+ self.assertEqual(udisks.partition_info(d, '/media/foo'),
3437+ {
3438+ 'mount': '/media/foo',
3439+ 'info': {
3440+ 'bytes': 16130244608,
3441+ 'filesystem': 'vfat',
3442+ 'filesystem_version': 'FAT32',
3443+ 'label': 'H4N_SD',
3444+ 'number': 1,
3445+ 'size': '16.1 GB',
3446+ 'uuid': '89E3-CE4D'
3447+ },
3448+ }
3449+ )
3450+
3451+ def test_drive_info(self):
3452+ d = {
3453+ 'DeviceSize': 16134438912,
3454+ 'DriveConnectionInterface': 'usb',
3455+ 'DriveModel': 'CFUDMASD',
3456+ 'DeviceIsSystemInternal': False,
3457+ 'DriveSerial': 'AA0000000009019',
3458+ }
3459+ self.assertEqual(udisks.drive_info(d),
3460+ {
3461+ 'partitions': {},
3462+ 'info': {
3463+ 'bytes': 16134438912,
3464+ 'connection': 'usb',
3465+ 'model': 'CFUDMASD',
3466+ 'removable': True,
3467+ 'serial': 'AA0000000009019',
3468+ 'size': '16.1 GB',
3469+ 'text': '16.1 GB Removable Drive',
3470+ },
3471+ }
3472+ )
3473+
3474+ d['DeviceIsSystemInternal'] = True
3475+ self.assertEqual(udisks.drive_info(d),
3476+ {
3477+ 'partitions': {},
3478+ 'info': {
3479+ 'bytes': 16134438912,
3480+ 'connection': 'usb',
3481+ 'model': 'CFUDMASD',
3482+ 'removable': False,
3483+ 'serial': 'AA0000000009019',
3484+ 'size': '16.1 GB',
3485+ 'text': '16.1 GB Drive',
3486+ },
3487+ }
3488+ )
3489+
3490+ def test_get_filestore_id(self):
3491+ tmp = TempDir()
3492+
3493+ # Test when file and directory are missing
3494+ self.assertIsNone(udisks.get_filestore_id(tmp.dir))
3495+
3496+ # Test when control dir exists, file missing
3497+ basedir = tmp.makedirs('.dmedia')
3498+ self.assertTrue(path.isdir(basedir))
3499+ self.assertIsNone(udisks.get_filestore_id(tmp.dir))
3500+
3501+ # Test when file is empty
3502+ store = tmp.touch('.dmedia', 'store.json')
3503+ self.assertTrue(path.isfile(store))
3504+ self.assertIsNone(udisks.get_filestore_id(tmp.dir))
3505+
3506+ # Test when correct
3507+ _id = random_id()
3508+ json.dump({'_id': _id}, open(store, 'w'))
3509+ self.assertEqual(udisks.get_filestore_id(tmp.dir), _id)
3510+
3511+ # Test when '_id' is missed from dict:
3512+ json.dump({'id': _id}, open(store, 'w'))
3513+ self.assertIsNone(udisks.get_filestore_id(tmp.dir))
3514+
3515
3516=== added file 'dmedia/service/udisks.py'
3517--- dmedia/service/udisks.py 1970-01-01 00:00:00 +0000
3518+++ dmedia/service/udisks.py 2012-04-05 13:22:39 +0000
3519@@ -0,0 +1,299 @@
3520+# dmedia: distributed media library
3521+# Copyright (C) 2012 Novacut Inc
3522+#
3523+# This file is part of `dmedia`.
3524+#
3525+# `dmedia` is free software: you can redistribute it and/or modify it under
3526+# the terms of the GNU Affero General Public License as published by the Free
3527+# Software Foundation, either version 3 of the License, or (at your option) any
3528+# later version.
3529+#
3530+# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY
3531+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
3532+# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
3533+# details.
3534+#
3535+# You should have received a copy of the GNU Affero General Public License along
3536+# with `dmedia`. If not, see <http://www.gnu.org/licenses/>.
3537+#
3538+# Authors:
3539+# Jason Gerard DeRose <jderose@novacut.com>
3540+
3541+"""
3542+Attempts to tame the UDisks beast.
3543+"""
3544+
3545+import json
3546+import os
3547+from os import path
3548+from gettext import gettext as _
3549+import logging
3550+
3551+from gi.repository import GObject
3552+from filestore import DOTNAME
3553+
3554+from dmedia.units import bytes10
3555+from dmedia.service.dbus import system
3556+
3557+
3558+log = logging.getLogger()
3559+TYPE_PYOBJECT = GObject.TYPE_PYOBJECT
3560+
3561+
3562+def major_minor(parentdir):
3563+ st_dev = os.stat(parentdir).st_dev
3564+ return (os.major(st_dev), os.minor(st_dev))
3565+
3566+
3567+def usable_mount(mounts):
3568+ """
3569+ Return mount point at which Dmedia will look for file-stores.
3570+
3571+ For example:
3572+
3573+ >>> usable_mount(['/', '/tmp']) is None
3574+ True
3575+ >>> usable_mount(['/', '/tmp', '/media/foo'])
3576+ '/media/foo'
3577+
3578+ """
3579+ for mount in mounts:
3580+ if mount.startswith('/media/') or mount.startswith('/srv/'):
3581+ return mount
3582+
3583+
3584+def partition_info(d, mount=None):
3585+ return {
3586+ 'mount': mount,
3587+ 'info': {
3588+ 'label': d['IdLabel'],
3589+ 'uuid': d['IdUuid'],
3590+ 'bytes': d['DeviceSize'],
3591+ 'size': bytes10(d['DeviceSize']),
3592+ 'filesystem': d['IdType'],
3593+ 'filesystem_version': d['IdVersion'],
3594+ 'number': d['PartitionNumber'],
3595+ },
3596+ }
3597+
3598+
3599+def drive_text(d):
3600+ if d['DeviceIsSystemInternal']:
3601+ template = _('{size} Drive')
3602+ else:
3603+ template = _('{size} Removable Drive')
3604+ return template.format(size=bytes10(d['DeviceSize']))
3605+
3606+
3607+def drive_info(d):
3608+ return {
3609+ 'partitions': {},
3610+ 'info': {
3611+ 'serial': d['DriveSerial'],
3612+ 'bytes': d['DeviceSize'],
3613+ 'size': bytes10(d['DeviceSize']),
3614+ 'model': d['DriveModel'],
3615+ 'removable': not d['DeviceIsSystemInternal'],
3616+ 'connection': d['DriveConnectionInterface'],
3617+ 'text': drive_text(d),
3618+ }
3619+ }
3620+
3621+
3622+def get_filestore_id(parentdir):
3623+ store = path.join(parentdir, DOTNAME, 'store.json')
3624+ try:
3625+ return json.load(open(store, 'r'))['_id']
3626+ except Exception:
3627+ pass
3628+
3629+
3630+
3631+class Device:
3632+ __slots__ = ('obj', 'proxy', 'cache', 'ispartition', 'drive')
3633+
3634+ def __init__(self, obj):
3635+ self.obj = obj
3636+ self.proxy = system.get(
3637+ 'org.freedesktop.UDisks',
3638+ obj,
3639+ 'org.freedesktop.DBus.Properties'
3640+ )
3641+ self.cache = {}
3642+ self.ispartition = self['DeviceIsPartition']
3643+ if self.ispartition:
3644+ self.drive = self['PartitionSlave']
3645+ else:
3646+ self.drive = None
3647+
3648+ def __repr__(self):
3649+ return '{}({!r})'.format(self.__class__.__name__, self.obj)
3650+
3651+ def __getitem__(self, key):
3652+ try:
3653+ return self.cache[key]
3654+ except KeyError:
3655+ value = self.proxy.Get('(ss)', 'org.freedesktop.UDisks.Device', key)
3656+ self.cache[key] = value
3657+ return value
3658+
3659+ @property
3660+ def ismounted(self):
3661+ return self['DeviceIsMounted']
3662+
3663+ def get_all(self):
3664+ return self.proxy.GetAll('(s)', 'org.freedesktop.UDisks.Device')
3665+
3666+ def reset(self):
3667+ self.cache.clear()
3668+
3669+
3670+class UDisks(GObject.GObject):
3671+ __gsignals__ = {
3672+ 'card_added': (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE,
3673+ [TYPE_PYOBJECT, TYPE_PYOBJECT]
3674+ ),
3675+ 'card_removed': (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE,
3676+ [TYPE_PYOBJECT, TYPE_PYOBJECT]
3677+ ),
3678+ 'store_removed': (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE,
3679+ [TYPE_PYOBJECT, TYPE_PYOBJECT, TYPE_PYOBJECT]
3680+ ),
3681+ 'store_added': (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE,
3682+ [TYPE_PYOBJECT, TYPE_PYOBJECT, TYPE_PYOBJECT]
3683+ ),
3684+ }
3685+
3686+ def __init__(self):
3687+ super().__init__()
3688+ self.devices = {}
3689+ self.drives = {}
3690+ self.cards = {}
3691+ self.stores = {}
3692+ self.proxy = system.get(
3693+ 'org.freedesktop.UDisks',
3694+ '/org/freedesktop/UDisks'
3695+ )
3696+
3697+ def monitor(self):
3698+ user = path.abspath(os.environ['HOME'])
3699+ home = path.dirname(user)
3700+
3701+ home_p = self.find(home)
3702+ try:
3703+ user_p = self.find(user)
3704+ except Exception:
3705+ user_p = home_p
3706+ self.special = {
3707+ home: home_p,
3708+ user: user_p,
3709+ }
3710+ self.proxy.connect('g-signal', self.on_g_signal)
3711+ for obj in self.proxy.EnumerateDevices():
3712+ self.change_device(obj)
3713+
3714+ def find(self, parentdir):
3715+ """
3716+ Return DBus object path of partition containing *parentdir*.
3717+ """
3718+ (major, minor) = major_minor(parentdir)
3719+ return self.proxy.FindDeviceByMajorMinor('(xx)', major, minor)
3720+
3721+ def on_g_signal(self, proxy, sender, signal, params):
3722+ if signal == 'DeviceChanged':
3723+ self.change_device(params.unpack()[0])
3724+ elif signal == 'DeviceRemoved':
3725+ self.remove_device(params.unpack()[0])
3726+
3727+ def get_device(self, obj):
3728+ if obj not in self.devices:
3729+ self.devices[obj] = Device(obj)
3730+ return self.devices[obj]
3731+
3732+ def get_drive(self, obj):
3733+ if obj not in self.drives:
3734+ d = self.get_device(obj)
3735+ self.drives[obj] = drive_info(d)
3736+ return self.drives[obj]
3737+
3738+ def change_device(self, obj):
3739+ d = self.get_device(obj)
3740+ if not d.ispartition:
3741+ return
3742+ d.reset()
3743+ if d.ismounted:
3744+ mount = usable_mount(d['DeviceMountPaths'])
3745+ if mount is None:
3746+ return
3747+ part = partition_info(d, mount)
3748+ drive = self.get_drive(d.drive)
3749+ drive['partitions'][obj] = part
3750+ store_id = get_filestore_id(mount)
3751+ if store_id:
3752+ self.add_store(obj, mount, store_id)
3753+ elif drive['info']['removable']:
3754+ self.add_card(obj, mount)
3755+ else:
3756+ try:
3757+ del self.drives[d.drive]['partitions'][obj]
3758+ if not self.drives[d.drive]['partitions']:
3759+ del self.drives[d.drive]
3760+ except KeyError:
3761+ pass
3762+ self.remove_store(obj)
3763+ self.remove_card(obj)
3764+
3765+ def add_card(self, obj, mount):
3766+ if obj in self.cards:
3767+ return
3768+ self.cards[obj] = mount
3769+ log.info('card_added %r %r', obj, mount)
3770+ self.emit('card_added', obj, mount)
3771+
3772+ def remove_card(self, obj):
3773+ try:
3774+ mount = self.cards.pop(obj)
3775+ log.info('card_removed %r %r', obj, mount)
3776+ self.emit('card_removed', obj, mount)
3777+ except KeyError:
3778+ pass
3779+
3780+ def add_store(self, obj, mount, store_id):
3781+ if obj in self.stores:
3782+ return
3783+ self.stores[obj] = {'parentdir': mount, 'id': store_id}
3784+ log.info('store_added %r %r %r', obj, mount, store_id)
3785+ self.emit('store_added', obj, mount, store_id)
3786+
3787+ def remove_store(self, obj):
3788+ try:
3789+ d = self.stores.pop(obj)
3790+ log.info('store_removed %r %r %r', obj, d['parentdir'], d['id'])
3791+ self.emit('store_removed', obj, d['parentdir'], d['id'])
3792+ except KeyError:
3793+ pass
3794+
3795+ def remove_device(self, obj):
3796+ try:
3797+ del self.devices[obj]
3798+ except KeyError:
3799+ pass
3800+
3801+ def get_parentdir_info(self, parentdir):
3802+ obj = self.find(parentdir)
3803+ d = self.get_device(obj)
3804+ return {
3805+ 'parentdir': parentdir,
3806+ 'partition': partition_info(d)['info'],
3807+ 'drive': self.get_drive(d.drive)['info'],
3808+ }
3809+
3810+ def json(self):
3811+ d = {
3812+ 'drives': self.drives,
3813+ 'stores': self.stores,
3814+ 'cards': self.cards,
3815+ 'special': self.special,
3816+ }
3817+ return json.dumps(d, sort_keys=True, indent=4)
3818+
3819
3820=== removed directory 'dmedia/webui'
3821=== removed file 'dmedia/webui/__init__.py'
3822--- dmedia/webui/__init__.py 2011-03-28 21:55:42 +0000
3823+++ dmedia/webui/__init__.py 1970-01-01 00:00:00 +0000
3824@@ -1,27 +0,0 @@
3825-# Authors:
3826-# Jason Gerard DeRose <jderose@novacut.com>
3827-#
3828-# dmedia: distributed media library
3829-# Copyright (C) 2011 Jason Gerard DeRose <jderose@novacut.com>
3830-#
3831-# This file is part of `dmedia`.
3832-#
3833-# `dmedia` is free software: you can redistribute it and/or modify it under the
3834-# terms of the GNU Affero General Public License as published by the Free
3835-# Software Foundation, either version 3 of the License, or (at your option) any
3836-# later version.
3837-#
3838-# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY
3839-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
3840-# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
3841-# details.
3842-#
3843-# You should have received a copy of the GNU Affero General Public License along
3844-# with `dmedia`. If not, see <http://www.gnu.org/licenses/>.
3845-
3846-"""
3847-Core HTML5 UI components.
3848-
3849-This is used both when running a web-accesible dmedia server, and when running
3850-an HTML5 UI in embedded WebKit.
3851-"""
3852
3853=== removed file 'dmedia/webui/js.py'
3854--- dmedia/webui/js.py 2011-04-17 23:00:44 +0000
3855+++ dmedia/webui/js.py 1970-01-01 00:00:00 +0000
3856@@ -1,552 +0,0 @@
3857-# Authors:
3858-# Jason Gerard DeRose <jderose@novacut.com>
3859-#
3860-# dmedia: distributed media library
3861-# Copyright (C) 2011 Jason Gerard DeRose <jderose@novacut.com>
3862-#
3863-# This file is part of `dmedia`.
3864-#
3865-# `dmedia` is free software: you can redistribute it and/or modify it under the
3866-# terms of the GNU Affero General Public License as published by the Free
3867-# Software Foundation, either version 3 of the License, or (at your option) any
3868-# later version.
3869-#
3870-# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY
3871-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
3872-# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
3873-# details.
3874-#
3875-# You should have received a copy of the GNU Affero General Public License along
3876-# with `dmedia`. If not, see <http://www.gnu.org/licenses/>.
3877-
3878-"""
3879-Allow ``unittest.TestCase`` assert methods to be called from JavaScript.
3880-
3881-
3882-Example
3883-=======
3884-
3885-On the Python side, you will need a `JSTestCase` subclass with one or more
3886-test methods, something like this:
3887-
3888->>> class TestFoo(JSTestCase):
3889-... js_files = (
3890-... '/abs/path/to/script/foo.js',
3891-... '/abs/path/to/script/test_foo.js',
3892-... )
3893-...
3894-... def test_bar(self):
3895-... # 1. Optionally do something cool like initialize a Couch DB
3896-... self.run_js() # 2. Call test_bar() JavaScript function
3897-... # 3. Optionally do something cool like verify changes in a Couch DB
3898-
3899-
3900-Your foo.js file (containing the ``bar()`` function) would look something like
3901-this:
3902-
3903- ::
3904-
3905- function bar() {
3906- return 'My JS unit testing is awesome';
3907- }
3908-
3909-
3910-And your test_foo.js file (containing the ``test_bar()`` function) would look
3911-something like this:
3912-
3913- ::
3914-
3915- py.TestFoo = {
3916- test_bar: function() {
3917- py.assertEqual(bar(), 'My JS unit testing is awesome');
3918- py.assertNotEqual(bar(), 'My JS unit testing is lame');
3919- },
3920- }
3921-
3922-
3923-When you call, say, ``py.assertEqual()`` from your JavaScript test, the test
3924-harness will ``JSON.stringify()`` the arguments and forward the call to the test
3925-server using a synchronous XMLHttpRequest. When the main process receives this
3926-request from the `ResultsApp`, the actual test is performed using
3927-``TestCase.assertEqual()``.
3928-
3929-All the reporting is handled by the Python unit test framework, and you get an
3930-overall pass/fail for the combined Python and JavaScript tests when you run:
3931-
3932- ::
3933-
3934- ./setup.py test
3935-
3936-
3937-Oh, and the tests run headless in embedded WebKit making this viable for
3938-automatic tests run with, say, Tarmac:
3939-
3940- https://launchpad.net/tarmac
3941-
3942-
3943-Why this is Awesome
3944-===================
3945-
3946-You might be thinking, "Why reinvent the wheel, why not just use an existing
3947-JavaScript unit testing framework?"
3948-
3949-Fair enough, but the entire client side piece for this test framework is just 39
3950-lines of JavaScript plus a bit of JSON data that gets written automatically.
3951-Yet those 39 lines of JavaScript give you access to all these rich test methods:
3952-
3953- - ``TestCase.assertTrue()``
3954- - ``TestCase.assertFalse()``
3955- - ``TestCase.assertEqual()``
3956- - ``TestCase.assertNotEqual()``
3957- - ``TestCase.assertAlmostEqual()``
3958- - ``TestCase.assertNotAlmostEqual()``
3959- - ``TestCase.assertGreater()``
3960- - ``TestCase.assertGreaterEqual()``
3961- - ``TestCase.assertLess()``
3962- - ``TestCase.assertLessEqual()``
3963- - ``TestCase.assertIn()``
3964- - ``TestCase.assertNotIn()``
3965- - ``TestCase.assertItemsEqual()``
3966- - ``TestCase.assertIsNone()``
3967- - ``TestCase.assertIsNotNone()``
3968- - ``TestCase.assertRegexpMatches()``
3969- - ``TestCase.assertNotRegexpMatches()``
3970-
3971-
3972-Existing JavaScript unit test frameworks don't tend to lend themselves to
3973-automatic testing as they typically report the results graphically in the
3974-browser... which is totally worthless if you want to run tests as part of an
3975-automatic pre-commit step. Remember what the late, great Ian Clatworthy taught
3976-us: pre-commit continuous integration is 100% awesome.
3977-
3978-If this weren't enough, there are some reasons why dmedia and Novacut in
3979-particular benefit from this type of JavaScript testing. Both use HTML5 user
3980-interfaces that do everything, and I mean *everything*, by talking directly to
3981-CouchDB with XMLHttpRequest. The supporting core JavaScript needs to be very
3982-solid to enable great user experiences to be built atop.
3983-
3984-Considering the importance of these code paths, it's very handy to do setup from
3985-Python before calling `JSTestCase.run_js()`... for example, putting a CouchDB
3986-database into a known state. Likewise, it's very handy to do out-of-band checks
3987-from Python after `JSTestCase.run_js()` completes... for example, verifying that
3988-the expected changes were made to the CouchDB database.
3989-
3990-
3991-How it Works
3992-============
3993-
3994-When you call `JSTestCase.run_js()`, the following happens:
3995-
3996- 1. The correct HTML and JavaScript for the test are prepared
3997-
3998- 2. The `ResultsApp` is fired up in a ``multiprocessing.Process``
3999-
4000- 3. The ``dummy-client`` script is launched using ``subprocess.Popen()``
4001-
4002- 4. The ``dummy-client`` requests the HTML and JavaScript from the
4003- `ResultsApp`
4004-
4005- 5. The ``dummy-client`` executes the ``py.run()`` JavaScript function and
4006- posts the results to the `ResultsApp` as the test runs
4007-
4008- 6. The `ResultsApp` puts the results in a ``multiprocessing.Queue`` as it
4009- receives them
4010-
4011- 7. In the main process, `JSTestCase.collect_results()` gets results from the
4012- queue and tests the assertions till the test completes, an exception is
4013- raised, or the 5 second timeout is exceeded
4014-
4015- 8. The `JSTestCase.tearDown()` method terminates the ``dummy-client`` and
4016- `ResultsApp` processes
4017-
4018-
4019-FIXME:
4020-
4021- 1. Need to be able to supply arbitrary files to ResultsApp so tests can GET
4022- these files... this is especially important for testing on binary data,
4023- which we can't directly make available to JavaScript. We can borrow code
4024- from test-server.py, which already makes this totally generic.
4025-"""
4026-
4027-
4028-from unittest import TestCase
4029-import sys
4030-from os import path
4031-from subprocess import Popen
4032-import multiprocessing
4033-from Queue import Empty
4034-from wsgiref.simple_server import make_server
4035-import json
4036-from textwrap import dedent
4037-
4038-from genshi.template import MarkupTemplate
4039-
4040-from .util import render_var
4041-
4042-
4043-tree = path.dirname(path.dirname(path.dirname(path.abspath(__file__))))
4044-if path.exists(path.join(tree, 'setup.py')):
4045- dummy_client = path.join(tree, 'dummy-client')
4046-else:
4047- dummy_client = path.join(sys.prefix, 'lib', 'dmedia', 'dummy-client')
4048-assert path.isfile(dummy_client)
4049-
4050-
4051-def read_input(environ):
4052- try:
4053- length = int(environ.get('CONTENT_LENGTH', '0'))
4054- except ValueError:
4055- return ''
4056- return environ['wsgi.input'].read(length)
4057-
4058-
4059-class ResultsApp(object):
4060- """
4061- Simple WSGI app for collecting results from JavaScript tests.
4062-
4063- REST API
4064- ========
4065-
4066- To retrieve the test HTML page (will have appropriate JavaScript):
4067-
4068- ::
4069-
4070- GET / HTTP/1.1
4071-
4072- To retrieve a JavaScript file:
4073-
4074- ::
4075-
4076- GET /scripts/foo.js HTTP/1.1
4077-
4078- To test an assertion (assertEqual, assertTrue, etc):
4079-
4080- ::
4081-
4082- POST /assert HTTP/1.1
4083- Content-Type: application/json
4084-
4085- {"method": "assertEqual", "args": ["foo", "bar"]}
4086-
4087- To report an unhandled exception:
4088-
4089- ::
4090-
4091- POST /error HTTP/1.1
4092- Content-Type: application/json
4093-
4094- "Oh no, caught an unhandled JavaScript exception!"
4095-
4096- Finally, to conclude a test:
4097-
4098- ::
4099-
4100- POST /complete HTTP/1.1
4101- """
4102- def __init__(self, q, scripts, index, mime='text/html'):
4103- self.q = q
4104- self.scripts = scripts
4105- self.index = index
4106- self.mime = mime
4107-
4108- def __call__(self, environ, start_response):
4109- method = environ['REQUEST_METHOD']
4110- if method not in ('GET', 'POST'):
4111- self.q.put(('bad_method', method))
4112- start_response('405 Method Not Allowed', [])
4113- return ''
4114- path_info = environ['PATH_INFO']
4115- if method == 'GET':
4116- if path_info == '/':
4117- headers = [
4118- ('Content-Type', self.mime),
4119- ('Content-Length', str(len(self.index))),
4120- ]
4121- self.q.put(('get', path_info))
4122- start_response('200 OK', headers)
4123- return self.index
4124- s = '/scripts/'
4125- if path_info.startswith(s):
4126- name = path_info[len(s):]
4127- if name in self.scripts:
4128- script = self.scripts[name]
4129- headers = [
4130- ('Content-Type', 'application/javascript'),
4131- ('Content-Length', str(len(script))),
4132- ]
4133- self.q.put(('get', path_info))
4134- start_response('200 OK', headers)
4135- return script
4136- self.q.put(('not_found', path_info))
4137- start_response('404 Not Found', [])
4138- return ''
4139- if method == 'POST':
4140- if path_info == '/assert':
4141- content = read_input(environ)
4142- self.q.put(('assert', content))
4143- start_response('202 Accepted', [])
4144- return ''
4145- if path_info == '/error':
4146- content = read_input(environ)
4147- self.q.put(('error', content))
4148- start_response('202 Accepted', [])
4149- return ''
4150- if path_info == '/complete':
4151- self.q.put(('complete', None))
4152- start_response('202 Accepted', [])
4153- return ''
4154- self.q.put(
4155- ('bad_request', '%(REQUEST_METHOD)s %(PATH_INFO)s' % environ)
4156- )
4157- start_response('400 Bad Request', [])
4158- return ''
4159-
4160-
4161-def results_server(q, scripts, index, mime):
4162- """
4163- Start HTTP server with `ResponseApp`.
4164-
4165- This function is the target of a ``multiprocessing.Process`` when the
4166- response server is started by `JSTestCase.start_results_server()`.
4167-
4168- :param q: a ``multiprocessing.Queue`` used to send results to main process
4169- :param scripts: a ``dict`` mapping script names to script content
4170- :param index: the HTML/XHTML to send to client
4171- :param mime: the content-type of the index page, eg ``'text/html'``
4172- """
4173- app = ResultsApp(q, scripts, index, mime)
4174- httpd = make_server('', 8000, app)
4175- httpd.serve_forever()
4176-
4177-
4178-class JavaScriptError(StandardError):
4179- pass
4180-
4181-
4182-class JavaScriptTimeout(StandardError):
4183- pass
4184-
4185-
4186-class InvalidTestMethod(StandardError):
4187- pass
4188-
4189-# unittest.TestCase methods that we allow to be called from JavaScript
4190-METHODS = (
4191- 'assertTrue',
4192- 'assertFalse',
4193-
4194- 'assertEqual',
4195- 'assertNotEqual',
4196- 'assertAlmostEqual',
4197- 'assertNotAlmostEqual',
4198-
4199- 'assertGreater',
4200- 'assertGreaterEqual',
4201- 'assertLess',
4202- 'assertLessEqual',
4203-
4204- 'assertIn',
4205- 'assertNotIn',
4206- 'assertItemsEqual',
4207-
4208- 'assertIsNone',
4209- 'assertIsNotNone',
4210-
4211- 'assertRegexpMatches',
4212- 'assertNotRegexpMatches',
4213-)
4214-
4215-
4216-class JSTestCase(TestCase):
4217- js_files = tuple()
4218- q = None
4219- server = None
4220- client = None
4221-
4222- template = """
4223- <html
4224- xmlns="http://www.w3.org/1999/xhtml"
4225- xmlns:py="http://genshi.edgewall.org/"
4226- >
4227- <head>
4228- <title py:content="title" />
4229- <script py:content="js_inline" type="text/javascript" />
4230- <script
4231- py:for="link in js_links"
4232- type="text/javascript"
4233- src="${link}"
4234- />
4235- </head>
4236- <body onload="py.run()">
4237- <div id="example" />
4238- </body>
4239- </html>
4240- """
4241-
4242- # The entire client-side test harness is only 39 lines of JavaScript!
4243- #
4244- # Note that py.data is dynamically written by JSTestCase.build_js_inline(),
4245- # and that this JavaScript will be inline in the first <script> tag in the
4246- # test HTML/XHTML page.
4247- #
4248- # The JavaScript files containing implementation and tests will follow in
4249- # the order they were defined in the JSTestClass.js_files subclass
4250- # attribute.
4251- javascript = """
4252- var py = {
4253- /* Synchronously POST results to ResultsApp */
4254- post: function(path, obj) {
4255- var request = new XMLHttpRequest();
4256- request.open('POST', path, false);
4257- if (obj) {
4258- request.setRequestHeader('Content-Type', 'application/json');
4259- request.send(JSON.stringify(obj));
4260- }
4261- else {
4262- request.send();
4263- }
4264- },
4265-
4266- /* Initialize the py.assertFoo() functions */
4267- init: function() {
4268- py.data.assertMethods.forEach(function(name) {
4269- py[name] = function() {
4270- var args = Array.prototype.slice.call(arguments);
4271- py.post('/assert', {method: name, args: args});
4272- };
4273- });
4274- },
4275-
4276- /* Run the test function indicated by py.data.methodName */
4277- run: function() {
4278- try {
4279- py.init();
4280- var className = py.data.className;
4281- var obj = py[className];
4282- if (!obj) {
4283- py.post('/error',
4284- 'Missing class ' + ['py', className].join('.')
4285- );
4286- }
4287- else {
4288- var methodName = py.data.methodName;
4289- var method = obj[methodName];
4290- if (!method) {
4291- py.post('/error',
4292- 'Missing method ' + ['py', className, methodName].join('.')
4293- );
4294- }
4295- else {
4296- method();
4297- }
4298- }
4299- }
4300- catch (e) {
4301- py.post('/error', e);
4302- }
4303- finally {
4304- py.post('/complete');
4305- }
4306- },
4307- };
4308- """
4309-
4310- @classmethod
4311- def setUpClass(cls):
4312- cls.template = dedent(cls.template).strip()
4313- cls.template_t = MarkupTemplate(cls.template)
4314- cls.javascript = dedent(cls.javascript).strip()
4315- cls.scripts = tuple(cls.load_scripts())
4316- cls.js_links = tuple(
4317- '/scripts/' + name for (name, script) in cls.scripts
4318- )
4319-
4320- @classmethod
4321- def load_scripts(cls):
4322- for filename in cls.js_files:
4323- yield (
4324- path.basename(filename),
4325- open(filename, 'rb').read()
4326- )
4327-
4328- def setUp(self):
4329- self.title = '%s.%s' % (self.__class__.__name__, self._testMethodName)
4330- self.q = multiprocessing.Queue()
4331- self.messages = []
4332-
4333- def run_js(self, **extra):
4334- index = self.build_page(**extra)
4335- self.start_results_server(dict(self.scripts), index)
4336- self.start_dummy_client()
4337- self.collect_results()
4338-
4339- def build_data(self, **extra):
4340- data = {
4341- 'className': self.__class__.__name__,
4342- 'methodName': self._testMethodName,
4343- 'assertMethods': METHODS,
4344- }
4345- data.update(extra)
4346- return data
4347-
4348- def build_js_inline(self, **extra):
4349- data = self.build_data(**extra)
4350- return '\n'.join([self.javascript, render_var('py.data', data, 4)])
4351-
4352- def render(self, **kw):
4353- return self.template_t.generate(**kw).render('xhtml', doctype='html5')
4354-
4355- def build_page(self, **extra):
4356- kw = dict(
4357- title=self.title,
4358- js_inline=self.build_js_inline(**extra),
4359- js_links=self.js_links,
4360- )
4361- return self.render(**kw)
4362-
4363- def start_results_server(self, scripts, index, mime='text/html'):
4364- self.server = multiprocessing.Process(
4365- target=results_server,
4366- args=(self.q, scripts, index, mime),
4367- )
4368- self.server.daemon = True
4369- self.server.start()
4370-
4371- def start_dummy_client(self):
4372- cmd = [dummy_client, 'http://localhost:8000/']
4373- self.client = Popen(cmd)
4374-
4375- def collect_results(self, timeout=5):
4376- while True:
4377- try:
4378- (action, data) = self.q.get(timeout=timeout)
4379- self.messages.append((action, data))
4380- except Empty:
4381- raise JavaScriptTimeout()
4382- self.assertIn(
4383- action,
4384- ['get', 'not_found', 'assert', 'error', 'complete']
4385- )
4386- # Note that no action is taken for 'get' and 'not_found'.
4387- # 'not_found' is allowed because of things like GET /favicon.ico
4388- if action == 'error':
4389- raise JavaScriptError(data)
4390- if action == 'complete':
4391- break
4392- if action == 'assert':
4393- d = json.loads(data)
4394- if d['method'] not in METHODS:
4395- raise InvalidTestMethod(data)
4396- method = getattr(self, d['method'])
4397- method(*d['args'])
4398-
4399- def tearDown(self):
4400- if self.server is not None:
4401- self.server.terminate()
4402- self.server.join()
4403- self.server = None
4404- self.q = None
4405- if self.client is not None:
4406- self.client.terminate()
4407- self.client.wait()
4408- self.client = None
4409
4410=== removed directory 'dmedia/webui/tests'
4411=== removed file 'dmedia/webui/tests/__init__.py'
4412--- dmedia/webui/tests/__init__.py 2011-03-26 06:53:57 +0000
4413+++ dmedia/webui/tests/__init__.py 1970-01-01 00:00:00 +0000
4414@@ -1,24 +0,0 @@
4415-# Authors:
4416-# Jason Gerard DeRose <jderose@novacut.com>
4417-#
4418-# dmedia: distributed media library
4419-# Copyright (C) 2011 Jason Gerard DeRose <jderose@novacut.com>
4420-#
4421-# This file is part of `dmedia`.
4422-#
4423-# `dmedia` is free software: you can redistribute it and/or modify it under the
4424-# terms of the GNU Affero General Public License as published by the Free
4425-# Software Foundation, either version 3 of the License, or (at your option) any
4426-# later version.
4427-#
4428-# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY
4429-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
4430-# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
4431-# details.
4432-#
4433-# You should have received a copy of the GNU Affero General Public License along
4434-# with `dmedia`. If not, see <http://www.gnu.org/licenses/>.
4435-
4436-"""
4437-Unit tests for the `dmedia.webui` package.
4438-"""
4439
4440=== removed file 'dmedia/webui/tests/test_basejs.py'
4441--- dmedia/webui/tests/test_basejs.py 2011-04-17 22:44:52 +0000
4442+++ dmedia/webui/tests/test_basejs.py 1970-01-01 00:00:00 +0000
4443@@ -1,69 +0,0 @@
4444-# Authors:
4445-# Jason Gerard DeRose <jderose@novacut.com>
4446-#
4447-# dmedia: distributed media library
4448-# Copyright (C) 2011 Jason Gerard DeRose <jderose@novacut.com>
4449-#
4450-# This file is part of `dmedia`.
4451-#
4452-# `dmedia` is free software: you can redistribute it and/or modify it under the
4453-# terms of the GNU Affero General Public License as published by the Free
4454-# Software Foundation, either version 3 of the License, or (at your option) any
4455-# later version.
4456-#
4457-# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY
4458-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
4459-# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
4460-# details.
4461-#
4462-# You should have received a copy of the GNU Affero General Public License along
4463-# with `dmedia`. If not, see <http://www.gnu.org/licenses/>.
4464-
4465-"""
4466-Test the base.js JavaScript.
4467-"""
4468-
4469-from dmedia.webui.js import JSTestCase
4470-from dmedia.webui.util import datafile
4471-
4472-class TestFunctions(JSTestCase):
4473- js_files = (
4474- datafile('base.js'),
4475- datafile('test_base.js'),
4476- )
4477-
4478- def test_dollar(self):
4479- """
4480- Test the $() JavaScript function.
4481- """
4482- self.run_js()
4483-
4484- def test_dollar_el(self):
4485- """
4486- Test the $el() JavaScript function.
4487- """
4488- self.run_js()
4489-
4490- def test_dollar_replace(self):
4491- """
4492- Test the $replace() JavaScript function.
4493- """
4494- self.run_js()
4495-
4496- def test_dollar_hide(self):
4497- """
4498- Test the $hide() JavaScript function.
4499- """
4500- self.run_js()
4501-
4502- def test_dollar_show(self):
4503- """
4504- Test the $show() JavaScript function.
4505- """
4506- self.run_js()
4507-
4508- def test_minsec(self):
4509- self.run_js()
4510-
4511- def test_todata(self):
4512- self.run_js()
4513
4514=== removed file 'dmedia/webui/tests/test_browserjs.py'
4515--- dmedia/webui/tests/test_browserjs.py 2011-04-13 05:03:16 +0000
4516+++ dmedia/webui/tests/test_browserjs.py 1970-01-01 00:00:00 +0000
4517@@ -1,38 +0,0 @@
4518-# Authors:
4519-# Jason Gerard DeRose <jderose@novacut.com>
4520-#
4521-# dmedia: distributed media library
4522-# Copyright (C) 2011 Jason Gerard DeRose <jderose@novacut.com>
4523-#
4524-# This file is part of `dmedia`.
4525-#
4526-# `dmedia` is free software: you can redistribute it and/or modify it under the
4527-# terms of the GNU Affero General Public License as published by the Free
4528-# Software Foundation, either version 3 of the License, or (at your option) any
4529-# later version.
4530-#
4531-# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY
4532-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
4533-# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
4534-# details.
4535-#
4536-# You should have received a copy of the GNU Affero General Public License along
4537-# with `dmedia`. If not, see <http://www.gnu.org/licenses/>.
4538-
4539-"""
4540-Test the browser.js JavaScript.
4541-"""
4542-
4543-from dmedia.webui.js import JSTestCase
4544-from dmedia.webui.util import datafile
4545-
4546-class TestBrowser(JSTestCase):
4547- js_files = (
4548- datafile('couch.js'),
4549- datafile('base.js'),
4550- datafile('browser.js'),
4551- datafile('test_browser.js'),
4552- )
4553-
4554- def test_init(self):
4555- self.run_js()
4556
4557=== removed file 'dmedia/webui/tests/test_couchjs.py'
4558--- dmedia/webui/tests/test_couchjs.py 2011-04-18 02:57:38 +0000
4559+++ dmedia/webui/tests/test_couchjs.py 1970-01-01 00:00:00 +0000
4560@@ -1,97 +0,0 @@
4561-# Authors:
4562-# Jason Gerard DeRose <jderose@novacut.com>
4563-#
4564-# dmedia: distributed media library
4565-# Copyright (C) 2011 Jason Gerard DeRose <jderose@novacut.com>
4566-#
4567-# This file is part of `dmedia`.
4568-#
4569-# `dmedia` is free software: you can redistribute it and/or modify it under the
4570-# terms of the GNU Affero General Public License as published by the Free
4571-# Software Foundation, either version 3 of the License, or (at your option) any
4572-# later version.
4573-#
4574-# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY
4575-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
4576-# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
4577-# details.
4578-#
4579-# You should have received a copy of the GNU Affero General Public License along
4580-# with `dmedia`. If not, see <http://www.gnu.org/licenses/>.
4581-
4582-"""
4583-Test the couch.js JavaScript.
4584-"""
4585-
4586-from dmedia.webui.js import JSTestCase
4587-from dmedia.webui.util import datafile
4588-
4589-
4590-class TestCouchRequest(JSTestCase):
4591- js_files = (
4592- datafile('couch.js'),
4593- datafile('test_couch.js'),
4594- )
4595-
4596- def test_request(self):
4597- self.run_js()
4598-
4599- def test_async_request(self):
4600- self.run_js()
4601-
4602-
4603-class TestCouchBase(JSTestCase):
4604- js_files = (
4605- datafile('couch.js'),
4606- datafile('test_couch.js'),
4607- )
4608-
4609- def test_init(self):
4610- self.run_js()
4611-
4612- def test_path(self):
4613- self.run_js()
4614-
4615- def test_request(self):
4616- self.run_js()
4617-
4618- def test_async_request(self):
4619- self.run_js()
4620-
4621- def test_post(self):
4622- self.run_js()
4623-
4624- def test_put(self):
4625- self.run_js()
4626-
4627- def test_get(self):
4628- self.run_js()
4629-
4630- def test_delete(self):
4631- self.run_js()
4632-
4633-
4634-class TestServer(JSTestCase):
4635- js_files = (
4636- datafile('couch.js'),
4637- datafile('test_couch.js'),
4638- )
4639-
4640- def test_database(self):
4641- self.run_js()
4642-
4643-
4644-class TestDatabase(JSTestCase):
4645- js_files = (
4646- datafile('couch.js'),
4647- datafile('test_couch.js'),
4648- )
4649-
4650- def test_save(self):
4651- self.run_js()
4652-
4653- def test_bulksave(self):
4654- self.run_js()
4655-
4656- def test_view(self):
4657- self.run_js()
4658
4659=== removed file 'dmedia/webui/tests/test_js.py'
4660--- dmedia/webui/tests/test_js.py 2011-04-17 22:04:38 +0000
4661+++ dmedia/webui/tests/test_js.py 1970-01-01 00:00:00 +0000
4662@@ -1,491 +0,0 @@
4663-# Authors:
4664-# Jason Gerard DeRose <jderose@novacut.com>
4665-#
4666-# dmedia: distributed media library
4667-# Copyright (C) 2010 Jason Gerard DeRose <jderose@novacut.com>
4668-#
4669-# This file is part of `dmedia`.
4670-#
4671-# `dmedia` is free software: you can redistribute it and/or modify it under the
4672-# terms of the GNU Affero General Public License as published by the Free
4673-# Software Foundation, either version 3 of the License, or (at your option) any
4674-# later version.
4675-#
4676-# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY
4677-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
4678-# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
4679-# details.
4680-#
4681-# You should have received a copy of the GNU Affero General Public License along
4682-# with `dmedia`. If not, see <http://www.gnu.org/licenses/>.
4683-
4684-"""
4685-Unit tests for `base.js` module.
4686-"""
4687-
4688-from unittest import TestCase
4689-import json
4690-import subprocess
4691-import time
4692-import multiprocessing
4693-import multiprocessing.queues
4694-
4695-from dmedia.webui import js
4696-from dmedia.webui.util import datafile, load_data
4697-from dmedia.tests.helpers import DummyQueue, raises
4698-
4699-
4700-class StartResponse(object):
4701- status = None
4702- headers = None
4703-
4704- def __call__(self, status, headers):
4705- assert self.status is None
4706- assert self.headers is None
4707- self.status = status
4708- self.headers = headers
4709-
4710-
4711-class Input(object):
4712- def __init__(self, content):
4713- self.content = content
4714-
4715- def read(self, length):
4716- return self.content
4717-
4718-
4719-class test_ResultsApp(TestCase):
4720- klass = js.ResultsApp
4721-
4722- def test_init(self):
4723- q = DummyQueue()
4724- scripts = {
4725- 'mootools.js': 'here be mootools',
4726- 'base.js': 'here be dmedia',
4727- }
4728- index = 'foo'
4729- inst = self.klass(q, scripts, index)
4730- self.assertTrue(inst.q is q)
4731- self.assertTrue(inst.scripts is scripts)
4732- self.assertTrue(inst.index is index)
4733- self.assertEqual(inst.mime, 'text/html')
4734-
4735- inst = self.klass(q, scripts, index, mime='application/xhtml+xml')
4736- self.assertTrue(inst.q is q)
4737- self.assertTrue(inst.scripts is scripts)
4738- self.assertTrue(inst.index is index)
4739- self.assertEqual(inst.mime, 'application/xhtml+xml')
4740-
4741- def test_call(self):
4742- q = DummyQueue()
4743- scripts = {
4744- 'mootools.js': 'here be mootools',
4745- 'base.js': 'here be dmedia',
4746- }
4747- index = 'foo bar'
4748- inst = self.klass(q, scripts, index)
4749-
4750- env = {
4751- 'REQUEST_METHOD': 'GET',
4752- 'PATH_INFO': '/',
4753- }
4754- sr = StartResponse()
4755- self.assertEqual(inst(env, sr), 'foo bar')
4756- self.assertEqual(sr.status, '200 OK')
4757- self.assertEqual(
4758- sr.headers,
4759- [
4760- ('Content-Type', 'text/html'),
4761- ('Content-Length', '7'),
4762- ]
4763- )
4764- self.assertEqual(q.messages, [('get', '/')])
4765-
4766- post1 = json.dumps({'args': ('one', 'two'), 'method': 'assertEqual'})
4767- env = {
4768- 'REQUEST_METHOD': 'POST',
4769- 'PATH_INFO': '/assert',
4770- 'wsgi.input': Input(post1),
4771- }
4772- sr = StartResponse()
4773- self.assertEqual(inst(env, sr), '')
4774- self.assertEqual(sr.status, '202 Accepted')
4775- self.assertEqual(sr.headers, [])
4776- self.assertEqual(
4777- q.messages,
4778- [
4779- ('get', '/'),
4780- ('assert', post1),
4781- ]
4782- )
4783-
4784- post2 = 'oh no, it no worky!'
4785- env = {
4786- 'REQUEST_METHOD': 'POST',
4787- 'PATH_INFO': '/error',
4788- 'wsgi.input': Input(post2),
4789- }
4790- sr = StartResponse()
4791- self.assertEqual(inst(env, sr), '')
4792- self.assertEqual(sr.status, '202 Accepted')
4793- self.assertEqual(sr.headers, [])
4794- self.assertEqual(
4795- q.messages,
4796- [
4797- ('get', '/'),
4798- ('assert', post1),
4799- ('error', post2),
4800- ]
4801- )
4802-
4803- env = {
4804- 'REQUEST_METHOD': 'POST',
4805- 'PATH_INFO': '/complete',
4806- }
4807- sr = StartResponse()
4808- self.assertEqual(inst(env, sr), '')
4809- self.assertEqual(sr.status, '202 Accepted')
4810- self.assertEqual(sr.headers, [])
4811- self.assertEqual(
4812- q.messages,
4813- [
4814- ('get', '/'),
4815- ('assert', post1),
4816- ('error', post2),
4817- ('complete', None),
4818- ]
4819- )
4820-
4821- # Test with bad requests
4822- q = DummyQueue()
4823- index = 'foo bar'
4824- inst = self.klass(q, scripts, index)
4825- env = {'REQUEST_METHOD': 'PUT'}
4826- sr = StartResponse()
4827- self.assertEqual(inst(env, sr), '')
4828- self.assertEqual(sr.status, '405 Method Not Allowed')
4829- self.assertEqual(sr.headers, [])
4830- self.assertEqual(q.messages, [('bad_method', 'PUT')])
4831-
4832- env = {
4833- 'REQUEST_METHOD': 'GET',
4834- 'PATH_INFO': '/error',
4835- }
4836- sr = StartResponse()
4837- self.assertEqual(inst(env, sr), '')
4838- self.assertEqual(sr.status, '404 Not Found')
4839- self.assertEqual(sr.headers, [])
4840- self.assertEqual(
4841- q.messages,
4842- [
4843- ('bad_method', 'PUT'),
4844- ('not_found', '/error'),
4845- ]
4846- )
4847-
4848- env = {
4849- 'REQUEST_METHOD': 'POST',
4850- 'PATH_INFO': '/nope',
4851- }
4852- sr = StartResponse()
4853- self.assertEqual(inst(env, sr), '')
4854- self.assertEqual(sr.status, '400 Bad Request')
4855- self.assertEqual(sr.headers, [])
4856- self.assertEqual(
4857- q.messages,
4858- [
4859- ('bad_method', 'PUT'),
4860- ('not_found', '/error'),
4861- ('bad_request', 'POST /nope'),
4862- ]
4863- )
4864-
4865- # Test script reqests
4866- q = DummyQueue()
4867- index = 'foo bar'
4868- inst = self.klass(q, scripts, index)
4869-
4870- # /scripts/mootools.js
4871- env = {
4872- 'REQUEST_METHOD': 'GET',
4873- 'PATH_INFO': '/scripts/mootools.js',
4874- }
4875- sr = StartResponse()
4876- self.assertEqual(inst(env, sr), 'here be mootools')
4877- self.assertEqual(sr.status, '200 OK')
4878- self.assertEqual(
4879- sr.headers,
4880- [
4881- ('Content-Type', 'application/javascript'),
4882- ('Content-Length', '16'),
4883- ]
4884- )
4885- self.assertEqual(inst.q.messages, [('get', '/scripts/mootools.js')])
4886-
4887- # /scripts/base.js
4888- env = {
4889- 'REQUEST_METHOD': 'GET',
4890- 'PATH_INFO': '/scripts/base.js',
4891- }
4892- sr = StartResponse()
4893- self.assertEqual(inst(env, sr), 'here be dmedia')
4894- self.assertEqual(sr.status, '200 OK')
4895- self.assertEqual(
4896- sr.headers,
4897- [
4898- ('Content-Type', 'application/javascript'),
4899- ('Content-Length', '14'),
4900- ]
4901- )
4902- self.assertEqual(
4903- inst.q.messages,
4904- [
4905- ('get', '/scripts/mootools.js'),
4906- ('get', '/scripts/base.js'),
4907- ]
4908- )
4909-
4910- # /scripts/foo.js
4911- env = {
4912- 'REQUEST_METHOD': 'GET',
4913- 'PATH_INFO': '/scripts/foo.js',
4914- }
4915- sr = StartResponse()
4916- self.assertEqual(inst(env, sr), '')
4917- self.assertEqual(sr.status, '404 Not Found')
4918- self.assertEqual(sr.headers, [])
4919- self.assertEqual(
4920- inst.q.messages,
4921- [
4922- ('get', '/scripts/mootools.js'),
4923- ('get', '/scripts/base.js'),
4924- ('not_found', '/scripts/foo.js'),
4925- ]
4926- )
4927-
4928- # /mootools.js
4929- env = {
4930- 'REQUEST_METHOD': 'GET',
4931- 'PATH_INFO': '/mootools.js',
4932- }
4933- sr = StartResponse()
4934- self.assertEqual(inst(env, sr), '')
4935- self.assertEqual(sr.status, '404 Not Found')
4936- self.assertEqual(sr.headers, [])
4937- self.assertEqual(
4938- inst.q.messages,
4939- [
4940- ('get', '/scripts/mootools.js'),
4941- ('get', '/scripts/base.js'),
4942- ('not_found', '/scripts/foo.js'),
4943- ('not_found', '/mootools.js'),
4944- ]
4945- )
4946-
4947-
4948-expected = """
4949-<!DOCTYPE html>
4950-<html xmlns="http://www.w3.org/1999/xhtml">
4951-<head>
4952-<title>Hello Naughty Nurse!</title>
4953-<script type="text/javascript">var foo = "bar";</script>
4954-<script type="text/javascript" src="/scripts/base.js"></script>
4955-</head>
4956-<body onload="py.run()">
4957-<div id="example"></div>
4958-</body>
4959-</html>
4960-""".strip()
4961-
4962-
4963-class test_JSTestCase(js.JSTestCase):
4964-
4965- def test_load_scripts(self):
4966- klass = self.__class__
4967- self.assertEqual(list(klass.load_scripts()), [])
4968- klass.js_files = (
4969- datafile('browser.js'),
4970- datafile('base.js'),
4971- )
4972- self.assertEqual(
4973- list(klass.load_scripts()),
4974- [
4975- ('browser.js', load_data(datafile('browser.js'))),
4976- ('base.js', load_data(datafile('base.js'))),
4977- ]
4978- )
4979-
4980- def test_start_results_server(self):
4981- self.assertEqual(
4982- self.title, 'test_JSTestCase.test_start_results_server'
4983- )
4984- self.start_results_server({}, 'foo bar')
4985- self.assertTrue(isinstance(self.q, multiprocessing.queues.Queue))
4986- self.assertTrue(isinstance(self.server, multiprocessing.Process))
4987- time.sleep(1)
4988- self.assertTrue(self.server.daemon)
4989- self.assertTrue(self.server.is_alive())
4990- self.assertEqual(
4991- self.server._args,
4992- (self.q, {}, 'foo bar', 'text/html')
4993- )
4994- self.assertEqual(self.server._kwargs, {})
4995- self.server.terminate()
4996- self.server.join()
4997-
4998- def test_start_dummy_client(self):
4999- self.assertEqual(
5000- self.title, 'test_JSTestCase.test_start_dummy_client'
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches