Simplified result parsing code and fixed sz() not working with Python versions that fixed the '\000' bug.
============================ Scripting Ecco using EccoDDE ============================ EccoDDE is a very thin wrapper over the DDE API of the Ecco Pro personal information manager for Windows. Unlike other Python interfaces to Ecco, it does not provide any higher-level objects to represent items, folders, etc., but instead permits you to create whatever higher-level objects suit your own particular application. Also, EccoDDE uses the original Ecco API names for its methods, so that you can use the Ecco API reference as a rough guide to EccoDDE. Some methods have enhanced functionality that you can access by using different argument types, but even these are nearly always just exposing capabilities of the underlying Ecco API, rather than doing any Python-specific wrapping. 48 of Ecco's 49 API calls are implemented. (The 49th, ``AddFileToMenu``, does not appear to have been documented anywhere on the 'net.) The main value-add that EccoDDE provides over writing your own ad-hoc interface is robustness. EccoDDE can transparently launch Ecco if it's not started, and it avoids many subtle quoting and line-termination problems that you'd run into when writing an interface from scratch. EccoDDE also has an automated test suite, so that any future additions to the library won't break current functionality. This library requires the PyWin32 package, but does not automatically install it, due to it not being compatible with easy_install at this time. You must manually install PyWin32 before using EccoDDE. For complete EccoDDE documentation, please consult the `EccoDDE developer's guide`_. Questions, comments, and bug reports for this package should be directed to the `PEAK mailing list`_. .. _Trellis: http://pypi.python.org/pypi/Trellis .. _Trellis tutorial: http://peak.telecommunity.com/DevCenter/Trellis .. _EccoDDE developer's guide: http://peak.telecommunity.com/DevCenter/EccoDDE#toc .. _PEAK mailing list: http://www.eby-sarna.com/mailman/listinfo/peak/ .. _toc: .. contents:: **Table of Contents** ----------------- Developer's Guide ----------------- To talk to Ecco, you will use an ``EccoDDE`` instance:: >>> from ecco_dde import EccoDDE >>> api = EccoDDE() The ``EccoDDE`` constructor accepts the following keyword-only arguments, which are used only if an initial attempt to contact Ecco fails: filename The filename to launch (with ``os.startfile()``) to run Ecco. If ``None`` or not supplied, the Windows registry will be consulted to find the shell-open command registered for Ecco files. retries The number of times to try connecting to Ecco after attempting to launch it. (10 by default) sleep The number of seconds to sleep between connection attempts (1 by default) Note, too, that creating an ``EccoDDE`` instance does not immediately launch or connect to Ecco. You can explicitly call the ``open()`` method, if you like, but it will also be called automatically whenever necessary. The ``close()`` method can be used to shut down the connection when it's not in use. If you use an instance after closing it, it will automatically be re-opened, which means you can (and probably should) close the connection when you won't be using it for a while. Working With Files and Sessions =============================== The ``close_all()`` method closes all currently-open files:: >>> api.close_all() # close any files currently open in Ecco >>> api.GetOpenFiles() [] ``NewFile()`` creats a new, untitled file, returning a session ID:: >>> session = api.NewFile() Which then will be visible in ``GetOpenFiles()`` (a list of the active session IDs), and as the ``GetCurrentFile()`` (which returns the active session ID):: >>> api.GetOpenFiles() == [session] True >>> api.GetCurrentFile() == session True The newly created file will be named '<Untitled>':: >>> api.GetFileName(session) '<Untitled>' Until it is saved:: >>> from tempfile import mkdtemp >>> tmpdir = mkdtemp() >>> import os >>> testfile = os.path.join(tmpdir, 'testfile.eco') >>> os.path.exists(testfile) False >>> api.SaveFile(session, testfile) >>> os.path.exists(testfile) True >>> api.GetFileName(session) '...\\testfile.eco' Once a session has a filename, it can be saved without specifying the name:: >>> api.SaveFile(session) And the ``CloseFile()`` and ``OpenFile()`` APIs work much as you would expect:: >>> api.CloseFile(session) >>> session = api.OpenFile(testfile) >>> api.GetOpenFiles() == [session] True >>> api.GetCurrentFile() == session True And you can also use the ``ChangeFile()`` API to switch to a given session:: >>> session2 = api.NewFile() >>> session2 == api.GetCurrentFile() True >>> api.SaveFile(session2, os.path.join(tmpdir, 'test2.eco')) >>> api.ChangeFile(session) >>> session == api.GetCurrentFile() True >>> session2 == api.GetCurrentFile() False >>> api.ChangeFile(session2) >>> session == api.GetCurrentFile() False >>> session2 == api.GetCurrentFile() True Note, by the way, that you can only close or save a file if it is the current session:: >>> api.SaveFile(session) Traceback (most recent call last): ... StateError: Attempt to close or save inactive session >>> api.CloseFile(session) Traceback (most recent call last): ... StateError: Attempt to close or save inactive session >>> api.CloseFile(session2) Working With Folders ==================== Listing and Looking Up Folders ------------------------------ The ``GetFolderOutline()`` method returns a list of ``(depth, id)`` tuples describing the folder outline of the current Ecco file, while the ``GetFolderName()`` and ``GetFolderType()`` methods return the name or type for a given folder ID:: >>> for folder, depth in api.GetFolderOutline(): ... print "%-30s %02d" % ( ... ' '*depth+api.GetFolderName(folder),api.GetFolderType(folder) ... ) Ecco Folders 01 PhoneBook 01 Mr./Ms. 04 Job Title 04 Company 04 Address 1 - Business 04 Address 2 - Business 04 City - Business 04 State - Business 04 Zip - Business 04 Country - Business 04 Work # 04 Home # 04 Fax # 04 Cell # 04 Alt # 04 Address 1 - Home 04 Address 2 - Home 04 City - Home 04 State - Home 04 Zip - Home 04 Country - Home 04 Phone / Time Log 02 E-Mail 04 Appointments 02 Done 02 Start Dates 02 Due Dates 02 To-Do's 02 Search Results 01 New Columns 01 Net Location 04 Recurring Note Dates 02 The values returned by ``GetFolderType()`` are available as constants in the ``FolderType`` enumeration class:: >>> from ecco_dde import FolderType >>> dir(FolderType) ['CheckMark', 'Date', 'Number', 'PopUpList', 'Text', ...] >>> FolderType.CheckMark 1 Which makes it convenient to fetch a list of folder ids based on folder type, using the ``GetFoldersByType()`` method:: >>> date_folders = api.GetFoldersByType(FolderType.Date) Both ``GetFolderName()`` and ``GetFolderType()`` will return multiple values if their input is an iterable:: >>> for name in api.GetFolderName(date_folders): # accepts multiples ... print name Phone / Time Log Appointments Done Start Dates Due Dates To-Do's Recurring Note Dates >>> api.GetFolderType(date_folders) [2, 2, 2, 2, 2, 2, 2] You can also find the folders by name, using ``GetFoldersByName()``:: >>> api.GetFoldersByName('Appointments') [4] (Note that this method always returns a list of ids, since more than one folder can have the same name.) Creating And Managing Folders ----------------------------- The ``CreateFolder()`` API can be used to create a single folder:: >>> f1 = api.CreateFolder('Test Folder 1') By default, it's created as a checkmark folder:: >>> api.GetFolderType(f1) == FolderType.CheckMark True But you can also specify a type explicitly:: >>> popup = api.CreateFolder('A popup folder', FolderType.PopUpList) >>> api.GetFolderType(popup) == FolderType.PopUpList True ``CreateFolder()`` can also create multiple folders at once, using a dictionary mapping names to folder types:: >>> d = api.CreateFolder( ... {'folder 3':FolderType.Text, 'folder 4':FolderType.Date} ... ) And the return value is a dictionary mapping the created folder names to their folder ids:: >>> d {'folder 4': ..., 'folder 3': ...} >>> f3 = d['folder 3'] >>> f4 = d['folder 4'] >>> api.GetFolderName(f3) 'folder 3' >>> api.GetFolderType(f4)==FolderType.Date True You can also rename an existing folder using ``SetFolderName()``:: >>> api.SetFolderName(f4, 'A Date Folder') >>> api.GetFolderName(f4) 'A Date Folder' Pop-ups and Auto-assign Rules ----------------------------- You can retrieve a pop-up folder's values using ``GetPopupValues()``:: >>> api.GetPopupValues(popup) [] If you pass in an iterable of folder ids, you will get back a list of lists of pop-up values:: >>> api.GetPopupValues([popup]) [[]] At the moment, our example popup folder doesn't have any values. Creating or modifying an item with values for this folder will add some:: >>> i1 = api.CreateItem('Test 1', [(popup, 'Blue')]) >>> i2 = api.CreateItem('Test 2', [(popup, 'Red')]) >>> api.GetPopupValues(popup) ['Blue', 'Red'] And the added values will stay around even if we delete the items:: >>> api.RemoveItem(i1) >>> api.RemoveItem(i2) >>> api.GetPopupValues([popup]) [['Blue', 'Red']] You can query a folder's auto-assign rules (if any) using ``GetFolderAutoAssignRules()`` (which will only accept one folder id, btw):: >>> api.GetFolderAutoAssignRules(api.GetFoldersByName('Net Location')[0]) ['http:#'] By the way, there is no way to programmatically delete an existing folder, change its type, or add/change its auto-assignment rules. These actions can only be done through the Ecco UI. Working With Items ================== Creating and Inspecting Items ----------------------------- As we saw above, you can create items from text, and an optional sequence of ``(folderid,value)`` pairs, passed to ``CreateItem()``:: >>> an_item = api.CreateItem('An item') >>> another_item = api.CreateItem('Another item', [(popup, 'Red')]) You can find out an item's text, folders, or type using ``GetItemText()``, ``GetItemFolders()``, and ``GetItemType()`` respectively:: >>> api.GetItemText(an_item) 'An item' >>> api.GetItemFolders(another_item)==[popup] True >>> api.GetItemType(an_item) 1 Or you can pass in multiple item ids as a sequence, to get back a list of strings, item types, or lists of folder ids:: >>> both = [an_item, another_item] >>> api.GetItemText(both) ['An item', 'Another item'] >>> api.GetItemFolders(both)==[[], [popup]] True >>> api.GetItemType(both) [1, 1] By the way, there are item type constants declared in the ``ItemType`` class:: >>> from ecco_dde import ItemType >>> dir(ItemType) ['ItemText', 'OLE', ...] An item is of type ``ItemText`` if it's a normal text item, and ``OLE`` if it's an OLE item (e.g., one created using ``PasteOLEItem()``). Querying and Updating Items/Values ---------------------------------- The ``GetFolderItems()`` method returns a list of item ids for a given folder id:: >>> api.GetFolderItems(popup) == [another_item] True It can also accept optional extra arguments to specify sort and search options, as described in the Ecco API documents:: >>> api.GetFolderItems(popup, 'EQ', 'Red') == [another_item] True >>> api.GetFolderItems(popup, 'EQ', 'Blue') [] You can set and retrieve folder values using ``SetFolderValues()`` and ``GetFolderValues()``, using either individual item ids and folder ids, or sequences thereof:: >>> api.SetFolderValues(an_item, f1, 1) >>> api.GetFolderValues(an_item, f1) '1' >>> api.SetFolderValues(another_item, [f1,f3], [1,'some text']) >>> api.GetFolderValues(another_item, [f3, f1]) ['some text', '1'] >>> api.SetFolderValues( ... [an_item, another_item], f4, ['20010101', '20020202'] ... ) >>> api.GetFolderValues([an_item,another_item], f4) ['20010101', '20020202'] >>> api.SetFolderValues( ... [an_item, another_item], [popup, f3], ... [['Blue', 'text a'], ['Red', 'text b']] ... ) >>> api.GetFolderValues([an_item, another_item], [popup, f3]) [['Blue', 'text a'], ['Red', 'text b']] Now let's retrieve some items sorted by their item text (ascending and descending):: >>> api.GetFolderItems(f1, 'ia') == [an_item, another_item] True >>> api.GetFolderItems(f1, 'id') == [another_item, an_item] True You can also change items' text using ``SetItemText()``, either by passing in a single item id and text:: >>> api.SetItemText(an_item, 'A') >>> api.GetItemText(an_item) 'A' Or by passing a dictionary mapping item ids to the desired text:: >>> api.SetItemText({an_item:'1', another_item:'2'}) >>> api.GetItemText([an_item, another_item]) ['1', '2'] Item Hierarchy, Relocation, and Removal --------------------------------------- The ``GetItemParents()`` method returns a (possibly-empty) list of parent item ids for one or more items:: >>> api.GetItemParents(an_item) [] >>> api.GetItemParents([an_item, another_item]) [[], []] Hm, no parents so far, so let's rearrange the hierarchy using ``InsertItem()``. For that, we need to know the "anchor item" (a parent or sibling), and the item or items to be placed relative to it. We also need to specify an insertion level:: >>> from ecco_dde import InsertLevel >>> dir(InsertLevel) ['Indent', 'Outdent', 'Same', ...] The default level of ``Indent`` places the targeted items as the first child(ren) of the anchor item, while ``Outdent`` places them as siblings of the anchor item's parent. ``Same`` makes the items a sibling of the existing item. Let's begin by creating a new item, then make our existing items children of it:: >>> i3 = api.CreateItem('Item 3') >>> api.InsertItem(i3, [an_item, another_item]) >>> api.GetItemParents([an_item, another_item]) == [[i3], [i3]] True The ``GetItemSubs()`` method can be used to retrieve a sequence of ``(depth,id)`` pairs, describing the children of an item:: >>> api.GetItemSubs(i3) == [(1, another_item), (1, an_item)] True Notice that the items here are listed in reverse order from the order we added them. This is because the default ``InsertLevel.Indent`` mode inserts children at the head of the parent's list of children. By default, ``GetItemSubs()`` returns a list of all children, to any depth:: >>> api.InsertItem(an_item, another_item) >>> api.GetItemParents(another_item) == [i3, an_item] True >>> api.GetItemSubs(i3) == [(1, an_item), (2, another_item)] True But it can be given a depth argument in order to prune the returned tree:: >>> api.GetItemSubs(i3, 1) == [(1, an_item)] True Notice, by the way, that ``GetItemParents()`` returns parents in high-to-low order, i.e., first the top-level item id, and the item's immediate parent last. Also notice that ``InsertItem()`` might better be called "move item", since it relocates the item(s) to the specified location. You can detach an item from any existing parent using zero for the anchor id, and of course its children will move with it:: >>> api.InsertItem(0, an_item) >>> api.GetItemParents(another_item) == [an_item] True >>> api.GetItemSubs(i3) [] Using a depth of ``InsertLevel.Same``, we can now put ``i3`` after ``another_item``:: >>> api.InsertItem(another_item, i3, InsertLevel.Same) >>> api.GetItemSubs(an_item) == [(1, another_item), (1, i3)] True And let's add a couple more items so we can see how ``InsertLevel.Outdent`` works:: >>> i4 = api.CreateItem('Item 4') >>> i5 = api.CreateItem('Item 5') >>> api.InsertItem(another_item, i4) >>> api.InsertItem(i4, i5, InsertLevel.Outdent) >>> api.GetItemSubs(an_item) == [(1,another_item), (2,i4), (1,i5), (1,i3)] True As you can see, outdenting places an item in the next spot after the anchor item's parent. We can now delete some of our unneeded items with ``RemoveItem``, which works with either a single item id, or an iterable of them:: >>> api.RemoveItem(i4) >>> api.GetItemSubs(an_item) == [(1, another_item), (1, i5), (1, i3)] True >>> api.RemoveItem([i3,i5]) >>> api.GetItemSubs(an_item) == [(1, another_item)] True (Note, by the way, that ``RemoveItem`` will also delete an item's children, if any exist.) Working With Views ================== The ``GetViews()`` method returns a list of view IDs for the current file:: >>> api.GetViews() [2, 3] And the ``GetViewNames()`` method returns a list of names, given a list of view IDs:: >>> api.GetViewNames([2, 3]) ['Calendar', 'PhoneBook'] Or one name, if given a single view ID:: >>> api.GetViewNames(2) 'Calendar' Or a sequence of ``(name, view_id)`` pairs for all views in the current file, if not given an argument:: >>> api.GetViewNames() [('Calendar', 2), ('PhoneBook', 3)] You can create a new view by passing a name and a non-empty list of folders to the ``CreateView()`` method:: >>> view = api.CreateView('All Dates', date_folders) >>> api.GetViewNames() [('Calendar', 2), ('PhoneBook', 3), ('All Dates', 5)] Folders and TLIs ---------------- You can query a view's folders using ``GetViewFolders()``, either with a single view ID:: >>> api.GetViewFolders(view) == date_folders True or with a list of view IDs, to get a list of lists of folders for each view:: >>> api.GetViewFolders([view]) == [date_folders] True You can also add folders to a view (although you can't remove them):: >>> api.AddFolderToView(view, popup) >>> api.GetViewFolders(view) == date_folders + [popup] True You can add multiple folders by passing a sequence as the second argument, instead of a single folder id:: >>> view2 = api.CreateView('Another View', [f1]) >>> api.AddFolderToView(view2, [f1, f3]) >>> api.GetViewFolders(view2) == [f1, f3] True And the ``GetViewTLIs`` method returns a list of ``(folderid, itemids)`` pairs, giving you each folder in the view and its corresponding top-level items:: >>> api.GetViewTLIs(view2) == [(f1, [an_item]), (f3, [an_item])] True By the way, you can pass a sequence of views to ``GetViewFolders()``:: >>> api.GetViewFolders([view, view2]) == [ ... date_folders+[popup], [f1,f3] ... ] True Columns ------- By default, any non-checkmark folders added to a view programmatically will also be added to the view as columns, with the most-recently added folders first:: >>> api.GetViewColumns(view2) == [f3] True But you can also add columns explicitly, using ``AddColumnToView`` to specify one or many folders:: >>> api.AddColumnToView(view2, f1) >>> api.GetViewColumns(view2) == [f1, f3] True >>> api.AddColumnToView(view2, [f4, popup]) >>> api.GetViewColumns(view2) == [f4, popup, f1, f3] True And please note, by the way, that ``GetViewColumns()`` does NOT work with multiple view ids. In testing, Ecco honors the format of the API by returning multiple lists, but the lists are all empty except for the first one. So if you need the column information for multiple views, you'll need to do the looping yourself. Selecting, Composing, and Deleting Views ---------------------------------------- The ``ChangeView()`` method lets you select which view is displayed in Ecco:: >>> api.ChangeView(view) >>> api.ChangeView(view2) Unfortunately, there's no way to get the current view ID, and thus no way of knowing whether this actually worked. Similarly, there's no way to find out what split-screen views are active, so even though you can add and remove up to 3 additional ("composite") views on the current view, there's no way to see if it's working, either:: >>> api.AddCompView(2) # add the calendar >>> api.AddCompView(3) # and the phonebook >>> api.AddCompView(view) # and the other view... >>> api.RemoveCompView(2) # take off the calendar >>> api.RemoveCompView(3) # and the phonebook >>> api.RemoveCompView(view) # and the other view... The ``DeleteView()`` API removes a view from the current file:: >>> api.GetViewNames() [('Calendar', 2), ('PhoneBook', 3), ('All Dates', 5), ('Another View', 6)] >>> api.DeleteView(view) >>> api.GetViewNames() [('Calendar', 2), ('PhoneBook', 3), ('Another View', 6)] (Note that although the Ecco API docs claim that this function can be given multiple view IDs, in practice it crashes Ecco.) Miscellaneous APIs ================== ``GetVersion()`` ---------------- The ``GetVersion()`` method returns the Ecco API version number:: >>> api.GetVersion() [2, 8, 0] ``GetChanges()`` ---------------- The ``GetChanges()`` API returns a 3-tuple of ``(nextstamp, items, folders)``, where `nextstamp` is a timestamp to be passed back in the next time you call ``GetChanges()``, `items` are the item ids that were changed or created since the last call, and `folders` are the folder ids of folders that had items removed from them since the last call. Initially, you should pass in a timestamp of 0, to get the ball rolling:: >>> nextstamp, items, folders = api.GetChanges(0) This will basically return all changes since the file was created, and a timestamp you can use to call again. If no changes have occurred since that stamp, you'll get empty lists and the same timestamp again, the next time you call:: >>> api.GetChanges(nextstamp) == (nextstamp, [], []) True Before using this, please read the Ecco DDE API docs carefully regarding how the timestamp resolution works and when/why you'll get duplicate change notices. (This is not necessarily a suitable method for staying in sync "live" with an active Ecco file, because every time a change occurs, you will receive a list of every change that's occurred within a 1 hour timestamp resolution -- i.e., duplicating previous changes.) ``GetChanges()`` can optionally take a second argument, that lists the folders to be watched. Any items that aren't in those folders won't be included in the results, but the folders themselves will be listed in the `folders` return if items were removed from them during the relevant timeframe. OLE Copy/Paste -------------- The ``CopyOLEItem(item_id)`` method copies an OLE item to the clipboard. To work, the item must have a ``GetItemType()`` of ``ItemType.OLE``. ``PasteOLEItem(mode, optional_id, optional_data)`` pastes an OLE object from the Windows clipboard, either into an existing item, or creating a new item and returning its id (if `optional_id` is omitted or ``None``). The first argument is an ``OLEMode``, either ``Embed`` or ``Link``:: >>> from ecco_dde import OLEMode >>> dir(OLEMode) ['Embed', 'Link', ...] And the third argument, if supplied, should be a sequence of ``(folderid,val)`` pairs, that will be used to initialize or update the item. (Unfortunately, there isn't any straightforward way to demo/test these API calls in this document, as you must have an OLE object either in the clipboard or in an existing Ecco file. If you experience any problems using them, please let me know how to reproduce the problem. Thanks.) ``ShowPhoneBookItem()`` ----------------------- The ``ShowPhoneBookItem()`` method switches to the phonebook view and displays the specified item. If you pass a false value as the second argument, the specified item will be added to the current search results in the phonebook view. Otherwise, the search results are cleared first:: >>> pb, = api.GetFoldersByName('PhoneBook') >>> api.SetFolderValues([an_item, another_item], pb, [1,1]) >>> api.InsertItem(0, another_item) # make this a top-level item >>> api.ShowPhoneBookItem(an_item) # display an item... >>> api.ShowPhoneBookItem(another_item, False) # then add another Note, by the way, that the specified item must either be in the ``Phonebook`` folder, or have a parent that is. Otherwise, an error will be raised. ``GetSelection()`` ------------------ The ``GetSelection()`` method returns a ``[kind, ids]`` pair, where `ids` is a list of folder or item ids, and `kind` is a ``SelectionType`` constant indicating whether the ids are items or folders:: >>> from ecco_dde import SelectionType >>> dir(SelectionType) ['Folders', 'Items', 'Nothing', ...] In the previous section, we navigated to ``another_item``, so the current selection should reflect that:: >>> api.GetSelection() == [SelectionType.Items, [another_item]] True (NOTE: In my own testing, I have never gotten ``GetSelection()`` to work correctly when one or more folders are selected on their own. Instead, my copy of Ecco returns a 1 (``SelectionType.Items``) and omits the `ids` list. If this happens, EccoDDE adds an empty `ids` list so that ``GetSelection()`` will always return a 2-element sequence.) ``SetCalDate()`` ---------------- The ``SetCalDate()`` method lets you set the calendar to the given date, provided that you change to the calendar view, and use an appropriately formatted date:: >>> api.ChangeView(2) # show the calendar >>> api.SetCalDate('20080301') Date Conversion =============== If you need to convert a Python date or datetime value to an Ecco DDE string, use these functions:: >>> from ecco_dde import format_date, format_datetime >>> from datetime import datetime >>> dt = datetime(2008, 3, 31, 17, 53, 46) >>> format_date(dt) '20080331' >>> format_datetime(dt) '200803311753' These functions are not particularly bright, however, and will pass through anything you give them that doesn't have a ``strftime()`` method:: >>> format_date(27) # objects w/out strftime pass thru 27 >>> format_datetime(99) 99 Conclusion ========== This is just a cleanup section where we close all open Ecco files, close our DDE connection, and delete the temporary directory we used for saving test files. You probably don't want to do these things in your application, except maybe closing the DDE connection:: >>> api.close_all() # close all files open in Ecco >>> api.close() # close the DDE connection >>> from shutil import rmtree # and wipe out the temp dir. >>> rmtree(tmpdir)
cvs-admin@eby-sarna.com Powered by ViewCVS 1.0-dev |
ViewCVS and CVS Help |