Skip to content

Commit ae2fdc5

Browse files
authored
Merge pull request #3 from crits/ckane-rwobj-example
Example of read/write and object translation
2 parents 47e88e8 + c586b43 commit ae2fdc5

File tree

8 files changed

+251
-4
lines changed

8 files changed

+251
-4
lines changed

light/__init__.py

+17-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66

77
from gunicorn.six import iteritems
88

9-
from .framework import route_framework
9+
from .framework import route_framework, load_driver
10+
from . import backend
1011

1112

1213
def number_of_workers():
@@ -52,14 +53,29 @@ def main():
5253
default=number_of_workers(),
5354
help='Number of workers to boot (default: CPU_COUNT * 2 + 1)'
5455
)
56+
parser.add_argument(
57+
'-D', '--driver', type=str, dest='driver',
58+
default='disk:demo_db',
59+
help='Driver with store name, such as disk:<folder name> or mysql:<database name>'
60+
)
5561

5662
args = parser.parse_args()
5763

64+
dbdriver, dbstore = args.driver.split(':', 1)[0:2]
5865
options = {
5966
'bind': '%s:%s' % (args.host, args.port),
6067
'workers': args.workers,
68+
'dbdriver': dbdriver,
69+
'dbstore': dbstore,
6170
}
6271

72+
# Instantiate the backend driver (TODO: Make this generic and a configurable)
73+
backend.current_driver = load_driver(options)
74+
75+
if not backend.current_driver:
76+
print('Cannot find driver {d}\n'.format(d=options['dbdriver']))
77+
return 1
78+
6379
# Create a Falcon app.
6480
app = falcon.API()
6581

light/backend.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
current_driver = None

light/drivers/__init__.py

Whitespace-only changes.

light/drivers/disk.py

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import os
2+
import json
3+
import bson
4+
5+
# A simple driver that uses a directory structure to manage
6+
# data objects as discrete text files containing JSON data
7+
8+
class disk_driver(object):
9+
def __init__(self, root_dir='.'):
10+
self.root = root_dir
11+
12+
# If they provided the trailing slash, remove it
13+
# for consistency
14+
if self.root[-1] == '/':
15+
self.root = self.root[:-1]
16+
17+
if not os.access('{root}'.format(root=self.root), os.W_OK):
18+
os.mkdir(path='{root}'.format(root=self.root), mode=0o755)
19+
20+
# Indiscriminantly write the object back to disk. Overwrite any existing instance.
21+
def store(self, set_name, obj):
22+
if not os.access('{root}/{set_name}'.format(root=self.root, set_name=set_name), os.W_OK):
23+
os.mkdir(path='{root}/{set_name}'.format(root=self.root, set_name=set_name), mode=0o755)
24+
with open('{root}/{set_name}/{objid}.json'.format(root=self.root, set_name=set_name,
25+
objid=obj['id']), 'w') as objfh:
26+
json.dump(obj, objfh)
27+
28+
# Given a set_name and objid, load the object identified by that objid. If it isn't present,
29+
# return None
30+
def load(self, set_name, objid):
31+
if os.access('{root}/{set_name}/{objid}.json'.format(root=self.root, set_name=set_name,
32+
objid=str(objid)), os.R_OK):
33+
with open('{root}/{set_name}/{objid}.json'.format(root=self.root, set_name=set_name,
34+
objid=str(objid)), 'r') as objfh:
35+
obj = json.load(objfh)
36+
return obj
37+
38+
return None
39+
40+
# Fetch all entities from a given set (given by set_name)
41+
def get_all(self, set_name):
42+
if not os.access('{root}/{set_name}'.format(root=self.root, set_name=set_name), os.W_OK):
43+
return
44+
45+
with os.scandir('{root}/{set_name}'.format(root=self.root, set_name=set_name)) as dir_handle:
46+
for d_ent in dir_handle:
47+
if not d_ent.name.startswith('.') and d_ent.is_file():
48+
# Yields only the ObjectId portion of the name, wraps it in an ObjectId
49+
yield bson.ObjectId(d_ent.name.split('.')[0])

