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 |