# Editor agnostic editing

Here are two more utility scripts I wrote to help me blog from any editor. I’m finally making a serious effort to break away from TextMate, and to do that I need some commands to take the place of what I’m used to using in the Blogging Bundle. A few days ago, I described the script I’ve been using for publishing posts; tonight I’ll describe the scripts that download posts for editing.

These scripts, like the one for publishing, are written to work from the command line using standard in and standard out. I’m writing them this way for maximum flexibility—by keeping them simple and not including any editor-specific code, I can use them with any text editor. Ultimately, to avoid jumping to the Terminal and back, I’ll wrap these scripts in code that is specific to an editor, but I want the core to be editor agnostic.

The first script, recent-posts, returns a list of the most recent posts to ANIAT. It can be called from the command line with the number of posts to list, e.g.,

recent-posts 5


If it’s called without an argument, it will list the ten most recent.

The output of recent-posts looks like this:

Roll with the changes           Fri 08/17   1918
Blogging from stdin             Wed 08/15   1913
App dot not                     Tue 08/14   1896
Don’t teach your submarine to   Sun 08/12   1895
A few tweet archive utilities   Wed 08/08   1893
Buck-U-Uppo                     Wed 08/08   1892
Afghanistan, July 2012          Wed 08/08   1891
The Safari 6 URL crisis         Wed 08/01   1890
Small fonts and lost memories   Tue 07/31   1889
Some Safari 6 stuff             Mon 07/30   1888


The first column gives the titles (truncated to the first 30 characters), the second gives the day and date of the post, and the third gives the WordPress ID number of the post. We’ll use this number later to retrieve a post for editing.

Here’s the code for recent-posts:

python:
1:  #!/usr/bin/python
2:
3:  import xmlrpclib
4:  import sys
5:  import os
6:  from datetime import datetime
7:  import pytz
8:
9:  # The number of posts to return.
10:  postCount = 10
11:  try:
12:    postCount = int(sys.argv[1])
13:  except:
14:    pass
15:
16:  # Blog parameters (url, user, pw) are stored in ~/.blogrc.
17:  # One parameter per line, with name and value separated by colon-space.
18:  p = {}
19:  with open(os.environ['HOME'] + '/.blogrc') as bloginfo:
21:      k, v = line.split(': ')
22:      p[k] = v.strip()
23:
24:  # Time zones. WP is trustworthy only in UTC.
25:  utc = pytz.utc
26:  myTZ = pytz.timezone('US/Central')
27:
28:  # Connect.
29:  blog = xmlrpclib.Server(p['url'])
30:
31:  # Return a list of post IDs and titles.
32:  posts = blog.metaWeblog.getRecentPosts(0, p['user'], p['pw'], postCount)
33:  for p in posts:
34:    dt = datetime.strptime(p['date_created_gmt'].value, "%Y%m%dT%H:%M:%S")
35:    dt = utc.localize(dt).astimezone(myTZ)
36:    s = u'{0:<30}  {1:%a %m/%d}  {2:>5}'.format(p['title'][:30], dt, p['postid'])
37:    print s.encode('utf8')


Lines 10-14 handle the argument, if given. Note that the script will run even if a nonsensical argument is given, like

recent-posts fred


If the argument can’t be interpreted as an integer, it’ll just use the default value of 10.

Lines 18-22 get the URL of the XMLRPC handler and my username and password from a file in my home directory. The format of the file is explained in my earlier post. The credentials are used to log into the blog server on Lines 29 and 32.

Lines 32-37 do the real work of the script. metaWeblog.getRecentPosts returns a list of dictionaries. Each item in the list represents a post, and the list is in reverse chronological order.

One aspect of the script that’s new to me is the use of the format command. I’ve been using Python’s old percent-encoding method for ages and just never got around to learning the new system. I decided it was time to start.

The next script, get-post, takes as its argument the ID number of a post and returns the source of that post, with the meta-information laid out in header lines as described in my post of a few days ago. If, for example, I wanted to edit that post, I issue the command get-post 1913 and either pipe the output to my editor (or pbcopy for later pasting) or redirect it to a file. So if I want to edit the post in BBEdit,

get-post 1913 | bbedit


would retrieve the post and open a new BBEdit window with it. Not quite as handy as the Blogging Bundle, but pretty accommodating, as I could also do

get-post 1913 | subl


to open the post in a new Sublime Text window or

get-post 1913 | choc


to open it in Chocolat.

Here’s the code for get-post:

python:
1:  #!/usr/bin/python
2:
3:  import xmlrpclib
4:  import sys
5:  import os
6:  from datetime import datetime
7:  import pytz
8:
9:  # Blog parameters (url, user, pw) are stored in ~/.blogrc.
10:  # One parameter per line, with name and value separated by colon-space.
11:  p = {}
12:  with open(os.environ['HOME'] + '/.blogrc') as bloginfo:
14:      k, v = line.split(': ')
15:      p[k] = v.strip()
16:
17:  # The header fields and their metaWeblog synonyms.
18:  hFields = [ 'Title', 'Keywords', 'Date', 'Post',
20:  wpFields = [ 'title', 'mt_keywords', 'date_created_gmt',  'postid',
22:  h2wp = dict(zip(hFields, wpFields))
23:
24:  # Get the post ID from the command line.
25:  try:
26:    postID = int(sys.argv[1])
27:  except:
28:    sys.exit()
29:
30:  # Time zones. WP is trustworthy only in UTC.
31:  utc = pytz.utc
32:  myTZ = pytz.timezone('US/Central')
33:
34:  # Connect.
35:  blog = xmlrpclib.Server(p['url'])
36:
37:  # Return the post as text in header/body format for possible editing.
38:  post = blog.metaWeblog.getPost(postID, p['user'], p['pw'])
40:  for f in hFields:
41:    if f == 'Date':
42:      # Change the date from UTC to local and from DateTime to string.
43:      dt = datetime.strptime(post[h2wp[f]].value, "%Y%m%dT%H:%M:%S")
44:      dt = utc.localize(dt).astimezone(myTZ)
45:      header += "%s: %s\n" % (f, dt.strftime("%Y-%m-%d %H:%M:%S"))
46:    else:
47:      header += "%s: %s\n" % (f, post[h2wp[f]])
48:

1. One nice “trick” you can do with format is to treat it like a mini-function. Say format_output = u'{0:<30} {1:%a %m/%d} {2:>5}'.format in the header-ish part of the file and then change lines 36–7 to return format_output(p['title'][:30], dt, p['postid']).encode("utf-8"). In webdev terms, you can think of it as a way of separating presentation from business logic.