Commits

Lars Yencken committed 2ef453a Merge

Merged in richo/doko/features/hax (pull request #1: Compatibility shims)

  • Participants
  • Parent commits 2e1f74f, 00f6b4b

Comments (0)

Files changed (5)

 
 Doko is a Python clone of Victor Jalencas's `whereami <https://github.com/victor/whereami>`_ utility. It requires OS X 10.6 (Mountain Lion) or later to function.
 
-Kudos to `Rich Healey <https://github.com/richo/>`_ for getting me started.
+Kudos to `Richo Healey <https://github.com/richo/>`_ for getting me started.
 
 Installing
 ----------
 
 On OS X 10.6 (Mountain Lion) or later, you can enable Core Location in System Preferences, in the "Security" or "Security & Privacy" section.
 
+Hacking
+-------
+
+For hacking on OSX, you will likely want to install ``requires-corelocation.txt`` as well as ``requires.txt``.
+
 Using on the command-line
 -------------------------
 
 Use the Core Location framework.
 """
 
+import os
 import sys
 import optparse
+from optparse import OptionValueError
 import time
 from collections import namedtuple
+from collections import OrderedDict
 import webbrowser
 
-import CoreLocation
+try:
+    import CoreLocation
+except ImportError:
+    # CoreLocation attempts will fail.
+    CoreLocation = None
+
 import requests
 import BeautifulSoup
 
-Location = namedtuple('Location', 'latitude longitude')
+class Location(namedtuple('Location', 'latitude longitude')):
+    precision = None
+    @classmethod
+    def set_precision(klass, digits):
+        klass.precision = digits
+
+    def safe_value(self, value):
+        if self.precision:
+            return round(value, self.precision)
+        else:
+            return value
+
+    def safe_longitude(self):
+        return self.safe_value(self.longitude)
+
+    def safe_latitude(self):
+        return self.safe_value(self.latitude)
+
+    def __repr__(self):
+        return "%s,%s" % (self.safe_latitude(), self.safe_longitude())
 
 DEFAULT_TIMEOUT = 3
 DEFAULT_RETRIES = 10
 
+LOCATION_STRATEGIES = OrderedDict()
+
+# Important, define strategies in default resolution order
+def location_strategy(name):
+    def _(fn):
+        LOCATION_STRATEGIES[name] = fn
+    return _
 
 class LocationServiceException(Exception):
     pass
 
 
-def location(timeout=DEFAULT_TIMEOUT):
-    """
-    Fetch and return a Location from OS X Core Location, or throw
-    a LocationServiceException trying.
-    """
-    m = CoreLocation.CLLocationManager.new()
-
-    if not m.locationServicesEnabled():
-        raise LocationServiceException(
-                'location services not enabled -- check privacy settings in System Preferences'  # noqa
-            )
+if CoreLocation:
+    @location_strategy("corelocation")
+    def corelocation_location(timeout=DEFAULT_TIMEOUT):
+        """
+        Fetch and return a Location from OS X Core Location, or throw
+        a LocationServiceException trying.
+        """
 
-    if not m.locationServicesAvailable():
-        raise LocationServiceException('location services not available')
+        m = CoreLocation.CLLocationManager.new()
 
-    m.startUpdatingLocation()
-    CoreLocation.CFRunLoopStop(CoreLocation.CFRunLoopGetCurrent())
-    l = m.location()
+        if not m.locationServicesEnabled():
+            raise LocationServiceException(
+                    'location services not enabled -- check privacy settings in System Preferences'  # noqa
+                )
 
-    # retry up to ten times, possibly sleeping between tries
-    for i in xrange(DEFAULT_RETRIES):
-        if l:
-            break
+        if not m.locationServicesAvailable():
+            raise LocationServiceException('location services not available')
 
-        time.sleep(float(timeout) / DEFAULT_RETRIES)
+        m.startUpdatingLocation()
         CoreLocation.CFRunLoopStop(CoreLocation.CFRunLoopGetCurrent())
         l = m.location()
 
-    if not l:
-        raise LocationServiceException(
-                'location could not be found -- is wifi enabled?'
-            )
+        # retry up to ten times, possibly sleeping between tries
+        for i in xrange(DEFAULT_RETRIES):
+            if l:
+                break
 
-    c = l.coordinate()
-    return Location(c.latitude, c.longitude)
+            time.sleep(float(timeout) / DEFAULT_RETRIES)
+            CoreLocation.CFRunLoopStop(CoreLocation.CFRunLoopGetCurrent())
+            l = m.location()
 
+        if not l:
+            raise LocationServiceException(
+                    'location could not be found -- is wifi enabled?'
+                )
 
-def geobytes_location():
+        c = l.coordinate()
+        return Location(c.latitude, c.longitude)
+
+@location_strategy("geoip")
+def geobytes_location(timeout=DEFAULT_TIMEOUT):
     external_ip = requests.get('http://jsonip.com/').json['ip']
-    resp = requests.post(
-            'http://www.geobytes.com/iplocator.htm?getlocation',
-            data={'ipaddress': external_ip},
-        )
+    try:
+        resp = requests.post(
+                'http://www.geobytes.com/iplocator.htm?getlocation',
+                data={'ipaddress': external_ip},
+                timeout=timeout,
+            )
+    except requests.exceptions.Timeout:
+        raise LocationServiceException('timeout fetching geoip location')
     try:
         s = BeautifulSoup.BeautifulSoup(resp.content)
         latitude = float(s.find('td',
             help='Suppress any error messages.')
     parser.add_option('--show', action='store_true',
             help='Show result on Google Maps in a browser.')
-    parser.add_option('--approx', action='store_true',
-            help='Use a GeoIP service if Core Location fails.')
+    parser.add_option('-f', '--force', action='store_true', dest='force',
+            help='Continue trying strategies if the first should fail')
+    parser.add_option('--strategy', action='store', dest='strategy',
+            help='Strategy for location lookup (corelocation|geoip)', default=LOCATION_STRATEGIES.keys()[0])
+    parser.add_option('--precision', action='store', dest='precision', type=int,
+            help='Store geodata with <precision> significant digits', default=None)
 
     return parser
 
         parser.print_help()
         sys.exit(1)
 
+    if options.strategy not in LOCATION_STRATEGIES:
+        raise OptionValueError("%s is not a valid strategy" % options.strategy)
+
+    if os.getenv("DOKO_PRECISION"):
+        try:
+            Location.set_precision(os.getenv("DOKO_PRECISION"))
+        except ValueError:
+            raise "Invalid value in DOKO_PRECISION"
+
+    if options.precision:
+        Location.set_precision(options.precision)
+
     l = None
     error = None
+
+    strategy = LOCATION_STRATEGIES.pop(options.strategy)
+
     try:
-        l = location(options.timeout)
+        l = strategy(options.timeout)
     except LocationServiceException, e:
         error = e.message
 
-    if not l and options.approx:
-        try:
-            l = geobytes_location()
-        except LocationServiceException, e:
-            error = e.message
+    if not l and options.force:
+        for _, strategy in LOCATION_STRATEGIES:
+            try:
+                l = geobytes_location()
+            except LocationServiceException, e:
+                error = e.message
 
     if not l:
         if not options.quiet:
             print >> sys.stderr, error
         sys.exit(1)
 
-    print ' '.join(map(str, l))
+    print(repr(l))
 
     if options.show:
         webbrowser.open(

requirements-osx.txt

+pyobjc==2.4
+pyobjc-core==2.4
+pyobjc-framework-Accounts==2.4
+pyobjc-framework-AddressBook==2.4
+pyobjc-framework-AppleScriptKit==2.4
+pyobjc-framework-AppleScriptObjC==2.4
+pyobjc-framework-Automator==2.4
+pyobjc-framework-CFNetwork==2.4
+pyobjc-framework-CalendarStore==2.4
+pyobjc-framework-Cocoa==2.4
+pyobjc-framework-Collaboration==2.4
+pyobjc-framework-CoreData==2.4
+pyobjc-framework-CoreLocation==2.4
+pyobjc-framework-CoreText==2.4
+pyobjc-framework-DictionaryServices==2.4
+pyobjc-framework-EventKit==2.4
+pyobjc-framework-ExceptionHandling==2.4
+pyobjc-framework-FSEvents==2.4
+pyobjc-framework-InputMethodKit==2.4
+pyobjc-framework-InstallerPlugins==2.4
+pyobjc-framework-InstantMessage==2.4
+pyobjc-framework-LatentSemanticMapping==2.4
+pyobjc-framework-LaunchServices==2.4
+pyobjc-framework-Message==2.4
+pyobjc-framework-PreferencePanes==2.4
+pyobjc-framework-PubSub==2.4
+pyobjc-framework-QTKit==2.4
+pyobjc-framework-Quartz==2.4
+pyobjc-framework-ScreenSaver==2.4
+pyobjc-framework-ScriptingBridge==2.4
+pyobjc-framework-SearchKit==2.4
+pyobjc-framework-ServerNotification==2.4
+pyobjc-framework-ServiceManagement==2.4
+pyobjc-framework-Social==2.4
+pyobjc-framework-SyncServices==2.4
+pyobjc-framework-SystemConfiguration==2.4
+pyobjc-framework-WebKit==2.4
-pyobjc==2.4
-pyobjc-core==2.4
-pyobjc-framework-Accounts==2.4
-pyobjc-framework-AddressBook==2.4
-pyobjc-framework-AppleScriptKit==2.4
-pyobjc-framework-AppleScriptObjC==2.4
-pyobjc-framework-Automator==2.4
-pyobjc-framework-CFNetwork==2.4
-pyobjc-framework-CalendarStore==2.4
-pyobjc-framework-Cocoa==2.4
-pyobjc-framework-Collaboration==2.4
-pyobjc-framework-CoreData==2.4
-pyobjc-framework-CoreLocation==2.4
-pyobjc-framework-CoreText==2.4
-pyobjc-framework-DictionaryServices==2.4
-pyobjc-framework-EventKit==2.4
-pyobjc-framework-ExceptionHandling==2.4
-pyobjc-framework-FSEvents==2.4
-pyobjc-framework-InputMethodKit==2.4
-pyobjc-framework-InstallerPlugins==2.4
-pyobjc-framework-InstantMessage==2.4
-pyobjc-framework-LatentSemanticMapping==2.4
-pyobjc-framework-LaunchServices==2.4
-pyobjc-framework-Message==2.4
-pyobjc-framework-PreferencePanes==2.4
-pyobjc-framework-PubSub==2.4
-pyobjc-framework-QTKit==2.4
-pyobjc-framework-Quartz==2.4
-pyobjc-framework-ScreenSaver==2.4
-pyobjc-framework-ScriptingBridge==2.4
-pyobjc-framework-SearchKit==2.4
-pyobjc-framework-ServerNotification==2.4
-pyobjc-framework-ServiceManagement==2.4
-pyobjc-framework-Social==2.4
-pyobjc-framework-SyncServices==2.4
-pyobjc-framework-SystemConfiguration==2.4
-pyobjc-framework-WebKit==2.4
 BeautifulSoup==3.2.1
 requests==0.14.0
 Package information for doko package.
 """
 
+import sys
 from setuptools import setup
 
 VERSION = '0.1.0'
 
+requires = [
+        'BeautifulSoup==3.2.1',
+        'requests==0.14.0',
+]
+if sys.platform == 'sys':
+    for package in ('pyobjc==2.4', 'pyobjc-core==2.4', 'pyobjc-framework-CoreLocation==2.4'):
+        requires.append(package)
+
 setup(
         name='doko',
         description="Detect location using CoreLocation on OS X.",
                     'doko = doko:main',
                 ],
         },
-        install_requires=[
-            'pyobjc==2.4',
-            'pyobjc-core==2.4',
-            'pyobjc-framework-CoreLocation==2.4',
-            'BeautifulSoup==3.2.1',
-            'requests==0.14.0',
-        ],
+        install_requires=requires,
     )