option.py
May 23rd, 2012Because my hobby seems to be writing Option libraries in various languages, I’ve written another, in Python: option.py. Enjoy.
Because my hobby seems to be writing Option libraries in various languages, I’ve written another, in Python: option.py. Enjoy.
Because hey, why not?
In Python:
>>> 1 in [1, 2, 3] True |
In Scala:
class In(a: Any) { def in[T](s: Seq[T]) = s.contains(a) } implicit def any2In(a : Any) = new In(a) scala> "a" in List(1, 2, "a") res0: Boolean = true scala> "a" in List(1, 2, "b") res1: Boolean = false |
Daniel Salber and I will be giving the next Appsterdam Weekly Wednesday Lecture on mobile APIs. The talk is on Wednesday July 27 at 12:30 at Vijzelstraat 20, Amsterdam.
Here is our talk description:
Many mobile apps get data or send data to a server. What are the issues in implementing this kind of apps? In this talk, Peter Robinett and Daniel Salber will share their personal experiences designing and implementing both the server and client (iOS) sides of APIs for mobile apps, taking examples from their apps Smakelijk Amsterdam, Coffeeshoppr, iFebo, and JHM. Starting from questions to ask at the beginning of a project, continuing all the way through to debugging and performance, they will lead you through a broad overview of the implementation of a mobile API in your project.
You can use locale to localize things like Python’s datetime module. However, its defaults aren’t always the nicest. For instance, for Dutch is defaults to ISO while I prefer UTF-8:
>>> locale.setlocale(locale.LC_ALL, 'nl_NL') 'nl_NL' >>> locale.getlocale() ('nl_NL', 'ISO8859-1') >>> locale.setlocale(locale.LC_ALL, ('nl_NL', 'utf8@euro')) 'nl_NL.UTF-8' >>> locale.getlocale() ('nl_NL', 'UTF8') |
As you can see, for some reason ‘utf8@euro’ is the wonky encoding name that you have to use.
I tried a lot more logical names before I went directly to the source code for the answer:
>>> locale.setlocale(locale.LC_ALL, ('nl_NL', 'UTF8')) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "/System/Library/Frameworks/Python.framework/Versions/2.6/lib/python2.6/locale.py", line 494, in setlocale return _setlocale(category, locale) locale.Error: unsupported locale setting >>> locale.setlocale(locale.LC_ALL, ('nl_NL', 'utf_8')) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "/System/Library/Frameworks/Python.framework/Versions/2.6/lib/python2.6/locale.py", line 494, in setlocale return _setlocale(category, locale) locale.Error: unsupported locale setting >>> locale.setlocale(locale.LC_ALL, ('nl_NL', 'UTF-8')) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "/System/Library/Frameworks/Python.framework/Versions/2.6/lib/python2.6/locale.py", line 494, in setlocale return _setlocale(category, locale) locale.Error: unsupported locale setting >>> locale.setlocale(locale.LC_ALL, 'nl_nl.utf8@euro') Traceback (most recent call last): File "<stdin>", line 1, in <module> File "/System/Library/Frameworks/Python.framework/Versions/2.6/lib/python2.6/locale.py", line 494, in setlocale return _setlocale(category, locale) locale.Error: unsupported locale setting >>> locale.setlocale(locale.LC_ALL, ('nl_NL', 'utf8')) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "/System/Library/Frameworks/Python.framework/Versions/2.6/lib/python2.6/locale.py", line 494, in setlocale return _setlocale(category, locale) locale.Error: unsupported locale setting |
Even worse, the locale returned for the UTF-8 version isn’t a valid locale tuple:
>>> locale.getlocale() ('nl_NL', 'UTF8') >>> locale.setlocale(locale.LC_ALL, locale.getlocale()) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "/System/Library/Frameworks/Python.framework/Versions/2.6/lib/python2.6/locale.py", line 494, in setlocale return _setlocale(category, locale) locale.Error: unsupported locale setting |
There are many great things going for Python but I find its type system to be rather wonky. Most people will direct you to use the type() function to determine types:
>>> type([]) <type 'list'> >>> type([]) == type(list) False >>> type([]) == type(list()) True >>> type([]).__name__ == 'list' True
However, in everyday use isinstance() is much more useful:
>>> isinstance([], list)
True
>>> isinstance({}, dict)
True
>>> isinstance('', str)
True
>>> isinstance(u'', unicode)
True
>>> isinstance(True, bool)
True
>>> isinstance(0, int)
True
>>> isinstance(0.0, float)
True
And yes, str and unicode are different types. Some have proposed various complicated approaches to deal with duck-typing and the like, but I’m happy with isinstance(). If you need more information, I suggest checking out the types section of the Python documentation or using the types modules. Finally, Shalabh Chaturvedi has written a nice little guide called Python Types and Objects.
Recently I’ve been working on a project for Mediamatic (hopefully more later) and a key part of my work involves using the REST API for their AnyMeta community management system. Unfortunately there isn’t a lot of documentation out there either for AnyMeta in general or for its API in specific. There’s the user guide if you have an AnyMeta system to manage (but you probably don’t, as Mediamatic normally does that for you) and one document outlining the principles behind the API. The latter has a very important table of API methods that is worth reproducing here:
| Method | Explanation |
|---|---|
| anymeta.predicates.get | Get a specific data field, for instance the title of a thing. |
| anymeta.thing.dump | Get a full data-dump of a thing, including all references to its images, video files, etc. |
| anymeta.attachment.create | Create an attachment, e.g. upload an image or video into the system |
| anymeta.edge.add | Connect things in the database using edges, semantical links between things. |
| anymeta.search.live | Full-text search on the database |
| query.execute | Advanced, powerful interface for querying the semantic network and the thing database. |
| identity.search | Search the Identity database which stores RFID — ID links |
| identity.add | Add an identity (e.g. an RFID tag) to the system |
As you can see from this table, AnyMeta is a system of Things that are linked via Edges. A Thing can have Predicates (essentially, properties or attributes) which give more information about the Thing. The full list of API methods available on your AnyMeta site – and this can change based upon what modules you have installed – can be seen at http://yoursite/services/rest/. For instance, http://www.mediamatic.nl/services/rest/.
Unfortunately the /services/rest documentation is where things get tricky, and so I figured you may need an article with more information. The documentation is automatically generated from the code and unfortunately there doesn’t seem to be enough detail to make everything readily apparent. For example, anymeta.thing.insert takes a data array, but what is that? I’ll try to explain some of these gotchas.
First, we’re going to use the AnyMetaAPI Python library. It’s by Mediamatic, handles all authentication (even OAuth), and even has a CLI. Really, use it. There’s a tutorial on the Mediamatic site but it’s quite brief, so this article will walk you through the whole process from start to finish and give you more tips on how to figure out the API. I’ll be using the AnyMetaAPI library for my examples here and I recommend you also use it for any scripts you write that interface with the API. Of course this then requires that some or all of your application be in Python. Your call.
First, we need to install AnyMetaAPI. Here’s one gotcha: I have been using pip recently to install Python modules and successfully installed AnyMetaAPI using it (pip install AnyMetaAPI from my command line). However, I couldn’t for the life of me figure out why I couldn’t complete an OAuth authentication session. Then I noticed that I had version 1.15 and the latest version is 1.22. Thinking the problem might be fixed by upgrading I tried to do that but, try as I might, I couldn’t get pip to upgrade AnyMetaAPI to the latest version. Carefully reading the pip homepage I discovered the cause: pip cannot install eggs, and the most recent versions of AnyMetaAPI have only been released as eggs. Solution: use easy_install.
Now that you have the AnyMetaAPI module installed you now should have a program called any-registry. It has a few basic options:
$ any-registry any-registry 1.22 Usage /usr/local/bin/any-registry [opts] [cmdoptions] Command is one of: list - List all registry entries gui - Show graphical user interface (linux only) add - Add API endpoint del - Remove API endpoint cli - Commandline interface
The first thing you will need to do is add an API endpoint:
$ any-registry add yourname http://yoursite
yourname should be a short name that you can remember and is easy to type. For example, ‘mediamatic’. The URL is the basic URL of your AnyMeta install, for example ‘http://www.mediamatic.nl/’. The program will ask you to open a special URL on the AnyMeta site where you can authorize the program access to your account there. Once you do that and tell the program (this is where I got stuck, mind my note earlier!), you’re ready to start using the CLI: any-registry cli yourname.
Let’s first get your own information:
>>> api.anymeta.user.info()
{u'auth_info': {u'usa_email': u'peter@bubblefoundry.com', u'usa_modify_date': u'2010-07-21 14:15:44', u'usa_logon_date': u'2010-09-15 08:47:51', u'usa_axo': u'public', u'usa_access_date': u'2010-09-15 08:47:51', u'usa_enabled': u'1', u'usa_axo_section': u'content', u'usa_email_sha1': u'2a27a3eec2c03b53e0861563dcf523dc585282a1', u'usa_confirm_logons': u'0', u'usa_create_date': u'2010-07-21 14:15:44', u'usa_id_ref': u'76', u'usa_confirm': u'', u'usa_prev_logon_date': u'2010-09-15 08:47:51'}, u'preferences': [], u'title': u'Peter Robinett', u'id': u'76', u'axo': [u'content', u'public'], u'aro': [{u'id': u'13', u'name': u'members'}]}
As you can, see an object called api is imported into our CLI’s scope, so we can fire authenticated API calls using its objects and methods. When we call the method correctly a Python dictionary is returned to us and, since we didn’t use the result at all, printed to the console. Of course I can use the result as a normal dictionary:
>>> me = api.anymeta.user.info() >>> me['id'] u'76'
As the documentation for anymeta.user.info says, you can also pass an id. Let’s try that:
>>> api.anymeta.user.info(76) Traceback (most recent call last): File "", line 1, in TypeError: __call__() takes exactly 1 argument (2 given)
Ah ha, another gotcha! I can’t exactly tell you why this is happening (my Python-fu isn’t that great), but I can given you the solution: use keyword arguments. Looking at the API documentation again you’ll notice that the parameter is called person_id, so let’s try that:
>>> api.anymeta.user.info(person_id=76)
{u'auth_info': {u'usa_email': u'peter@bubblefoundry.com', u'usa_modify_date': u'2010-07-21 14:15:44', u'usa_logon_date': u'2010-09-15 08:47:51', u'usa_axo': u'public', u'usa_access_date': u'2010-09-15 08:47:51', u'usa_enabled': u'1', u'usa_axo_section': u'content', u'usa_email_sha1': u'2a27a3eec2c03b53e0861563dcf523dc585282a1', u'usa_confirm_logons': u'0', u'usa_create_date': u'2010-07-21 14:15:44', u'usa_id_ref': u'76', u'usa_confirm': u'', u'usa_prev_logon_date': u'2010-09-15 08:47:51'}, u'preferences': [], u'title': u'Peter Robinett', u'id': 76, u'axo': [u'content', u'public'], u'aro': [{u'id': u'13', u'name': u'members'}]}
Much better! Now let’s create a new Thing and we have the handy method anymeta.thing.insert to do so. But what goes in the data array? A brief discursion1:
The best way to view the potential values of a certain type of Thing is to call anymeta.thing.dump on a similar, existing Thing. Like with the previous method, you need to use a keyword argument. Unfortunately, this time it’s called id. While I can see the attraction of having descriptive id names (person_id when we know the Thing is a Person, id when it’s any Thing), I think this is also an annoying inconsistency and forces developers to constantly check the API documentation to see what the id keywords are called. But this article is about using AnyMeta, not improving it. Instead, let’s consider the output:
>>> api.anymeta.thing.dump(id=199)
{u'create_date': u'2010-09-10 18:20:26', u'axo_section': u'content', u'picture_playable': {u'orientation': u'1', u'height': u'0', u'width': u'0', u'att_id_ref': u'199', u'mime': u'image/jpeg', u'is_picture': u'1'}, u'text': {u'body': u'', u'redirect_uri': u'', u'title_short': u'', u'subtitle': u'', u'language': u'en', u'title': u'An Image', u'chapeau': u'', u'label': [], u'intro': u''}, u'share': u'1', u'is_thing': 1, u'file': {u'orientation': u'1', u'is_media': u'1', u'height': u'0', u'width': u'0', u'file_blob': {u'encode': u'base64', u'data':
....
anymeta.thing.dump really does give you everything, so watch out calling it on image Things like I’ve done: you get the entire image data in base64 encoding dumped to your screen!
Note, you may have all your dump commands fail like so:
>>> api.anymeta.thing.dump() Traceback (most recent call last): File "", line 1, in File "/Library/Python/2.6/site-packages/AnyMetaAPI-1.22-py2.6.egg/anymeta/api/base.py", line 21, in __call__ return self.api.doMethod(self.part, kwargs) File "/Library/Python/2.6/site-packages/AnyMetaAPI-1.22-py2.6.egg/anymeta/api/base.py", line 161, in doMethod return self._getPage(http_method, str(url), postdata, headers, format) File "/Library/Python/2.6/site-packages/AnyMetaAPI-1.22-py2.6.egg/anymeta/api/base.py", line 226, in _getPage_httplib return self._processPage(page, format) File "/Library/Python/2.6/site-packages/AnyMetaAPI-1.22-py2.6.egg/anymeta/api/base.py", line 210, in _processPage raise AnyMetaException(err['code'], err['msg']) anymeta.api.base.AnyMetaException: method-not-available: The method anymeta.thing.dump is not available
If so this means that your AnyMeta site does not have the ThingDump module installed. To enable it your site administrator will need to go to http://yoursite/admintools/index/ModuleLoader and install the module. Then you should see your commands work and the anymeta.thing.dump method show up in the /services/rest documentation.
Perhaps more useful than the image I dumped is to dump my information:
>>> api.anymeta.thing.dump(id=76)
{u'create_date': u'2010-07-21 14:15:43', u'axo_section': u'content', u'text': {u'body': u'', u'redirect_uri': u'', u'title_short': u'', u'subtitle': u'', u'language': u'en', u'title': u'Peter Robinett', u'chapeau': u'', u'label': [], u'intro': u''}, u'share': u'1', u'is_thing': 1, u'trust': {u'edit': {u'predicate': None, u'level': u'ME'}, u'react': {u'predicate': None, u'level': u'MEMBERS'}, u'link': {u'predicate': u'KNOWS_OF', u'level': u'PREDICATE'}, u'view': {u'predicate': u'false', u'level': u'EVERYONE'}}, u'modifier_id': u'http://173.45.230.55/id/76', u'category': [], u'authoritative': u'1', u'symbolic_name': u'', u'pubstate': u'1', u'masked': u'0', u'props': [], u'type': {u'symbolic': {u'1': u'NOMAIL'}}, u'owner_id': u'http://173.45.230.55/id/76', u'pub_date_end': u'9999-01-01 00:00:00', u'views': u'0', u'retirement_date': u'9999-01-01 00:00:00', u'auth': {u'confirm_logons': u'0', u'enabled': u'1', u'email': u'peter@bubblefoundry.com', u'confirm': u''}, u'uri_abs': u'http://173.45.230.55/person/76/en', u'lang': {u'en': {u'body': u'', u'redirect_uri': u'', u'title_short': u'', u'subtitle': u'', u'language': u'en', u'title': u'Peter Robinett', u'chapeau': u'', u'label': [], u'intro': u''}}, u'kind': u'PERSON', u'name': {u'full': u'Peter Robinett'}, u'pub_date_start': u'1970-01-01 00:00:00', u'uri': u'/person/76/en', u'edge': [{u'kind': u'TYPE', u'symbolic_name': u'NOMAIL', u'edge': {u'predicate': u'HAS_TYPE', u'create_date': u'2010-07-21 14:15:46', u'uuid': u'a35c66cd-e622-102d-a1f5-4040ad2de637', u'authoritative': u'1', u'object_id': u'http://mediamatic.nl/ns/anymeta/2008/type/nomail', u'predicate_id': u'http://www.w3.org/2000/01/rdf-schema#Class', u'creator_id': u'http://173.45.230.55/id/76', u'object_symbolic_name': u'NOMAIL', u'pending_import': u'0', u'modify_date': u'2010-07-21 14:15:46', u'object_kind': u'TYPE', u'data': None, u'order': u'9999', u'modifier_id': u'http://173.45.230.55/id/76', u'owner_id': u'http://173.45.230.55/id/76'}}], u'modify_date': u'2010-07-21 14:15:46', u'activity': u'0', u'axo': u'persons', u'resource_uri': u'http://173.45.230.55/id/76'}
>>> api.anymeta.user.info()
{u'auth_info': {u'usa_email': u'peter@bubblefoundry.com', u'usa_modify_date': u'2010-07-21 14:15:44', u'usa_logon_date': u'2010-09-15 08:47:51', u'usa_axo': u'public', u'usa_access_date': u'2010-09-15 08:47:51', u'usa_enabled': u'1', u'usa_axo_section': u'content', u'usa_email_sha1': u'2a27a3eec2c03b53e0861563dcf523dc585282a1', u'usa_confirm_logons': u'0', u'usa_create_date': u'2010-07-21 14:15:44', u'usa_id_ref': u'76', u'usa_confirm': u'', u'usa_prev_logon_date': u'2010-09-15 08:47:51'}, u'preferences': [], u'title': u'Peter Robinett', u'id': u'76', u'axo': [u'content', u'public'], u'aro': [{u'id': u'13', u'name': u'members'}]}
I’ve also included the anymeta.user.info output so that you can see how much more information anymeta.thing.dump returns. Particularly interesting is how dictionaries are nested:
u'text': {u'body': u'', u'redirect_uri': u'', u'title_short': u'', u'subtitle': u'', u'language': u'en', u'title': u'Peter Robinett', u'chapeau': u'', u'label': [], u'intro': u''} |
This nesting needs to be preserved when creating a new Thing, while the same names must be used. So here’s how we can create a new, generic Thing:
>>> api.anymeta.thing.insert(data={'text': {'title': 'Test Thing'}})
Traceback (most recent call last):
File "", line 1, in
File "/Library/Python/2.6/site-packages/AnyMetaAPI-1.22-py2.6.egg/anymeta/api/base.py", line 21, in __call__
return self.api.doMethod(self.part, kwargs)
File "/Library/Python/2.6/site-packages/AnyMetaAPI-1.22-py2.6.egg/anymeta/api/base.py", line 161, in doMethod
return self._getPage(http_method, str(url), postdata, headers, format)
File "/Library/Python/2.6/site-packages/AnyMetaAPI-1.22-py2.6.egg/anymeta/api/base.py", line 226, in _getPage_httplib
return self._processPage(page, format)
File "/Library/Python/2.6/site-packages/AnyMetaAPI-1.22-py2.6.egg/anymeta/api/base.py", line 210, in _processPage
raise AnyMetaException(err['code'], err['msg'])
anymeta.api.base.AnyMetaException: 0: Not allowed to insert a new UNKNOWN into axo public
Oops, it didn’t like that! The problem is that I didn’t specify the Kind of my Thing. What what Kinds are there? This, I’m afraid is still somewhat of a mystery to me. Spelunking through the AnyMeta source code (something probably not possible for you), I have found the following Kinds: ARTICLE, ATTACHMENT2, ARTEFACT, NOTE, SET, PERSON3, LOCATION, INSTITUTION, LISTPUBLISH, LISTEDIT, LANGUAGE, TAG, KEYWORD, THEME, TYPE, ROLE, TPLSCOMP, TPLPAGE, TOPIC, and of course UNKNOWN. Let’s just make our Thing an ARTICLE for now:
>>> api.anymeta.thing.insert(data={'text': {'title': 'Test Thing'}, 'kind': 'ARTICLE'})
{u'rsc_id': theID, u'edit_uri': u'http://yoursite/edit/theID', u'rsc_uri': u'http://yoursite/id/theID'}
If you visit the URL corresponding to the rsc_uri key you should see a blank AnyMeta page (plus the various navigation elements and such) with ‘Test Thing’ in large type at the top. Cool, we created a Thing using the API! Now let’s update our thing, using the id in the rsc_id value (again with the inconsistent names of id values!), to add a body (i.e. article text):
>>> api.anymeta.thing.update(id=202, data={'text': {'body': 'This is some body text.'}})
Traceback (most recent call last):
File "", line 1, in
File "/Library/Python/2.6/site-packages/AnyMetaAPI-1.22-py2.6.egg/anymeta/api/base.py", line 21, in __call__
return self.api.doMethod(self.part, kwargs)
File "/Library/Python/2.6/site-packages/AnyMetaAPI-1.22-py2.6.egg/anymeta/api/base.py", line 161, in doMethod
return self._getPage(http_method, str(url), postdata, headers, format)
File "/Library/Python/2.6/site-packages/AnyMetaAPI-1.22-py2.6.egg/anymeta/api/base.py", line 226, in _getPage_httplib
return self._processPage(page, format)
File "/Library/Python/2.6/site-packages/AnyMetaAPI-1.22-py2.6.egg/anymeta/api/base.py", line 210, in _processPage
raise AnyMetaException(err['code'], err['msg'])
anymeta.api.base.AnyMetaException: 1: Please supply a thing_id
>>> api.anymeta.thing.update(thing_id=202, data={'text': {'body': 'This is some body text.'}})
[]
See my mistake and why inconsistent id names annoy the heck out me? Anyway, if you refresh the url you just visited you should now see our new text underneath the title. That empty list ([]) returned by anymeta.thing.update unfortunately indicates that our request succeeded. If I had my way you’d get a nice dictionary with fields from the Thing relevant to its Kind, at least including the fields we updated, to indicate success and to make subsequent queries easier. Oh well.
Finally, let’s try working with Edges. You may have noticed on our Thing page that it says that it was created by whatever authenticated account you’re using with any-registry. That’s great, but let’s add an actual authorial relationship between me and this thing. This relationship will be reflected many pages throughout the page and site, even to the point of AnyMeta putting meta tags in the HTML indicating that I’m the author. So, we can mark me as the AUTHOR of the Thing like so:
>>> api.anymeta.edge.add(id=202, object=76, predicate='AUTHOR')
{u'predicate': u'AUTHOR', u'date_start': None, u'title': u'Peter Robinett', u'edg_id': u'16', u'date_end': None, u'object': u'76', u'title_short': u'', u'value': None, u'predicate_id': u'7', u'kind': u'PERSON', u'order': u'9999', u'subject': u'202'}
Notice that we’re marking the authorship from the ARTICLE, with Thing id 202, to the PERSON Thing with id 76 using the AUTHOR predicate. If you reverse the order of the ids so that id=76 and object=202 then the article would be the author of me and we wouldn’t want that! anymeta.edge.add gives us some reasonable return values and we can use the new edg_id (I didn’t say anything, honest! =) to update or remove the edge in the future. You should now
Like with Kinds there’s no readily available list of Predicates. Here are the ones I’ve dug up: ABOUT, ACTOR, AUTHOR, BLOCKS, CHILD, DOCUMENT, EXTRACONTEXT, FIGURE, FRIEND, HAS_TYPE, ICON, INTEREST, KIN, KNOWS, KNOWS_OF, LIVED, LOCATED_IN, ORGANISED_BY, PRESENTED_AT, PUBLISHED_BY, RELATED, RESOURCE, SETMEMBER, SETMOTHER, SIBLING, SPOUSE, SUBPROPERTY_OF, TAKEN_AT, VISITOR, WORKS_FOR. It is my understanding the Predicates (and Kinds) can vary from AnyMeta install to install depending on the domain model used, so you may not have the exact same Predicates as the ones I just listed, though I would imagine that you can rely on having Predicates like AUTHOR, LOCATED_IN, and TAKEN_AT.
In this article I’ve walked you through setting up AnyMetaAPI, connecting to an AnyMeta site, and doing some basic manipulations with the API. I’m still learning the ins and outs of the API, so there may be mistakes here or better ways to do things. However, I hope this has been useful and if you have any questions after reading this please ask them in the comments section.
I made this a month or two ago but I forgot to mention it here: Jacob Biljnai of Tumblr was complaining on Twitter that there wasn’t a Javascript-based image resizing API so I made him one using App English, Imajs. Enjoy.
I’m using Twisted for a project and the default sudo easy_install twisted was giving me errors. It turns out that my default Python install, via Fink, was built for the i386 architecture while Twisted was trying to link to its libraries using the x86_64 architecture, for which it does not have libraries built. sudo ARCHFLAGS='-arch i386' easy_install twisted did the trick.
Last night I discovered that Scala’s RichString has a format method quite similar to Python’s. However, in Python I’m used to using the % operator and I wondered if I could use the same syntax in Scala. With a little library pimping and help from my friends on Stack Overflow, I can:
class BFString(s: String) { def %(in: Any*) = s.format(in: _*) } implicit def add_%(s: String) = new BFString(s) |
scala> "The %s costs $%d." % ("dog", 255)
res2: String = The dog costs $255.
Pretty cool!
My Apple iPhone Developer account expires in 56 days, so I wanted to know what expiration date I should mark in my calendar. Here’s how I calculated it from the Python console:
>>> import datetime
>>> datetime.date.today() + datetime.timedelta(days=56)
datetime.date(2009, 11, 8)