light/framework.py

+11
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,17 @@ def on_get(self, req, res):
99
"""Handles all GET requests."""
1010
_test_resource_get(req, res)
1111

12+
def load_driver(options):
13+
# Dynamically find db driver among installed packages, and return it if it
14+
# is found
15+
for importer, pkgname, ispkg in pkgutil.walk_packages(['./light/drivers']):
16+
if pkgname == options['dbdriver']:
17+
full_pkg_name = 'drivers.{pkgname}'.format(pkgname=pkgname)
18+
module = importer.find_module(full_pkg_name).load_module(full_pkg_name)
19+
cobj = getattr(module, '{pkgname}_driver'.format(pkgname=pkgname))
20+
cinst = cobj(options['dbstore'])
21+
return cinst
22+
1223
def route_framework(app):
1324
# Instantiate the TestResource class
1425
test_resource = TestResource()

light/light_types.py

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import bson
2+
from . import backend
3+
4+
class LightField(object):
5+
def __init__(self, init=None, required=False, pk=False, unique=False):
6+
self.required = required
7+
self.pk = pk
8+
self.unique = unique
9+
self.assign(init)
10+
self.field_name = type(self).__name__
11+
12+
def val(self):
13+
return self.value
14+
15+
def assign(self, val):
16+
self.value = val
17+
return val
18+
19+
def __str__(self):
20+
return str(self.val())
21+
22+
class LightStr(LightField):
23+
def __init__(self, init=None, required=False, pk=False, unique=False):
24+
super(LightStr, self).__init__(init, required, pk, unique)
25+
26+
def assign(self, val):
27+
assert(isinstance(val, str))
28+
super(LightStr, self).assign(val)
29+
30+
class LightInt(LightField):
31+
def __init__(self, init=None, required=False, pk=False, unique=False):
32+
super(LightInt, self).__init__(init, required, pk, unique)
33+
34+
def assign(self, val):
35+
assert(isinstance(val, int))
36+
super(LightInt, self).assign(val)
37+
38+
class LightBool(LightField):
39+
def __init__(self, init=None, required=False, pk=False, unique=False):
40+
super(LightBool, self).__init__(init, required, pk, unique)
41+
42+
def assign(self, val):
43+
assert(isinstance(val, bool))
44+
super(LightBool, self).assign(val)
45+
46+
# A base type for a document object that, at least, matches the following
47+
# signature:
48+
#
49+
# obj = {
50+
# "id": ObjectId()
51+
# }
52+
#
53+
class LightDoc(object):
54+
def __init__(self, **kwargs):
55+
self.special_fields = ['set_name']
56+
self.valid = False
57+
self.set_name = type(self).set_name
58+
self.data = {}
59+
self.pk = None
60+
61+
# Dynamically identify the defined fields from the subclass definition
62+
db_fields = filter(lambda x: isinstance(getattr(type(self),x),LightField), vars(type(self)))
63+
for fieldk in db_fields:
64+
new_field = getattr(type(self), fieldk)
65+
self.data[fieldk] = new_field.val()
66+
assert(not(self.pk and new_field.pk))
67+
self.pk = new_field
68+
69+
if 'oid' not in kwargs or kwargs['oid'] == None:
70+
# If instance construction gives us a NoneType oid, then we presume
71+
# to be constructing a new entitiy, so give it a brand new ObjectId
72+
self.data['id'] = bson.ObjectId()
73+
74+
# Also, walk the rest of the args for field initializers
75+
for fieldk in db_fields:
76+
if fieldk in kwargs:
77+
self.data[fieldk] = type(getattr(self, fieldk))(init=kwargs[fieldk])
78+
else:
79+
self.data[fieldk] = type(getattr(self, fieldk))()
80+
81+
else:
82+
# Otherwise, we are to perform a lookup and load of the designated
83+
# object
84+
self.load(kwargs['oid'])
85+
86+
def get_all(set_name, dtype):
87+
for objid in backend.current_driver.get_all(set_name=set_name):
88+
yield(dtype(oid=objid))
89+
90+
def save(self):
91+
output_data = {}
92+
for obj_key in self.data:
93+
output_data[obj_key] = str(self.data[obj_key])
94+
backend.current_driver.store(self.set_name, output_data)
95+
96+
def load(self, objid):
97+
input_data = backend.current_driver.load(self.set_name, objid)
98+
99+
# Clear the instance data
100+
self.data = {}
101+
102+
if input_data:
103+
for obj_key in input_data:
104+
if obj_key == 'id':
105+
self.data[obj_key] = bson.ObjectId(input_data[obj_key])
106+
else:
107+
self.data[obj_key] = input_data[obj_key]
108+
self.valid = True
109+
else:
110+
# Invalidate if the object doesn't exist
111+
self.valid = False

