Happy New Year, everyone. I’ve been busy at paying work recently, plus cleaning up and testing existing code I’ve been discussing here over the last few months. At work I’ve been developing in WebObjects, which though a lovely platform is not the way of the future so I’m not documenting many of my experiences here.
The applications I’ve been working on recently, Pipe Cleaner and GenX, need – like most applications – configuration. This will store information which can be safely exposed to the public, such as my Google Maps API key, and information that I need to keep private within the application, such as my Freebase username and password (cf. however the password anti-pattern). Furthermore, though the code I’m writing is in Python it is possible that the code that provides the UI will be written in another language, such as PHP inside of WordPress.
Given these considerations, here’s my design choices:
- configuration files are stored as multiple individual files inside a directory (or directories)
- configuration files are in JSON, and contain a dictionary of dictionaries (see below)
- configuration files can be marked as private or public
- the same logical configuration (say for Amazon, which has both public and private information) can be in a public and private file
- the configuration is global, but is accessed through setter/getter properties
- non-global versions of the configuration can be made
That all said, here’s what I’ve written. First, the setters and getters:
class Cfg:
_cfg_private = {}
_cfg_public = {}
@apply
def public():
def fget(self):
return self._cfg_public
return property(**locals())
@apply
def private():
def fget(self):
return self._cfg_private
return property(**locals())
As an aside, I’m not 100% sure about Python decorators and wonder if my favorite language is being turned into a C++ like mess.
Next, the ‘add’ function that adds information to the configuration ensuring private and public are handled correctly. Note that there can be multiple dictionaries inside of ‘d’, but ‘d’ is either all Public or not.
def add(self, d):
if type(d) != types.DictType:
raise TypeError("only dictionaries can be added")
if d.get('@Public'):
#
# Public definitions never overwrite private definitions
#
for key, value in d.iteritems():
if type(value) != types.DictType:
continue
if not self._cfg_private.has_key(key):
self._cfg_private[key] = value
self._cfg_public[key] = value
else:
self._cfg_private.update(d)
And finally the loader, which gets everything in a directory or one level down. Note the ‘exception’ parameter which makes me a bad person, but I don’t like code failing unless I tell it to.
def load(self, path, exception = False, depth = 0):
try:
if os.path.isdir(path) and depth < 2:
for file in os.listdir(path):
self.load(os.path.join(path, file))
elif os.path.isfile(path):
if path.endswith(".json"):
self.add(json.loads(bm_io.readfile(path)))
except:
if exception:
raise
Log("ignoring exception", exception = True, path = path)
And one more thing: make the global configuation:
cfg = Cfg()
Here’s how you use it:
import bm_cfg
# setup ... on a per-file or directory basis
for file in sys.argv[1:]:
bm_cfg.cfg.load(file)
# use it
pprint.pprint({
"private" : bm_cfg.cfg.private,
"public" : bm_cfg.cfg.public,
}, width = 1)
Here’s what my configuration directory looks like:
$ pwd /Users/davidjanes/Sites/pc/cfg $ ls amazon.json freebase.json praized.json amazon.public.json gmaps.json yahoo.json
Here’s the (private) amazon.json:
{
"amazon" : {
"Locale" : "us",
"AccessKeyID" : "0......",
"AssociateTag" : "ona-20",
"Private" : "Don't See"
}
}
And here’s the (public) amazon.public.json:
{
"@Public" : 1,
"amazon" : {
"Locale" : "us",
"AccessKeyID" : "0......",
"AssociateTag" : "ona-20"
}
}
Note that if the private version of the Amazon file wasn’t available, the public version would also be in the private one. I.e. the private configuration basically is “everything” (noting possibly exceptions above in the code).
[...] need to be able to persist information to disk or database. As documented here several weeks ago, we already have that covered with our bm_cfg module. In ~/.cfg/fireeagle.json, create the following JSON format [...]