Andy Mikhailenko avatar Andy Mikhailenko committed aca42b2

Upgraded extension "mobile" to the new Tool API. Added documentation. Probably some bugfixes. (Bulk commit, unsure about dates.)

Comments (0)

Files changed (3)

orgtool/ext/mobile/__init__.py

 
 .. _python-gammu: http://wammu.eu/python-gammu/
 
+Configuration
+-------------
+
 Settings example (in YAML)::
 
-    bundles:
-        orgtool.ext.gammu:
+    extensions:
+        orgtool.ext.mobile.MobileETL:
             my_number: "+1234567890"
 
 .. note::
     not be fixed because the exact order seems to depend on the phone model. We
     just omit the folder.
 
-Adds commands:
+Commands
+--------
 
-* import-mobile-sms
-* import-mobile-contacts
-* import-mobile-plans
+This extension provides following commands within namespace "mobile":
 
+* import-sms
+* import-contacts
+* import-plans
+
+API reference
+-------------
 """
 from tool.dist import check_dependencies
+from tool.plugins import features, requires #, BasePlugin
 
 check_dependencies(__name__)
 
-import commands
+from commands import (
+    import_contacts, import_sms, import_plans,
+)
+
+
+@features('mobile')
+@requires('{document_storage}')
+def setup(app, conf):
+    "Tool extension for importing data from mobile phones."
+    assert isinstance(conf.get('my_numbers'), dict)
+    commands = (import_contacts, import_sms, import_plans)
+    app.cli_parser.add_commands(commands, namespace='mobile')
+    return conf
+
+'''
+class MobileETL(BasePlugin):
+    "Tool extension for importing data from mobile phones."
+    features = 'mobile'
+    requires = ('{document_storage}',)
+    commands = (import_contacts, import_sms, import_plans)
+
+    def make_env(self, my_numbers):
+        assert isinstance(my_numbers, dict)
+        return {'my_numbers': my_numbers}
+'''

orgtool/ext/mobile/commands.py

 import datetime
 import gammu      # XXX cannot be installed via pip, only as part of gammu dist
 
-from tool import cli
-from tool import context
-from tool.ext.documents import db
+from tool.cli import arg, CommandError
+from tool import app
 
 from schema import GammuSMS, GammuContact, GammuPlan
 
 
-def _get_my_number():
+def _get_my_numbers():
     bundle_name = '.'.join(__name__.split('.')[:-1])
-    conf = context.app.get_settings_for_bundle(bundle_name)
-    my_number = conf.get('my_number')
-    assert my_number, 'Gammu integration requires "my_number" setting'
-    if isinstance(my_number, int):
-        return u'+{0}'.format(my_number)
-    return unicode(my_number)
+    my_numbers = app.get_feature('mobile').get('my_numbers')
+    assert my_numbers, ('Gammu integration requires "my_numbers" setting. '
+                        'It must be a dictionary like {"work": "123"}.')
+    assert isinstance(my_numbers, dict)
+    def _normalize(value):
+        if isinstance(value, int):
+            return u'+{0}'.format(value)
+        return unicode(value)
+    return dict((k,_normalize(v)) for k,v in my_numbers.iteritems())
 
-
-def _import_one_sms(data):
+def _import_one_sms(data, current_number=None, dry_run=False):
     # define who's the actor and who's the receiver
-    my_number = _get_my_number()
     other_number = unicode(data['Number'])
     if data['State'] == 'Sent':
-        sent_by, sent_to = my_number, other_number
+        sent_by, sent_to = current_number, other_number
     else:
-        sent_by, sent_to = other_number, my_number
+        sent_by, sent_to = other_number, current_number
 
     fields = dict(
         sent_by = sent_by,
         sent_to = sent_to,
         date_time = data['DateTime'],
         #is_confirmed = True,  # it is sent, yup
-        summary = data['Text'],
+        summary = data['Text'] or u'[text missing]',  # u'' is invalid, None not accepted
     )
-    search_fields = dict(fields).pop('summary')
+    search_fields = dict(fields)
+    search_fields.pop('summary')
 
-    if not GammuSMS.objects(db).where(**fields).count():
-        print 'SAVING', sent_by, '->', sent_to, fields['date_time'], fields['summary'][:20]
-        return GammuSMS(**fields).save(db)
+    # workaround: we don't know which number was "current" when the message was
+    # last imported, so we look for any "our" number.
+    my_numbers = _get_my_numbers()
+    k = 'sent_by' if data['State']=='Sent' else 'sent_to'
+    search_fields.pop(k)
+    search_fields.update({'{0}__in'.format(k): my_numbers.values()})
+
+    db = app.get_feature('document_storage').default_db
+    if not GammuSMS.objects(db).where(**search_fields).count():
+        #print 'NOT FOUND:', search_fields
+        print u'SAVING {0} {1} → {2} {3}{4}'.format(
+            fields['date_time'], sent_by, sent_to, fields['summary'][:20],
+            u'…' if 20 < len(fields['summary']) else '')
+        if dry_run:
+            return '(stub: dry run)'
+        else:
+            return GammuSMS(**fields).save(db)
 
 def _get_state_machine():
     print 'Connecting to the phone...'
 
     return sm
 
-@cli.command()
-def import_mobile_contacts():
+def import_contacts():
+    db = app.get_feature('document_storage').default_db
     sm = _get_state_machine()
     memory = 'ME'    # TODO: allow importing from SIM memory, too
 
 
     print 'Imported {saved_cnt} of {seen_cnt}.'.format(**locals())
 
-@cli.command()
-def import_mobile_sms():
+@arg('-p', '--current-phone')
+@arg('-f', '--full-archive', default=False, help='scan the whole SMS archive')
+@arg('-d', '--dry-run', default=False, help='do not save imported messages')
+def import_sms(args):
+    """Imports SMS from mobile phone.
+
+    :param current_phone:
+        Expects "current phone" so that incoming and outgoing messages can be
+        correctly populated with both sender and receiver numbers (the phone only
+        stores the "other" number) and you have to manually specify yours). Can be
+        omitted if there's only one "my number" in the settings.
+
+        Note that you should specify the label (e.g. "personal", "work" or
+        "primary") instead of the number itself. The labels are defined in the
+        "my_numbers" setting for the bundle::
+
+            extensions:
+                orgtool.ext.mobile.MobileETL:
+                    my_numbers:
+                        home: +1234567890
+                        work: +0987654321
+
+    :param full_archive:
+        If True, attempts to import all messages in the phone. Despite this
+        implies checking for duplicates, the process takes longer and issues
+        with dates and phone numbers may arise. By default this option is off
+        and only "new" messages are imported. Message is considered "new" if
+        its date is greater than the last known message's date. Time is
+        ignored in this check.
+
+    :param dry_run:
+        If True, newly imported messages are not saved. Use this for testing.
+        Default is False.
+
+    """
+    if args.dry_run:
+        yield 'Dry run, no data will be changed.'
+
+    db = app.get_feature('document_storage').default_db
+    my_numbers = _get_my_numbers()
+    current_number = None
+    if args.current_phone:
+        assert args.current_phone in my_numbers, (
+            'unknown number label "{0}"'.format(args.current_phone))
+        current_number = my_numbers[args.current_phone]
+    else:
+        if len(my_numbers) != 1:
+            raise CommandError('Which phone (SIM card) is that? Choices: '
+                               '{0}'.format(', '.join(my_numbers)))
+        the_only_label = my_numbers.keys()[0]
+        current_number = my_numbers[the_only_label]
+    assert current_number
+
+    # find latest known message date
+    if args.full_archive:
+        last_imported_date = None
+        yield 'Importing all messages from the phone...'
+    else:
+        msgs = GammuSMS.objects(db).order_by('date_time', reverse=True)
+        last_imported_date = msgs[0].date_time.date() if msgs.count() else None
+        yield 'Importing messages since {0}...'.format(last_imported_date)
+
     sm = _get_state_machine()
 
     seen_cnt = saved_cnt = 0
     for data in _iterate_results(sm.GetNextSMS, Folder=0):
-        saved = _import_one_sms(data)
+        if last_imported_date and not args.full_archive:
+            # skip message without full check if a later message had been
+            # already imported
+            if data['DateTime'].date() < last_imported_date:
+                continue
+        saved = _import_one_sms(data, current_number, dry_run=args.dry_run)
         if saved:
             saved_cnt += 1
         seen_cnt += 1
 
-    print 'Imported {saved_cnt} of {seen_cnt}.'.format(**locals())
+    yield 'Imported {saved_cnt} of {seen_cnt}.'.format(**locals())
+    if args.dry_run:
+        yield '(Dry run, nothing really changed.)'
 
     # TODO: check msg['UDH'] -- it contains info on concatenated msgs:
     #
     #          'PartNumber': 2,
     #          'Type': 'ConcatenatedMessages'}
 
-@cli.command()
-def import_mobile_plans():
+def import_plans():
     sm = _get_state_machine()
 
     for data in _iterate_results(sm.GetNextCalendar):

orgtool/ext/mobile/schema.py

 import datetime
-from docu import Field as f
+from doqu import Field as f
 from orgtool.ext.talks import Message
-from orgtool.ext.contacts import Contact, Actor
+from orgtool.ext.actors.schema import Actor
+from orgtool.ext.contacts import Contact
 from orgtool.ext.events import Plan
 
 
 class GammuSMS(Message):
     source = f(unicode, choices=[SMS_SOURCE], default=SMS_SOURCE)
 
+    defaults = {
+        'summary': u'No text',
+    }
+
 
 class GammuActor(Actor):
     source = f(unicode, choices=[ACTOR_SOURCE], default=ACTOR_SOURCE)
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.