light/photons/Sources.py

+61-3
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,77 @@
11
import falcon
2+
import json
3+
from bson import ObjectId
4+
from light.light_types import LightDoc, LightField, LightStr, LightInt, LightBool
25

36
resources = ["SourceSet","SourceItem"]
47

8+
# A very simple entity schema that's built on top of the LightDoc and
9+
# contains the following additional fields:
10+
#
11+
# obj = {
12+
# .....
13+
# 'active': boolean,
14+
# 'source_name': string
15+
#}
16+
#
17+
class Source(LightDoc):
18+
set_name = 'sources'
19+
source_name = LightStr(init="Test", required=True)
20+
active = LightBool(init=True)
21+
22+
def get_all():
23+
for source_obj in LightDoc.get_all(Source.set_name, Source):
24+
yield source_obj
25+
26+
def json(self):
27+
output_json = self.data
28+
output_json['id'] = str(output_json['id'])
29+
return output_json
30+
31+
def load(self, oid):
32+
# First, load the content from disk into the JSON structure. This will
33+
# only perform a data-conversion on the 'id' (str -> ObjectId)
34+
super(Source, self).load(oid)
35+
36+
# XXX: It is up to you to perform any remaining data conversions to the
37+
# fields managed by *this* class, here, after the initial load. Remember,
38+
# set self.valid to False if the data loaded fails any validation, as the
39+
# superclass load() function will have set it to True
40+
#
41+
# This could include loading data from within separate collections/tables/sets
42+
# which represent sub-documents of this data, and which are represented as
43+
# foreign keys within the core object's data fields.
44+
#
45+
546
class SourceSet(object):
647
def routes_set(self, app):
748
app.add_route('/sources', self)
849

950
def on_get(self, req, res):
1051
res.status = falcon.HTTP_200
11-
res.body = ('Testing source access gets.')
52+
res.body = json.dumps(list(x.json() for x in Source.get_all()))
53+
54+
def on_post(self, req, res):
55+
if req.content_length > 0:
56+
json_input = json.load(req.stream)
57+
if 'active' in json_input and 'source_name' in json_input:
58+
src_obj = Source(source_name=LightStr(init=json_input['source_name']), active=LightBool(init=json_input['active']))
59+
src_obj.save()
60+
res.body = json.dumps({'success': True, 'oid': src_obj.json()['id']})
61+
else:
62+
res.status = 500
63+
else:
64+
res.status = 500
1265

1366
class SourceItem(object):
1467
def routes_set(self, app):
1568
app.add_route('/sources/{source}', self)
1669

1770
def on_get(self, req, res, source):
18-
res.status = falcon.HTTP_200
19-
res.body = ('Testing source access gets, getting item {source}.'.format(source=source))
71+
src_obj = Source(oid=source)
72+
if src_obj.valid:
73+
res.status = falcon.HTTP_200
74+
res.body = json.dumps(src_obj.json())
75+
else:
76+
res.status = falcon.HTTP_500
77+
res.body = json.dumps({'success': False, 'message': 'Unable to find source with id {objid}'.format(objid=source)})

requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
bson
12
cython
23
falcon --install-option="--no-binary :all:"
34
gunicorn

0 commit comments

Comments
 (0)