Commits

Alexandre Macabies committed 9f702bf

implements search!

Comments (0)

Files changed (4)

distribute_setup.py

 """Bootstrap distribute installation
 
 If you want to use setuptools in your package's setup.py, just include this
-file in the same directory with it, and add this to the top of your setup.py::
+file in the same directory with it, and add this to the top of your setup.py:
 
     from distribute_setup import use_setuptools
     use_setuptools()
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
+import logging
 import json
 import pprint
 import datetime
 
 from http.client import HTTPConnection
 
+logger = logging.getLogger(__name__)
+
 API_BASE = 'https://open.ge.tt/1/'
 API_KEY = 't05kormjprb2o6rm8f8wmts2thjjor'
-DEBUG = False
 
 class APIError(Exception):
     pass
     return _request(request)
 
 def _request(req):
-    if DEBUG:
-        print('--------------------------------------------------------------------------------')
-        print("%s request to %s -->" % (req.get_method(), req.full_url))
-
-        if req.data:
-            print('data: ' + req.data.decode('utf-8'))
+    logger.debug("%s request to %s", req.get_method(), req.full_url)
+    if req.data: logger.debug("data: %s", req.data.decode('utf-8'))
 
     try:
         resp = urlopen(req)
 
     try:
         result = json.loads(raw)
-    except:
-        print('Error: unable to decode JSON: %s' % raw)
-        raise
+    except Exception:
+        print("Error: unable to decode JSON: %s" % raw)
+        raise APIError("Unable to decode JSON: %s", raw)
 
-    if DEBUG:
-        print('response (%d):' % resp.status)
+    logger.debug("response (%d):\n%s", resp.status, pprint.pformat(result))
 
-        pprint.pprint(result)
-        print()
-
-    if "error" in result:
-        raise APIError(result["error"])
+    if 'error' in result:
+        raise APIError(result['error'])
 
     return result
 
         self.login_token(self.rtoken)
 
     def login_auth(self, email, password):
+        logger.debug("Loging-in user %r", email)
         result = _post_request('users/login', apikey=API_KEY, email=email, password=password)
         self._load(result)
 
     def login_token(self, rtoken):
+        logger.debug("Loging-in (refresh) user with token %r", rtoken)
         result = _post_request('users/login', refreshtoken=rtoken)
         self._load(result)
 
     def list_shares(self, skip=None, limit=None):
+        logger.debug("Listing shares")
         if skip is not None and limit is not None:
             results = _get_request('shares', accesstoken=self.atoken, skip=str(skip), limit=str(limit))
         else:
              yield share
 
     def get_share(self, name):
+        logger.debug("Getting share %r", name)
         result = _get_request('shares/' + name)
 
         share = UserShare(self)
         return share
 
     def create_share(self, title=None):
+        logger.debug("Creating share with title %r", title)
         if title is not None:
             result = _post_request('shares/create', _accesstoken=self.atoken, title=title)
         else:
             self.files[f.id] = f
 
     def refresh(self):
-        result = _get_request('shares/' + self.name)
+        logger.debug("Refreshing share %r", self.name)
+        result = _get_request('shares/%s' % self.name)
         self._load(result)
 
 class UserShare(Share):
         self.user = user
 
     def update(self, **fields):
+        logger.debug("Updating user share %r", self.name)
         result = _post_request('shares/%s/update' % self.name, _accesstoken=self.user.atoken, **fields)
         self._load(result)
 
     def destroy(self):
+        logger.debug("Destroying user share %r", self.name)
         _post_request('shares/%s/destroy' % self.name, _accesstoken=self.user.atoken)
 
     def create_file(self, filename, size=None):
+        logger.debug("Creating file %r in user share %r", filename, self.name)
         result = _post_request('files/%s/create' % self.name, _accesstoken=self.user.atoken, filename=filename)
         file = File(self)
         file._load(result)
             self.put_url = None
 
     def destroy(self):
+        logger.debug("Destoying file %s/%s", self.share.name, self.id)
         _post_request('files/%s/%s/destroy' % (self.share.name, self.id), _accesstoken=self.share.user.atoken)
         del self.share.files[self.id]
 
     def refresh(self):
+        logger.debug("Refreshing file %s/%s", self.share.name, self.id)
         result = _get_request('files/%s/%s' % (self.share.name,  self.id))
         self._load(result)
 
         self.ex = None
 
     def run(self):
+        logger.debug("Runnning FileUpload thread for file %r", self.file)
         try:
             parsed = urlparse(self.file.put_url)
             conn = HTTPConnection(parsed.netloc)
 import argparse
 import signal
 import re
+import unicodedata
+import difflib
+import pprint
 
+from collections import defaultdict
 from itertools import chain
 from time import sleep
 
 from gett import *
 
 url_re = re.compile(r'^(?:http://ge\.tt/|/)?(\w+)(?:/(?:v/(\d+)/?)?)?$')
+DEFAULT_SIMILARITY_RATIO = .95
+
+def ascii_only(input_str):
+    nkfd_form = unicodedata.normalize('NFKD', input_str)
+    return ''.join([c for c in nkfd_form if not unicodedata.combining(c)])
+
+def similar(a, b, ratio_floor):
+        return difflib.SequenceMatcher(a=a, b=b).ratio() > ratio_floor
 
 def humansize(nbytes, pad=False):
     if nbytes is None:
         return ''
 
-    for (exp, unit) in ((9, 'GB'), (6, 'MB'), (3, 'KB'), (0, 'B')):
+    for (exp, unit) in ((9, 'GB'), (6, 'MB'), (3, 'KB'), (0, ' B')):
        if nbytes >= 10**exp:
            break
 
     print('--------------------------------------------------------------------------------')
 
     if share.files:
-        max_url = max([len(_.url) for _ in share.files.values()])
+        max_url = max(len(_.url) for _ in share.files.values())
 
         for file in share.files.values():
-            print('- %s  %s  %s  %s' % (shorten(file.name, 29), humansize(file.size, True), file.url.ljust(max_url), file.readystate))
+            print(' - %s  %s  %s  %s' % (shorten(file.name, 28), humansize(file.size, True), file.url.ljust(max_url), file.readystate))
     else:
-        print('- No files')
+        print(' - No files')
 
     print()
 
     parser = argparse.ArgumentParser(
             description="A command-line Ge.tt uploader and manager",
             epilog="Note that whenever http://ge.tt/<share_name>[/v/<fileid>] is expected, you can omit the http://ge.tt/ part.")
-    parser.add_argument('-D', dest='debug', action='store_true', help='Debug API calls')
+    parser.add_argument('-D', dest='debug', action='store_true', help='Debug API calls (warning: very verbose)')
+    # parser.add_argument('-R', '--similarity-ratio', default=DEFAULT_SIMILARITY_RATIO, type=float, dest='similarity_ratio', action='store_const', help='Similarity ratio (between 0 and 1) used for searching')
 
     upload_group = parser.add_argument_group('Upload options')
     upload_group.add_argument('file', nargs='*', type=pattern, help="Name of a file or a directory to upload. Patterns are allowed. This is not recursive.")
 
     other_group = parser.add_argument_group('Other actions')
     other_group.add_argument('--delete', nargs='+', dest='delete', metavar='URL', help='Delete a share or a file')
-    other_group.add_argument('--list', nargs='*', dest='list', metavar='SHARE_URL', help='List the files in the specified share. If no share is specified, list all your shares.')
+    other_group.add_argument('-l', '--list', nargs='*', dest='list', metavar='SHARE_URL', help='List the files in the specified share. If no share is specified, list all your shares.')
+    other_group.add_argument('-S', '--search', nargs='+', dest='search', metavar='SEARCH_TERMS', help='Search in share titles and file names.')
 
     auth_group = parser.add_argument_group('Authentification')
     auth_group.add_argument('-L', dest='ignore_token', action='store_true', help='Log-in with a different account than the stored one (if any)')
     auth_group.add_argument('-e', dest='email', help='Email to login with')
     auth_group.add_argument('-p', dest='password', help='Password to login with')
-    auth_group.add_argument('-k', dest='tokenfile', help='Ge.tt token file path (~/.gett-token)', default=os.path.join(home, '.gett-token'))
+    auth_group.add_argument('-k', dest='tokenfile', help='Ge.tt token file path (default: ~/.gett-token)', default=os.path.join(home, '.gett-token'))
 
     args = parser.parse_args()
 
     if args.debug:
-        import gett
-        gett.DEBUG = True
+        import logging
+        logging.basicConfig(level=logging.DEBUG)
 
     user = User()
     logged = False
 
         if reply.lower() == 'y':
             # Save the refreshtoken to the user's home directory (by default)
-
             with open(args.tokenfile, 'w') as file:
                 file.write(user.rtoken)
 
                 print('Deleted share: %s [%s]' % (share.title or 'Untitled', share.url))
         print()
 
+    # --search command
+
+    if args.search:
+        simplify = lambda s: ascii_only(s.lower())
+        phrase = ' '.join(args.search)
+        phrase_simple = simplify(phrase)
+        print('Searching for `%s`...\n' % phrase)
+
+        # ratio = args.similarity_ratio
+        # if not 0 <= ratio <= 1:
+        #     ratio = DEFAULT_SIMILARITY_RATIO
+        #     print('Bad similarity ratio value. Using default: %2.f' % DEFAULT_SIMILARITY_RATIO)
+        ratio = DEFAULT_SIMILARITY_RATIO
+        is_similar = lambda a, b: similar(a, b, ratio)
+
+        found_in_shares = []
+        found_in_files = defaultdict(lambda: [])
+        for share in user.list_shares():
+            title = simplify(share.title or '')
+            if phrase_simple in title or is_similar(phrase_simple, title):
+                found_in_shares.append(share)
+
+            for file in share.files.values():
+                fname = simplify(file.name or '')
+                if phrase_simple in fname or is_similar(phrase_simple, fname):
+                    found_in_files[share].append(file)
+
+        try:
+            max_url = max(len(_.url) for _ in found_in_shares)
+        except ValueError:
+            max_url = 0
+        try:
+            max_url_shares = max(len(_.url) for _ in found_in_files.keys())
+            max_url_files = max(max(len(_.url) for _ in f) for f in found_in_files.values())
+        except ValueError:
+            max_url_shares = max_url_files = 0
+
+        maximax = max(max_url, max_url_shares + 6, max_url_files + 3)
+
+        if found_in_shares:
+            print('Found %d share(s):' % len(found_in_shares))
+
+            for share in found_in_shares:
+                print(' - %s  %s%s' % (share.url.ljust(max_url), ' '*(maximax-max_url), shorten(share.title, 75 - max_url)))
+        else:
+            print('Nothing found in shares.')
+
+        print()
+
+        if found_in_files:
+
+            print('Found %d file(s):' % sum(map(len, found_in_files.values())))
+
+            for share, files in found_in_files.items():
+                print(' - Share %s  %s%s' % (share.url.ljust(max_url_shares), ' '*(maximax-max_url_shares-6), shorten(share.title, 69 - max_url_shares)))
+
+                for file in files:
+                    print('    - %s  %s%s' % (file.url.ljust(max_url_files), ' '*(maximax-max_url_files-6), shorten(file.name, 72 - max_url_files)))
+        else:
+            print('Nothing found in files.')
+
+        print()
+
     # File uploads
 
     if args.file:
 setup(name='gett-cli',
       version='0.2.1',
       description='A command-line Ge.tt uploader and manager',
-      author=u'Mickaël THOMAS',
+      author='Mickaël THOMAS',
       author_email='mickael9@gmail.com',
       url='https://bitbucket.org/mickael9/gett-cli/',
       py_modules=['gett', 'gett_uploader'],