David Blume commited on 2016-09-08 23:30:02
Showing 6 changed files, with 400 additions and 0 deletions.
| ... | ... |
@@ -0,0 +1,33 @@ |
| 1 |
+# kvs |
|
| 2 |
+ |
|
| 3 |
+kvs is a rudimentary key/value store written in Python. |
|
| 4 |
+ |
|
| 5 |
+# Getting the project |
|
| 6 |
+ |
|
| 7 |
+You can get a copy of this project by clicking on the |
|
| 8 |
+[ZIP](http://git.dlma.com/kvs.git/zipball/master) |
|
| 9 |
+or [TAR](http://git.dlma.com/kvs.git/tarball/master) buttons |
|
| 10 |
+near the top right of the GitList web page. |
|
| 11 |
+ |
|
| 12 |
+If you're me, and you want to contribute to the repo, then you can clone it like so: |
|
| 13 |
+ |
|
| 14 |
+ git clone ssh://USERNAME@dlma.com/~/git/kvs.git |
|
| 15 |
+ |
|
| 16 |
+# Building it |
|
| 17 |
+ |
|
| 18 |
+1. Enable Python pages at your web server. |
|
| 19 |
+2. Move auth\_sample.txt to auth.txt. |
|
| 20 |
+ |
|
| 21 |
+# Is it any good? |
|
| 22 |
+ |
|
| 23 |
+[Yes](https://news.ycombinator.com/item?id=3067434). |
|
| 24 |
+ |
|
| 25 |
+# To Do |
|
| 26 |
+ |
|
| 27 |
+1. Better document it. |
|
| 28 |
+2. PEP-8 |
|
| 29 |
+3. Productize it. |
|
| 30 |
+ |
|
| 31 |
+# Licence |
|
| 32 |
+ |
|
| 33 |
+This software uses the [MIT license](http://git.dlma.com/roku_ip_tagger.git/blob/master/LICENSE.txt). |
| ... | ... |
@@ -0,0 +1 @@ |
| 1 |
+yourauthorizationhere |
| ... | ... |
@@ -0,0 +1,85 @@ |
| 1 |
+#!/usr/bin/python |
|
| 2 |
+#!C:/Python26/python.exe |
|
| 3 |
+# |
|
| 4 |
+# http://www.evanfosmark.com/2009/01/cross-platform-file-locking-support-in-python/ |
|
| 5 |
+ |
|
| 6 |
+import os |
|
| 7 |
+import time |
|
| 8 |
+import errno |
|
| 9 |
+ |
|
| 10 |
+class FileLockException(Exception): |
|
| 11 |
+ pass |
|
| 12 |
+ |
|
| 13 |
+class FileLock(object): |
|
| 14 |
+ """ A file locking mechanism that has context-manager support so |
|
| 15 |
+ you can use it in a with statement. This should be relatively cross |
|
| 16 |
+ compatible as it doesn't rely on msvcrt or fcntl for the locking. |
|
| 17 |
+ """ |
|
| 18 |
+ |
|
| 19 |
+ def __init__(self, file_name, timeout=10, delay=.05): |
|
| 20 |
+ """ Prepare the file locker. Specify the file to lock and optionally |
|
| 21 |
+ the maximum timeout and the delay between each attempt to lock. |
|
| 22 |
+ """ |
|
| 23 |
+ self.is_locked = False |
|
| 24 |
+ self.lockfile = "%s.lock" % file_name |
|
| 25 |
+ self.timeout = timeout |
|
| 26 |
+ self.delay = delay |
|
| 27 |
+ |
|
| 28 |
+ |
|
| 29 |
+ def acquire(self): |
|
| 30 |
+ """ Acquire the lock, if possible. If the lock is in use, it check again |
|
| 31 |
+ every `wait` seconds. It does this until it either gets the lock or |
|
| 32 |
+ exceeds `timeout` number of seconds, in which case it throws |
|
| 33 |
+ an exception. |
|
| 34 |
+ """ |
|
| 35 |
+ if self.is_locked: |
|
| 36 |
+ return |
|
| 37 |
+ start_time = time.time() |
|
| 38 |
+ while True: |
|
| 39 |
+ try: |
|
| 40 |
+ self.fd = os.open(self.lockfile, os.O_CREAT|os.O_EXCL|os.O_RDWR) |
|
| 41 |
+ os.write(self.fd, "%d" % os.getpid()) |
|
| 42 |
+ break; |
|
| 43 |
+ except OSError, e: |
|
| 44 |
+ if e.errno != errno.EEXIST: |
|
| 45 |
+ raise |
|
| 46 |
+ if (time.time() - start_time) >= self.timeout: |
|
| 47 |
+ raise FileLockException("Timeout occured.")
|
|
| 48 |
+ time.sleep(self.delay) |
|
| 49 |
+ self.is_locked = True |
|
| 50 |
+ |
|
| 51 |
+ |
|
| 52 |
+ def release(self): |
|
| 53 |
+ """ Get rid of the lock by deleting the lockfile. |
|
| 54 |
+ When working in a `with` statement, this gets automatically |
|
| 55 |
+ called at the end. |
|
| 56 |
+ """ |
|
| 57 |
+ if self.is_locked: |
|
| 58 |
+ os.close(self.fd) |
|
| 59 |
+ os.unlink(self.lockfile) |
|
| 60 |
+ self.is_locked = False |
|
| 61 |
+ |
|
| 62 |
+ |
|
| 63 |
+ def __enter__(self): |
|
| 64 |
+ """ Activated when used in the with statement. |
|
| 65 |
+ Should automatically acquire a lock to be used in the with block. |
|
| 66 |
+ """ |
|
| 67 |
+ if not self.is_locked: |
|
| 68 |
+ self.acquire() |
|
| 69 |
+ return self |
|
| 70 |
+ |
|
| 71 |
+ |
|
| 72 |
+ def __exit__(self, type, value, traceback): |
|
| 73 |
+ """ Activated at the end of the with statement. |
|
| 74 |
+ It automatically releases the lock if it isn't locked. |
|
| 75 |
+ """ |
|
| 76 |
+ if self.is_locked: |
|
| 77 |
+ self.release() |
|
| 78 |
+ |
|
| 79 |
+ |
|
| 80 |
+ def __del__(self): |
|
| 81 |
+ """ Make sure that the FileLock instance doesn't leave a lockfile |
|
| 82 |
+ lying around. |
|
| 83 |
+ """ |
|
| 84 |
+ self.release() |
|
| 85 |
+ |
| ... | ... |
@@ -0,0 +1,169 @@ |
| 1 |
+#!/usr/bin/env python |
|
| 2 |
+# index.py for a rudimentary key/value server by David Blume |
|
| 3 |
+import os |
|
| 4 |
+import sys |
|
| 5 |
+import time |
|
| 6 |
+import datetime |
|
| 7 |
+import cgi, cgitb |
|
| 8 |
+import filelock |
|
| 9 |
+import tempfile |
|
| 10 |
+import yaml |
|
| 11 |
+import texttime |
|
| 12 |
+ |
|
| 13 |
+cgitb.enable(display=0, logdir="/tmp") |
|
| 14 |
+store_name = 'store.txt' |
|
| 15 |
+ |
|
| 16 |
+ |
|
| 17 |
+def read_file(full_pathname): |
|
| 18 |
+ store = dict() |
|
| 19 |
+ if os.path.isfile(full_pathname): |
|
| 20 |
+ try: |
|
| 21 |
+ with filelock.FileLock(full_pathname): |
|
| 22 |
+ with open(full_pathname, 'r') as f: |
|
| 23 |
+ store = yaml.load(f) |
|
| 24 |
+ except filelock.FileLockException as e: |
|
| 25 |
+ raise |
|
| 26 |
+ return store |
|
| 27 |
+ |
|
| 28 |
+ |
|
| 29 |
+def write_file(full_pathname, data): |
|
| 30 |
+ ''' See http://david.dlma.com/blog/dads-project-in-the-garage ''' |
|
| 31 |
+ try: |
|
| 32 |
+ with filelock.FileLock(full_pathname): |
|
| 33 |
+ with tempfile.NamedTemporaryFile(mode='w', |
|
| 34 |
+ dir=os.path.dirname(full_pathname), |
|
| 35 |
+ delete=False) as f: |
|
| 36 |
+ yaml.dump(data, f) |
|
| 37 |
+ # exiting this "with" flushes, but does not sync |
|
| 38 |
+ f.flush() |
|
| 39 |
+ os.fdatasync(f.fileno()) # Faster, Unix only |
|
| 40 |
+ tempname = f.name |
|
| 41 |
+ os.rename(tempname, full_pathname) # Atomic on Unix, not Windows |
|
| 42 |
+ except filelock.FileLockException as e: |
|
| 43 |
+ raise |
|
| 44 |
+ |
|
| 45 |
+ |
|
| 46 |
+def map_ip_to_location(ip): |
|
| 47 |
+ if ip == '67.188.28.83': |
|
| 48 |
+ return 'home'.ljust(13) |
|
| 49 |
+ elif ip in ('50.224.7.233', '50.224.7.248'):
|
|
| 50 |
+ return 'work'.ljust(13) |
|
| 51 |
+ return ip |
|
| 52 |
+ |
|
| 53 |
+ |
|
| 54 |
+if __name__ == '__main__': |
|
| 55 |
+ localdir = os.path.abspath(os.path.dirname(sys.argv[0])) |
|
| 56 |
+ args = cgi.FieldStorage() |
|
| 57 |
+ key = None |
|
| 58 |
+ extra = None |
|
| 59 |
+ auth = None |
|
| 60 |
+ get_key = False |
|
| 61 |
+ auth_in_get = False |
|
| 62 |
+ doing_post = False |
|
| 63 |
+ |
|
| 64 |
+ # Find the parameters that are GET parameters |
|
| 65 |
+ # Could also check os.environ['REQUEST_METHOD'] == 'POST', |
|
| 66 |
+ # but that doesn't say which way individual params were sent. |
|
| 67 |
+ if os.environ['QUERY_STRING']: |
|
| 68 |
+ qs = os.environ['QUERY_STRING'].split('&')
|
|
| 69 |
+ for pair in qs: |
|
| 70 |
+ k, v = pair.split('=')
|
|
| 71 |
+ if k == 'k': |
|
| 72 |
+ get_key = True |
|
| 73 |
+ if k == 'auth': |
|
| 74 |
+ auth_in_get = True |
|
| 75 |
+ |
|
| 76 |
+ # Could be either GET or POST. Need to check QUERY_STRING above. |
|
| 77 |
+ if "k" in args: |
|
| 78 |
+ key = args["k"].value |
|
| 79 |
+ |
|
| 80 |
+ if os.environ['REQUEST_METHOD'] == 'POST': |
|
| 81 |
+ doing_post = True |
|
| 82 |
+ if not auth_in_get and "auth" in args: |
|
| 83 |
+ auth = args["auth"].value |
|
| 84 |
+ |
|
| 85 |
+ if "extra" in args: |
|
| 86 |
+ extra = args["extra"].value |
|
| 87 |
+ |
|
| 88 |
+ store = read_file(os.path.join(localdir, store_name)) |
|
| 89 |
+ if get_key: |
|
| 90 |
+ keys = key.split(',')
|
|
| 91 |
+ if len(keys) > 1: |
|
| 92 |
+ devices = set() |
|
| 93 |
+ for k in store: |
|
| 94 |
+ d = store[k] |
|
| 95 |
+ if k in keys: |
|
| 96 |
+ devices.add((d['time'], key, d['value'])) |
|
| 97 |
+ if devices: # not empty |
|
| 98 |
+ print "Content-type: text/plain; charset=utf-8\n" |
|
| 99 |
+ sys.stdout.write(sorted(devices, reverse=True)[0][2]) |
|
| 100 |
+ else: |
|
| 101 |
+ print 'Status: 404 Not Found\n' |
|
| 102 |
+ elif key in store: |
|
| 103 |
+ print "Content-type: text/plain; charset=utf-8\n" |
|
| 104 |
+ sys.stdout.write(store[key]['value']) |
|
| 105 |
+ if extra is not None: |
|
| 106 |
+ sys.stdout.write('\n%s' % (store[key]['time'],))
|
|
| 107 |
+ else: |
|
| 108 |
+ print 'Status: 404 Not Found\n' |
|
| 109 |
+ else: |
|
| 110 |
+ wrote_response = False |
|
| 111 |
+ with open('auth.txt', 'r') as f:
|
|
| 112 |
+ authorization = f.read().strip() |
|
| 113 |
+ if doing_post and auth == authorization: |
|
| 114 |
+ for new_key in args: |
|
| 115 |
+ if new_key not in ('auth', 'extra') and \
|
|
| 116 |
+ len(new_key) < 80 and len(args[new_key].value) < 140: |
|
| 117 |
+ if new_key in store and 'created' in store[new_key]: |
|
| 118 |
+ created_time = store[new_key]['created'] |
|
| 119 |
+ else: |
|
| 120 |
+ created_time = int(time.time()) |
|
| 121 |
+ store[new_key] = {'value': args[new_key].value,
|
|
| 122 |
+ 'time': int(time.time()), |
|
| 123 |
+ 'created': created_time, |
|
| 124 |
+ 'origin': os.environ['REMOTE_ADDR']} |
|
| 125 |
+ write_file(os.path.join(localdir, store_name), store) |
|
| 126 |
+ wrote_response = True |
|
| 127 |
+ print "Content-type: text/plain; charset=utf-8\n" |
|
| 128 |
+ print "OK" |
|
| 129 |
+ break |
|
| 130 |
+ elif not os.environ['QUERY_STRING'] and not doing_post: |
|
| 131 |
+ # just the home page, then |
|
| 132 |
+ print "Content-type: text/plain; charset=UTF-8\n" |
|
| 133 |
+ wrote_response = True |
|
| 134 |
+ print "key \tvalue \tcreated \tupdated \torigin \tNotes" |
|
| 135 |
+ print "------------ \t------------- \t----------\t----------\t-------------\t---------------------" |
|
| 136 |
+ devices = { '1G': '(Au)',
|
|
| 137 |
+ '5S': '(Br)', |
|
| 138 |
+ 'YW': '(FW)', |
|
| 139 |
+ 'YY': '(Da)', |
|
| 140 |
+ '2N': '(Li)', |
|
| 141 |
+ '1R': '(Ty)', |
|
| 142 |
+ '1R': '(Ty)', |
|
| 143 |
+ 'YU': '(Lf)', |
|
| 144 |
+ 'YP': '(C4)', |
|
| 145 |
+ } |
|
| 146 |
+ for key in sorted(store, key=lambda k: store[k]['time'], reverse=True): |
|
| 147 |
+ if key[:2] in devices: |
|
| 148 |
+ device = devices[key[:2]] + ' ' |
|
| 149 |
+ else: |
|
| 150 |
+ device = '' |
|
| 151 |
+ d = store[key] |
|
| 152 |
+ td = texttime.stringify(datetime.timedelta(seconds=time.time()-d['time'])) |
|
| 153 |
+ if 'created' in d: |
|
| 154 |
+ created = time.strftime('%Y-%m-%d', time.localtime(d['created']))
|
|
| 155 |
+ else: |
|
| 156 |
+ created = " " |
|
| 157 |
+ if 'origin' in d: |
|
| 158 |
+ origin = map_ip_to_location(d['origin']) |
|
| 159 |
+ else: |
|
| 160 |
+ origin = " " |
|
| 161 |
+ print '\t'.join((key.ljust(12), d['value'].ljust(13), created, str(d['time']), origin, "%supdated %s ago" % (device, td))) |
|
| 162 |
|
|
| 163 |
+ print 'Tip:' |
|
| 164 |
+ print 'sync_ip () { export ENV_VAR=$(curl -s "%s?k=2FN50L002036,2FN44N010911"); }' % os.environ['SCRIPT_URI']
|
|
| 165 |
+ print 'curl --data "`hostname -s`=`hostname -i`&auth=$AUTH" "%s"' % os.environ['SCRIPT_URI'] |
|
| 166 |
+ |
|
| 167 |
+ if not wrote_response: |
|
| 168 |
+ print 'Status: 400 Bad Request\n' |
|
| 169 |
+ |
| ... | ... |
@@ -0,0 +1,107 @@ |
| 1 |
+from datetime import timedelta |
|
| 2 |
+ |
|
| 3 |
+# Set this to the language you want to use. |
|
| 4 |
+LANG = "en" |
|
| 5 |
+ |
|
| 6 |
+# Singular and plural forms of time units in your language. |
|
| 7 |
+unit_names = dict( |
|
| 8 |
+ en = {"year" : ("year", "years"),
|
|
| 9 |
+ "month" : ("month", "months"),
|
|
| 10 |
+ "week" : ("week", "weeks"),
|
|
| 11 |
+ "day" : ("day", "days"),
|
|
| 12 |
+ "hour" : ("hour", "hours"),
|
|
| 13 |
+ "minute" : ("minute", "minutes"),
|
|
| 14 |
+ "second" : ("second", "seconds")})
|
|
| 15 |
+ |
|
| 16 |
+num_repr = dict( |
|
| 17 |
+ en = {1 : "a",
|
|
| 18 |
+ 2 : "two", |
|
| 19 |
+ 3 : "three", |
|
| 20 |
+ 4 : "four", |
|
| 21 |
+ 5 : "five", |
|
| 22 |
+ 6 : "six", |
|
| 23 |
+ 7 : "seven", |
|
| 24 |
+ 8 : "eight", |
|
| 25 |
+ 9 : "nine", |
|
| 26 |
+ 10 : "ten", |
|
| 27 |
+ 11 : "eleven", |
|
| 28 |
+ 12 : "twelve"}) |
|
| 29 |
+ |
|
| 30 |
+def amount_to_str(amount, unit_name): |
|
| 31 |
+ if amount == 1 and unit_name == "hour" and LANG == "en": |
|
| 32 |
+ return "an" |
|
| 33 |
+ if amount in num_repr[LANG]: |
|
| 34 |
+ return num_repr[LANG][amount] |
|
| 35 |
+ return str(amount) |
|
| 36 |
+ |
|
| 37 |
+def seconds_in_units(seconds): |
|
| 38 |
+ """ |
|
| 39 |
+ Returns a tuple containing the most appropriate unit for the |
|
| 40 |
+ number of seconds supplied and the value in that units form. |
|
| 41 |
+ |
|
| 42 |
+ >>> seconds_in_units(7700) |
|
| 43 |
+ (2, 'hour') |
|
| 44 |
+ """ |
|
| 45 |
+ unit_limits = [("year", 365 * 24 * 3600),
|
|
| 46 |
+ ("month", 30 * 24 * 3600),
|
|
| 47 |
+ ("week", 7 * 24 * 3600),
|
|
| 48 |
+ ("day", 24 * 3600),
|
|
| 49 |
+ ("hour", 3600),
|
|
| 50 |
+ ("minute", 60)]
|
|
| 51 |
+ for unit_name, limit in unit_limits: |
|
| 52 |
+ if seconds >= limit * 0.94: |
|
| 53 |
+ amount = int(round(float(seconds) / limit)) |
|
| 54 |
+ return amount, unit_name |
|
| 55 |
+ return seconds, "second" |
|
| 56 |
+ |
|
| 57 |
+def stringify(td): |
|
| 58 |
+ """ |
|
| 59 |
+ Converts a timedelta into a nicely readable string. |
|
| 60 |
+ |
|
| 61 |
+ >>> td = timedelta(days = 77, seconds = 5) |
|
| 62 |
+ >>> print readable_timedelta(td) |
|
| 63 |
+ two months |
|
| 64 |
+ """ |
|
| 65 |
+ seconds = td.days * 3600 * 24 + td.seconds |
|
| 66 |
+ amount, unit_name = seconds_in_units(seconds) |
|
| 67 |
+ |
|
| 68 |
+ # Localize it. |
|
| 69 |
+ i18n_amount = amount_to_str(amount, unit_name) |
|
| 70 |
+ i18n_unit = unit_names[LANG][unit_name][1] |
|
| 71 |
+ if amount == 1: |
|
| 72 |
+ i18n_unit = unit_names[LANG][unit_name][0] |
|
| 73 |
+ return "%s %s" % (i18n_amount, i18n_unit) |
|
| 74 |
+ |
|
| 75 |
+def test(td): |
|
| 76 |
+ if td.days > 100: |
|
| 77 |
+ fmt = "In %s, it's a long time. (%s)" |
|
| 78 |
+ elif td.days > 4: |
|
| 79 |
+ fmt = "I've only got %s to finish the project. (%s)" |
|
| 80 |
+ elif td.days > 0: |
|
| 81 |
+ fmt = "The party was %s ago. (%s)" |
|
| 82 |
+ elif td.seconds > 3600: |
|
| 83 |
+ fmt = "Something weird happened %s ago. (%s)" |
|
| 84 |
+ elif td.seconds > 60: |
|
| 85 |
+ fmt = "The train arrives in %s. (%s)" |
|
| 86 |
+ else: |
|
| 87 |
+ fmt = "%s passes fast. (%s)" |
|
| 88 |
+ print fmt % (stringify(td), str(td)) |
|
| 89 |
+ |
|
| 90 |
+def main(): |
|
| 91 |
+ global LANG |
|
| 92 |
+ LANG = "en" |
|
| 93 |
+ test(timedelta(weeks = 7, days = 3)) |
|
| 94 |
+ test(timedelta(weeks = 1)) |
|
| 95 |
+ test(timedelta(days = 1000)) |
|
| 96 |
+ test(timedelta(days = 400)) |
|
| 97 |
+ test(timedelta(days = 4)) |
|
| 98 |
+ test(timedelta(seconds = 2000)) |
|
| 99 |
+ test(timedelta(seconds = 9888)) |
|
| 100 |
+ test(timedelta(seconds = 999888)) |
|
| 101 |
+ test(timedelta(seconds = 999)) |
|
| 102 |
+ test(timedelta(seconds = 99)) |
|
| 103 |
+ test(timedelta(seconds = 45)) |
|
| 104 |
+ test(timedelta(seconds = 3)) |
|
| 105 |
+ |
|
| 106 |
+if __name__ == "__main__": |
|
| 107 |
+ main() |
|
| 0 | 108 |