[CalendarServer-changes] [2248] CalDAVClientLibrary/trunk

source_changes at macosforge.org source_changes at macosforge.org
Tue Mar 25 11:56:22 PDT 2008


Revision: 2248
          http://trac.macosforge.org/projects/calendarserver/changeset/2248
Author:   cdaboo at apple.com
Date:     2008-03-25 11:56:21 -0700 (Tue, 25 Mar 2008)

Log Message:
-----------
Initial import.

Added Paths:
-----------
    CalDAVClientLibrary/trunk/.project
    CalDAVClientLibrary/trunk/.pydevproject
    CalDAVClientLibrary/trunk/LICENSE
    CalDAVClientLibrary/trunk/README
    CalDAVClientLibrary/trunk/runadmin.py
    CalDAVClientLibrary/trunk/runshell.py
    CalDAVClientLibrary/trunk/setup.py
    CalDAVClientLibrary/trunk/src/
    CalDAVClientLibrary/trunk/src/__init__.py
    CalDAVClientLibrary/trunk/src/admin/
    CalDAVClientLibrary/trunk/src/admin/__init__.py
    CalDAVClientLibrary/trunk/src/admin/xmlaccounts/
    CalDAVClientLibrary/trunk/src/admin/xmlaccounts/__init__.py
    CalDAVClientLibrary/trunk/src/admin/xmlaccounts/commands/
    CalDAVClientLibrary/trunk/src/admin/xmlaccounts/commands/__init__.py
    CalDAVClientLibrary/trunk/src/admin/xmlaccounts/commands/addrecord.py
    CalDAVClientLibrary/trunk/src/admin/xmlaccounts/commands/changepassword.py
    CalDAVClientLibrary/trunk/src/admin/xmlaccounts/commands/command.py
    CalDAVClientLibrary/trunk/src/admin/xmlaccounts/commands/listrecords.py
    CalDAVClientLibrary/trunk/src/admin/xmlaccounts/commands/removerecord.py
    CalDAVClientLibrary/trunk/src/admin/xmlaccounts/directory.py
    CalDAVClientLibrary/trunk/src/admin/xmlaccounts/manage.py
    CalDAVClientLibrary/trunk/src/admin/xmlaccounts/record.py
    CalDAVClientLibrary/trunk/src/admin/xmlaccounts/recordtypes.py
    CalDAVClientLibrary/trunk/src/admin/xmlaccounts/tags.py
    CalDAVClientLibrary/trunk/src/admin/xmlaccounts/tests/
    CalDAVClientLibrary/trunk/src/admin/xmlaccounts/tests/__init__.py
    CalDAVClientLibrary/trunk/src/admin/xmlaccounts/tests/test_directory.py
    CalDAVClientLibrary/trunk/src/admin/xmlaccounts/tests/test_record.py
    CalDAVClientLibrary/trunk/src/admin/xmlaccounts/tests/utils.py
    CalDAVClientLibrary/trunk/src/browser/
    CalDAVClientLibrary/trunk/src/browser/__init__.py
    CalDAVClientLibrary/trunk/src/browser/baseshell.py
    CalDAVClientLibrary/trunk/src/browser/command.py
    CalDAVClientLibrary/trunk/src/browser/commands/
    CalDAVClientLibrary/trunk/src/browser/commands/__init__.py
    CalDAVClientLibrary/trunk/src/browser/commands/acl.py
    CalDAVClientLibrary/trunk/src/browser/commands/cat.py
    CalDAVClientLibrary/trunk/src/browser/commands/cd.py
    CalDAVClientLibrary/trunk/src/browser/commands/help.py
    CalDAVClientLibrary/trunk/src/browser/commands/history.py
    CalDAVClientLibrary/trunk/src/browser/commands/logging.py
    CalDAVClientLibrary/trunk/src/browser/commands/ls.py
    CalDAVClientLibrary/trunk/src/browser/commands/principal.py
    CalDAVClientLibrary/trunk/src/browser/commands/props.py
    CalDAVClientLibrary/trunk/src/browser/commands/proxies.py
    CalDAVClientLibrary/trunk/src/browser/commands/quit.py
    CalDAVClientLibrary/trunk/src/browser/commands/server.py
    CalDAVClientLibrary/trunk/src/browser/commands/whoami.py
    CalDAVClientLibrary/trunk/src/browser/shell.py
    CalDAVClientLibrary/trunk/src/browser/subshell.py
    CalDAVClientLibrary/trunk/src/browser/utils.py
    CalDAVClientLibrary/trunk/src/client/
    CalDAVClientLibrary/trunk/src/client/__init__.py
    CalDAVClientLibrary/trunk/src/client/account.py
    CalDAVClientLibrary/trunk/src/client/calendar.py
    CalDAVClientLibrary/trunk/src/client/calendaruseraddress.py
    CalDAVClientLibrary/trunk/src/client/clientsession.py
    CalDAVClientLibrary/trunk/src/client/fullclient.py
    CalDAVClientLibrary/trunk/src/client/principal.py
    CalDAVClientLibrary/trunk/src/client/simple.py
    CalDAVClientLibrary/trunk/src/protocol/
    CalDAVClientLibrary/trunk/src/protocol/__init__.py
    CalDAVClientLibrary/trunk/src/protocol/caldav/
    CalDAVClientLibrary/trunk/src/protocol/caldav/__init__.py
    CalDAVClientLibrary/trunk/src/protocol/caldav/definitions/
    CalDAVClientLibrary/trunk/src/protocol/caldav/definitions/__init__.py
    CalDAVClientLibrary/trunk/src/protocol/caldav/definitions/caldavxml.py
    CalDAVClientLibrary/trunk/src/protocol/caldav/definitions/headers.py
    CalDAVClientLibrary/trunk/src/protocol/caldav/definitions/methods.py
    CalDAVClientLibrary/trunk/src/protocol/caldav/makecalendar.py
    CalDAVClientLibrary/trunk/src/protocol/caldav/multiget.py
    CalDAVClientLibrary/trunk/src/protocol/caldav/tests/
    CalDAVClientLibrary/trunk/src/protocol/caldav/tests/__init__.py
    CalDAVClientLibrary/trunk/src/protocol/caldav/tests/test_makecalendar.py
    CalDAVClientLibrary/trunk/src/protocol/caldav/tests/test_multiget.py
    CalDAVClientLibrary/trunk/src/protocol/http/
    CalDAVClientLibrary/trunk/src/protocol/http/__init__.py
    CalDAVClientLibrary/trunk/src/protocol/http/authentication/
    CalDAVClientLibrary/trunk/src/protocol/http/authentication/__init__.py
    CalDAVClientLibrary/trunk/src/protocol/http/authentication/authenticator.py
    CalDAVClientLibrary/trunk/src/protocol/http/authentication/basic.py
    CalDAVClientLibrary/trunk/src/protocol/http/authentication/digest.py
    CalDAVClientLibrary/trunk/src/protocol/http/authentication/tests/
    CalDAVClientLibrary/trunk/src/protocol/http/authentication/tests/__init__.py
    CalDAVClientLibrary/trunk/src/protocol/http/authentication/tests/test_basic.py
    CalDAVClientLibrary/trunk/src/protocol/http/data/
    CalDAVClientLibrary/trunk/src/protocol/http/data/__init__.py
    CalDAVClientLibrary/trunk/src/protocol/http/data/data.py
    CalDAVClientLibrary/trunk/src/protocol/http/data/file.py
    CalDAVClientLibrary/trunk/src/protocol/http/data/string.py
    CalDAVClientLibrary/trunk/src/protocol/http/definitions/
    CalDAVClientLibrary/trunk/src/protocol/http/definitions/__init__.py
    CalDAVClientLibrary/trunk/src/protocol/http/definitions/headers.py
    CalDAVClientLibrary/trunk/src/protocol/http/definitions/methods.py
    CalDAVClientLibrary/trunk/src/protocol/http/definitions/statuscodes.py
    CalDAVClientLibrary/trunk/src/protocol/http/requestresponse.py
    CalDAVClientLibrary/trunk/src/protocol/http/session.py
    CalDAVClientLibrary/trunk/src/protocol/http/tests/
    CalDAVClientLibrary/trunk/src/protocol/http/tests/__init__.py
    CalDAVClientLibrary/trunk/src/protocol/http/tests/test_requestresponse.py
    CalDAVClientLibrary/trunk/src/protocol/http/tests/test_util.py
    CalDAVClientLibrary/trunk/src/protocol/http/util.py
    CalDAVClientLibrary/trunk/src/protocol/tests/
    CalDAVClientLibrary/trunk/src/protocol/tests/__init__.py
    CalDAVClientLibrary/trunk/src/protocol/tests/test_url.py
    CalDAVClientLibrary/trunk/src/protocol/url.py
    CalDAVClientLibrary/trunk/src/protocol/utils/
    CalDAVClientLibrary/trunk/src/protocol/utils/__init__.py
    CalDAVClientLibrary/trunk/src/protocol/utils/xmlhelpers.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/
    CalDAVClientLibrary/trunk/src/protocol/webdav/__init__.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/ace.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/acl.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/copy.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/copymovebase.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/definitions/
    CalDAVClientLibrary/trunk/src/protocol/webdav/definitions/__init__.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/definitions/davxml.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/definitions/headers.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/definitions/methods.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/definitions/statuscodes.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/delete.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/get.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/getbase.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/head.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/lock.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/makecollection.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/move.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/multiresponseparser.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/options.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/principalmatch.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/propall.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/propfind.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/propfindbase.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/propfindparser.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/propnames.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/proppatch.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/put.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/report.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/requestresponse.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/session.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/tests/
    CalDAVClientLibrary/trunk/src/protocol/webdav/tests/__init__.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_ace.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_acl.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_copy.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_delete.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_get.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_head.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_lock.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_move.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_options.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_principalmatch.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_propfind.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_propfindparser.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_propnames.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_put.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_report.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_template.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_unlock.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/unlock.py
    CalDAVClientLibrary/trunk/src/protocol/webdav/xmlresponseparser.py
    CalDAVClientLibrary/trunk/src/ui/
    CalDAVClientLibrary/trunk/src/ui/WebDAVBrowser.nib/
    CalDAVClientLibrary/trunk/src/ui/WebDAVBrowser.nib/classes.nib
    CalDAVClientLibrary/trunk/src/ui/WebDAVBrowser.nib/info.nib
    CalDAVClientLibrary/trunk/src/ui/WebDAVBrowser.nib/keyedobjects.nib
    CalDAVClientLibrary/trunk/src/ui/WebDAVBrowser.py
    CalDAVClientLibrary/trunk/src/ui/__init__.py
    CalDAVClientLibrary/trunk/src/ui/resource.py
    CalDAVClientLibrary/trunk/src/ui/session.py

Added: CalDAVClientLibrary/trunk/.project
===================================================================
--- CalDAVClientLibrary/trunk/.project	                        (rev 0)
+++ CalDAVClientLibrary/trunk/.project	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>CalDAVClientLibrary</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>org.python.pydev.PyDevBuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>org.python.pydev.pythonNature</nature>
+	</natures>
+</projectDescription>

Added: CalDAVClientLibrary/trunk/.pydevproject
===================================================================
--- CalDAVClientLibrary/trunk/.pydevproject	                        (rev 0)
+++ CalDAVClientLibrary/trunk/.pydevproject	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<?eclipse-pydev version="1.0"?>
+
+<pydev_project>
+<pydev_pathproperty name="org.python.pydev.PROJECT_SOURCE_PATH">
+<path>/CalDAVClientLibrary/src</path>
+</pydev_pathproperty>
+<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python 2.5</pydev_property>
+<pydev_pathproperty name="org.python.pydev.PROJECT_EXTERNAL_SOURCE_PATH">
+<path>/Volumes/Data/Users/cyrusdaboo/Documents/Development/Apple/eclipse/PyCalendar/src</path>
+<path>/System/Library/Frameworks/Python.framework/Versions/2.5/Extras/lib/python/PyObjC</path>
+</pydev_pathproperty>
+</pydev_project>

Added: CalDAVClientLibrary/trunk/LICENSE
===================================================================
--- CalDAVClientLibrary/trunk/LICENSE	                        (rev 0)
+++ CalDAVClientLibrary/trunk/LICENSE	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

Added: CalDAVClientLibrary/trunk/README
===================================================================
--- CalDAVClientLibrary/trunk/README	                        (rev 0)
+++ CalDAVClientLibrary/trunk/README	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,105 @@
+README for CalDAVClientLibrary
+
+INTRODUCTION
+
+CalDAVCLientLibrary is a Python library and tool for CalDAV. It is
+comprised of four main modules:
+
+protocol: this implements an HTTP/WebDAV/CalDAV protocol stack, using
+httplib to communicate with the server.
+
+client: this implements a CalDAV client session, with higher level
+functionality than the protocol module (e.g. 'get properties on resource
+X and return as a dict'). There is a higher level abstraction using an
+object model to repesent a session, accounts, principals and calendars
+as objects.
+
+browser: this implements a shell-like browser that lets you interact
+with the CalDAV server directly via protocol. You can 'cd' to different
+parts of the repository, 'ls' to list a collection, 'cat' to read
+resource data, 'props' to get properties. Then there are some higher
+level functions such as 'proxies' which let you manage (read and edit)
+the proxy list for a principal, and 'acl' which lets you manage ACLs
+directly on resources. For those, the tool takes care of mapping from
+principal paths to principal URLs etc. Help is provided for each command
+(type '?'). It is easily extensible by adding new commands.
+
+ui: a PyObjC application with a WebDAV browser GUI. This provides a file
+system like browser that allows properties and the data for a selected
+WebDAV resource do be displayed.
+
+admin: a user account administration tool. Currently works only with the
+XML file directory account.
+
+*** NB This package requires Python 2.5. ***
+
+The runshell.py script will launch the command line browser shell.
+The runadmin.py script will run the XML directory admin tool.
+
+
+SHELL TOOL
+
+-- COMMAND LINE OPTIONS
+
+    Usage: runshell [OPTIONS]
+    
+    Options:
+    
+    -l              start with HTTP logging on.
+    
+    --server=HOST   url of the server include http/https scheme and
+                    port [REQUIRED].
+                    
+    --user=USER     user name to login as - will be prompted if not
+                    present [OPTIONAL].
+                    
+    --pswd=PSWD     password for user - will be prompted if not
+                    present [OPTIONAL].
+
+-- QUICKSTART - COMMANDLINE
+
+To browse a calendar server on the local machine:
+
+    ./runshell.py --server http://localhost:8008
+    
+or, for SSL:
+
+    ./runshell.py --server https://localhost:8443
+
+Then type '?' followed by return to see the list of available commands.
+
+
+UI TOOL
+
+-- QUICKSTART - GUI
+
+Build the GUI app using:
+
+    python setup.py py2app
+
+The application will be placed in the 'dist' directory. Double-click
+that to launch it. One it it running, click the 'Server' toolbar button
+and specify a server, user id and password.
+
+The app will then display the top-level of the server resource hierarchy
+in the browser pane on the left. You can click and navigate through the
+resources via that pane (the 'Browser' toolbar buttons determine whether
+the browser uses a column or list view).
+
+When a resource is selected in the browser pane, its properties or data
+are display in the right hand pane. You can toggle between viewing
+properties or data by using the 'View' toolbar buttons.
+
+
+ADMIN TOOL
+
+-- QUICKSTART - COMMANDLINE
+
+To run the tool and see the list of available commands:
+
+    ./runadmin.py --help
+    
+
+TO DO
+
+Lots of error handling and documentation.

Added: CalDAVClientLibrary/trunk/runadmin.py
===================================================================
--- CalDAVClientLibrary/trunk/runadmin.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/runadmin.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,32 @@
+#!/usr/bin/env python
+
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+#
+# Runs the CalDAVTester test suite ensuring that required packages are available.
+#
+
+if __name__ == "__main__":
+
+    import os
+    import sys
+
+    cwd = os.getcwd()
+    sys.path.append(os.path.join(cwd, "src"))
+
+    from admin.xmlaccounts import manage
+    manage.runit()


Property changes on: CalDAVClientLibrary/trunk/runadmin.py
___________________________________________________________________
Name: svn:executable
   + *

Added: CalDAVClientLibrary/trunk/runshell.py
===================================================================
--- CalDAVClientLibrary/trunk/runshell.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/runshell.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,32 @@
+#!/usr/bin/env python
+
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+#
+# Runs the CalDAVTester test suite ensuring that required packages are available.
+#
+
+if __name__ == "__main__":
+
+    import os
+    import sys
+
+    cwd = os.getcwd()
+    sys.path.append(os.path.join(cwd, "src"))
+
+    from src.browser import shell
+    shell.runit()


Property changes on: CalDAVClientLibrary/trunk/runshell.py
___________________________________________________________________
Name: svn:executable
   + *

Added: CalDAVClientLibrary/trunk/setup.py
===================================================================
--- CalDAVClientLibrary/trunk/setup.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/setup.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,32 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Script for building the UI app (OS X only).
+
+Usage:
+    python setup.py py2app
+""" 
+
+from distutils.core import setup
+import py2app
+
+plist = dict(NSMainNibFile="WebDAVBrowser")
+setup(
+    app=["src/ui/WebDAVBrowser.py"],
+    data_files=["src/ui/WebDAVBrowser.nib", ],
+    options=dict(py2app=dict(plist=plist, includes=["urllib", "sha", "md5",], packages=["src/client", "src/protocol", "src/ui",])),
+)

Added: CalDAVClientLibrary/trunk/src/__init__.py
===================================================================
--- CalDAVClientLibrary/trunk/src/__init__.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/__init__.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,15 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##

Added: CalDAVClientLibrary/trunk/src/admin/__init__.py
===================================================================
--- CalDAVClientLibrary/trunk/src/admin/__init__.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/admin/__init__.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,16 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+

Added: CalDAVClientLibrary/trunk/src/admin/xmlaccounts/__init__.py
===================================================================
--- CalDAVClientLibrary/trunk/src/admin/xmlaccounts/__init__.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/admin/xmlaccounts/__init__.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,16 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+

Added: CalDAVClientLibrary/trunk/src/admin/xmlaccounts/commands/__init__.py
===================================================================
--- CalDAVClientLibrary/trunk/src/admin/xmlaccounts/commands/__init__.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/admin/xmlaccounts/commands/__init__.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,28 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from admin.xmlaccounts.commands.addrecord import AddRecord
+from admin.xmlaccounts.commands.changepassword import ChangePassword
+from admin.xmlaccounts.commands.listrecords import ListRecords
+from admin.xmlaccounts.commands.removerecord import RemoveRecord
+
+# Commands register themselves in this dict
+registered = {}
+
+registered[AddRecord.CMDNAME] = AddRecord
+registered[ChangePassword.CMDNAME] = ChangePassword
+registered[ListRecords.CMDNAME] = ListRecords
+registered[RemoveRecord.CMDNAME] = RemoveRecord

Added: CalDAVClientLibrary/trunk/src/admin/xmlaccounts/commands/addrecord.py
===================================================================
--- CalDAVClientLibrary/trunk/src/admin/xmlaccounts/commands/addrecord.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/admin/xmlaccounts/commands/addrecord.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,102 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from admin.xmlaccounts.commands.command import Command
+from admin.xmlaccounts.record import XMLRecord
+from uuid import uuid4
+from admin.xmlaccounts import recordtypes
+
+class AddRecord(Command):
+    
+    CMDNAME = "add"
+
+    def __init__(self):
+        super(AddRecord, self).__init__(self.CMDNAME, "Add a record of the specified type.")
+
+    def doCommand(self):
+        if self.doAdd():
+            return self.writeAccounts()
+        return 0
+    
+    def doAdd(self):
+        
+        # Prompt for each thing we need in the record
+        record = XMLRecord()
+        record.recordType = self.recordType
+        print "Enter the fields for the record (ctrl-D to stop at any time)"
+        try:
+            # uid
+            while True:
+                record.uid = raw_input("Id: ")
+                if not record.uid:
+                    print "A valid uid is required. Please try again."
+                elif self.directory.containsRecord(self.recordType, record.uid):
+                    print "Record uid: '%s' of type: '%s' does already exists in the directory." % (record.uid, self.recordType,)
+                else:
+                    break
+
+            # guid
+            while True:
+                record.guid = raw_input("GUID [leave empty to auto-generate]: ")
+                if record.guid and self.directory.containsGUID(record.guid):
+                    print "GUID: '%s' already used in the directory" % (record.guid,) 
+                else:
+                    break
+            
+            # password
+            record.password = self.promptPassword()
+
+            # name
+            record.name = raw_input("Name: ")
+            
+            # members
+            if self.recordType in (recordtypes.recordType_groups,):
+                record.members = self.getMemberList("Enter members of this group", "Member", "members")
+
+            # cuaddr
+            while True:
+                cuaddr = raw_input("Calendar user address [leave empty to stop adding addresses]: ")
+                if cuaddr:
+                    record.calendarUserAddresses.add(cuaddr)
+                else:
+                    break
+            
+            # auto-schedule
+            if self.recordType in (recordtypes.recordType_locations, recordtypes.recordType_resources,):
+                auto_schedule = raw_input("Turn on automatic scheduling [y/n]?: ")
+                if auto_schedule == "y":
+                    record.autoSchedule = True
+
+            # enabled for calendaring
+            if self.recordType in (recordtypes.recordType_users, recordtypes.recordType_groups,):
+                enable_calendars = raw_input("Create calendar account rather than access-only account [y/n]?: ")
+                if enable_calendars == "n":
+                    record.enabledForCalendaring = False
+
+            # proxies
+            if self.recordType in (recordtypes.recordType_locations, recordtypes.recordType_resources,):
+                record.proxies = self.getMemberList("Enter proxies of this location or resource", "Proxy", "proxies")
+
+        except EOFError:
+            return 0
+        
+        # Now validate the record and save it
+        if not record.guid:
+            record.guid = str(uuid4())
+            
+        self.directory.addRecord(record)
+
+        return 1

Added: CalDAVClientLibrary/trunk/src/admin/xmlaccounts/commands/changepassword.py
===================================================================
--- CalDAVClientLibrary/trunk/src/admin/xmlaccounts/commands/changepassword.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/admin/xmlaccounts/commands/changepassword.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,93 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from admin.xmlaccounts.commands.command import Command
+import getopt
+
+class ChangePassword(Command):
+    
+    CMDNAME = "passwd"
+
+    def __init__(self):
+        super(ChangePassword, self).__init__(self.CMDNAME, "Change the password for a record.")
+        self.uid = None
+
+    def usage(self):
+        print """USAGE: %s TYPE [OPTIONS]
+
+TYPE: One of "users", "groups", "locations" or "resources". Also,
+"u", "g", "l" or "r" as shortcuts.
+
+Options:
+    -f    file path to accounts.xml
+    --uid UID of record to change
+""" % (self.cmdname,)
+
+    def execute(self, argv):
+        
+        # Check first argument for type
+        argv = self.getTypeArgument(argv)
+        if argv is None:
+            return 0
+        
+        opts, args = getopt.getopt(argv, 'f:h', ["help", "uid=",])
+        
+        for name, value in opts:
+            if name == "-f":
+                self.path = value
+            elif name in ("-h", "--help"):
+                self.usage()
+                return 1
+            elif name == "--uid":
+                self.uid = value
+            else:
+                print "Unknown option: %s." % (name,)
+                self.usage()
+                return 0
+
+        if not self.path:
+            print "Must specify a path."
+            self.usage()
+            return 0
+        if not self.uid:
+            print "Must specify a UID."
+            self.usage()
+            return 0
+        if args:
+            print "Arguments not allowed."
+            self.usage()
+            return 0
+        
+        if not self.loadAccounts():
+            return 0
+        return self.doCommand()
+
+    def doCommand(self):
+        if self.doChangePassword():
+            return self.writeAccounts()
+        return 0
+    
+    def doChangePassword(self):
+        
+        # First check record exists
+        record = self.directory.getRecord(self.recordType, self.uid)
+        if record is None:
+            print "No '%s' record matching uid '%s'" % (self.recordType, self.uid,)
+            return 0
+            
+        record.password = self.promptPassword()
+        return 1
+

Added: CalDAVClientLibrary/trunk/src/admin/xmlaccounts/commands/command.py
===================================================================
--- CalDAVClientLibrary/trunk/src/admin/xmlaccounts/commands/command.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/admin/xmlaccounts/commands/command.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,187 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from admin.xmlaccounts.directory import XMLDirectory
+from xml.etree.ElementTree import XML
+from StringIO import StringIO
+from protocol.utils.xmlhelpers import BetterElementTree
+from admin.xmlaccounts import recordtypes
+from getpass import getpass
+
+import getopt
+
+class Command(object):
+    
+    def __init__(self, cmdname, description):
+        self.path = None
+        self.cmdname = cmdname
+        self.description = description
+        self.recordType = None
+
+    def usage(self):
+        if self.allRecordsAllowed():
+            print """USAGE: %s [TYPE] [OPTIONS]
+        
+TYPE: One of "all", "users", "groups", "locations" or "resources". Also,
+"a", "u", "g", "l" or "r" as shortcuts. Invalid or missing type is
+treated as "all".
+
+Options:
+    -f  file path to accounts.xml
+""" % (self.cmdname,)
+        else:
+            print """USAGE: %s TYPE [OPTIONS]
+        
+TYPE: One of "users", "groups", "locations" or "resources". Also,
+"u", "g", "l" or "r" as shortcuts.
+
+Options:
+    -f  file path to accounts.xml
+""" % (self.cmdname,)
+
+    def allRecordsAllowed(self):
+        return False
+
+    def execute(self, argv):
+        
+        # Check first argument for type
+        argv = self.getTypeArgument(argv)
+        if argv is None:
+            return 0
+        
+        opts, args = getopt.getopt(argv, 'f:h', ["help", ])
+        
+        for name, value in opts:
+            if name == "-f":
+                self.path = value
+            elif name in ("-h", "--help"):
+                self.usage()
+                return 1
+            else:
+                print "Unknown option: %s." % (name,)
+                self.usage()
+                return 0
+
+        if not self.path:
+            print "Must specify a path."
+            self.usage()
+            return 0
+        if args:
+            print "Arguments not allowed."
+            self.usage()
+            return 0
+        
+        if not self.loadAccounts():
+            return 0
+        return self.doCommand()
+
+    def getTypeArgument(self, argv):
+        # Check first argument for type
+        if len(argv) == 0:
+            print "Must specify a record type."
+            self.usage()
+            return None
+        type = argv[0]
+        type = self.mapType(type)
+        if not type and not self.allRecordsAllowed():
+            print "Invalid type '%s'." % (argv[0],)
+            self.usage()
+            return None
+        self.recordType = type if type else recordtypes.recordType_all
+        if type:
+            return argv[1:]
+        else:
+            return argv
+
+    def mapType(self, type):
+        return {
+            "users"    : recordtypes.recordType_users,
+            "u"        : recordtypes.recordType_users,
+            "groups"   : recordtypes.recordType_groups,
+            "g"        : recordtypes.recordType_groups,
+            "locations": recordtypes.recordType_locations,
+            "l"        : recordtypes.recordType_locations,
+            "resources": recordtypes.recordType_resources,
+            "r"        : recordtypes.recordType_resources,
+            "all"      : recordtypes.recordType_all,
+            "a"        : recordtypes.recordType_all,
+        }.get(type, None)
+        
+    def loadAccounts(self):
+        
+        f = open(self.path, "r")
+        if not f:
+            print "Could not open file: %s" % (self.path,)
+            return 0
+        xmldata = f.read()
+        f.close()
+        self.directory = XMLDirectory()
+        self.directory.parseXML(XML(xmldata))
+        return 1
+
+    def writeAccounts(self):
+        
+        node = self.directory.writeXML()
+        os = StringIO()
+        xmldoc = BetterElementTree(node)
+        xmldoc.writeUTF8(os)
+        f = open(self.path, "w")
+        if not f:
+            print "Could not open file: %s for writing" % (self.path,)
+            return 0
+        f.write(os.getvalue())
+        f.close()
+        return 1
+
+    def doCommand(self):
+        pass
+
+    def promptPassword(self):
+        """
+        Prompt the user for a password.
+        """
+        while True:
+            password = getpass("Password: ")
+            temp = getpass("Password (again): ")
+            if temp != password:
+                print "Passwords do not match. Try again."
+            else:
+                return password
+
+    def getMemberList(self, prompt, title, type):
+        """
+        Prompt the user for a list of members.
+        """
+        results = []
+        print prompt
+        while True:
+            memberType = raw_input("%s type [u/g/l/r or leave empty to stop adding %s]: " % (title, type,))
+            if memberType in ("u", "g", "l", "r",):
+                memberUid = raw_input("%s uid [leave empty to stop adding %s]: " % (title, type,))
+                if memberUid:
+                    # Verify that member type exists
+                    recordType = self.mapType(memberType)
+                    if self.directory.containsRecord(recordType, memberUid):
+                        results.append((recordType, memberUid,))
+                    else:
+                        print "Record uid: '%s 'of type: '%s' does not exist in the directory." % (memberUid, recordType,)
+                else:
+                    break
+            elif memberType:
+                print "Member type must be one of 'u' (users), 'g' (groups), 'l' (locations) or 'r' (resources)."
+            else:
+                break
+        return results

Added: CalDAVClientLibrary/trunk/src/admin/xmlaccounts/commands/listrecords.py
===================================================================
--- CalDAVClientLibrary/trunk/src/admin/xmlaccounts/commands/listrecords.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/admin/xmlaccounts/commands/listrecords.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,125 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from admin.xmlaccounts.commands.command import Command
+from admin.xmlaccounts import recordtypes
+import itertools
+
+class ListRecords(Command):
+    
+    CMDNAME = "list"
+
+    def __init__(self):
+        super(ListRecords, self).__init__(self.CMDNAME, "List all records of the specified type.")
+
+    def allRecordsAllowed(self):
+        return True
+
+    def doCommand(self):
+        self.listRecords(self.recordType)
+
+    def listRecords(self, recordType):
+        
+        if recordType == recordtypes.recordType_all:
+            users = [l for l in self.directory.records.itervalues()]
+            print "Full List\n"
+        else:
+            users = (self.directory.records[recordType],)
+            print "%s List\n" % (recordType.capitalize(),)
+        
+        
+        table = [
+            ["UID", "GUID", "Name", "CUADDR",]
+        ]
+        if recordType == recordtypes.recordType_all:
+            table[0].insert(0, "TYPE")
+            table[0].append("MEMBERS")
+            table[0].append("PROXIES")
+        elif recordType in (recordtypes.recordType_groups,):
+            table[0].append("MEMBERS")
+        elif recordType in (recordtypes.recordType_locations, recordtypes.recordType_resources,):
+            table[0].append("PROXIES")
+        for user in itertools.chain(*users):
+            if len(table) > 1:
+                table.append(None)
+            cuaddrs = user.calendarUserAddresses if user.calendarUserAddresses else ("",)
+            if user.recordType in (recordtypes.recordType_groups,):
+                members = user.members if user.members else (None,)
+            elif user.recordType in (recordtypes.recordType_locations, recordtypes.recordType_resources,):
+                members = user.proxies if user.proxies else (None,)
+            else:
+                members = (None,)
+            for ctr, items in enumerate(map(None, cuaddrs, members,)):
+                cuaddr, member = items
+                if cuaddr is None:
+                    cuaddr = ""
+                if member is None:
+                    member = ""
+                else:
+                    member = "(%s) %s" % (member[0], member[1],)
+                if recordType == recordtypes.recordType_all:
+                    row = (user.recordType,)
+                else:
+                    row = ()
+                row += (
+                    user.uid if not ctr else "",
+                    user.guid if not ctr else "",
+                    user.name if not ctr else "",
+                    cuaddr,
+                )
+                if recordType == recordtypes.recordType_all:
+                    if user.recordType in (recordtypes.recordType_groups,):
+                        row += (member, "")
+                    elif user.recordType in (recordtypes.recordType_locations, recordtypes.recordType_resources,):
+                        row += ("", member,)
+                    else:
+                        row += ("", "")
+                elif user.recordType in (recordtypes.recordType_groups, recordtypes.recordType_locations, recordtypes.recordType_resources,):
+                    row += (member,)
+                table.append(row)
+        
+        self.printTable(table)
+        return 1
+        
+    def printTable(self, table):
+        
+        maxWidths = [0 for _ignore in table[0]]
+        for row in table:
+            if row is not None:
+                for ctr, col in enumerate(row):
+                    maxWidths[ctr] = max(maxWidths[ctr], len(col) if col else 0)
+        
+        self.printDivider(maxWidths, False)
+        for rowctr, row in enumerate(table):
+            if row is None:
+                self.printDivider(maxWidths)
+            else:
+                print "|",
+                for colctr, col in enumerate(row):
+                    print "%- *s" % (maxWidths[colctr], col if col else ""),
+                    print "|",
+                print ""
+            if not rowctr:
+                self.printDivider(maxWidths)
+        self.printDivider(maxWidths, False)
+
+    def printDivider(self, maxWidths, intermediate=True):
+        t = "|" if intermediate else "+"
+        for widthctr, width in enumerate(maxWidths):
+            t += "-"
+            t += "-" * width
+            t += "-+" if widthctr < len(maxWidths) - 1 else ("-|" if intermediate else "-+")
+        print t

Added: CalDAVClientLibrary/trunk/src/admin/xmlaccounts/commands/removerecord.py
===================================================================
--- CalDAVClientLibrary/trunk/src/admin/xmlaccounts/commands/removerecord.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/admin/xmlaccounts/commands/removerecord.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,97 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from admin.xmlaccounts.commands.command import Command
+import getopt
+
+class RemoveRecord(Command):
+    
+    CMDNAME = "remove"
+
+    def __init__(self):
+        super(RemoveRecord, self).__init__(self.CMDNAME, "Remove a record of the specified type.")
+        self.uid = None
+
+    def usage(self):
+        print """USAGE: %s TYPE [OPTIONS]
+
+TYPE: One of "users", "groups", "locations" or "resources". Also,
+"u", "g", "l" or "r" as shortcuts.
+
+Options:
+    -f    file path to accounts.xml
+    --uid UID to remove
+""" % (self.cmdname,)
+
+    def execute(self, argv):
+        
+        # Check first argument for type
+        argv = self.getTypeArgument(argv)
+        if argv is None:
+            return 0
+        
+        opts, args = getopt.getopt(argv, 'f:h', ["help", "uid=",])
+        
+        for name, value in opts:
+            if name == "-f":
+                self.path = value
+            elif name in ("-h", "--help"):
+                self.usage()
+                return 1
+            elif name == "--uid":
+                self.uid = value
+            else:
+                print "Unknown option: %s." % (name,)
+                self.usage()
+                return 0
+
+        if not self.path:
+            print "Must specify a path."
+            self.usage()
+            return 0
+        if not self.uid:
+            print "Must specify a UID."
+            self.usage()
+            return 0
+        if args:
+            print "Arguments not allowed."
+            self.usage()
+            return 0
+        
+        if not self.loadAccounts():
+            return 0
+        return self.doCommand()
+
+    def doCommand(self):
+        if self.doRemove():
+            return self.writeAccounts()
+        return 0
+    
+    def doRemove(self):
+        
+        # First check record exists
+        record = self.directory.getRecord(self.recordType, self.uid)
+        if record is None:
+            print "No '%s' record matching uid '%s'" % (self.recordType, self.uid,)
+            return 0
+            
+        
+        confirm = raw_input("Really delete the record for '%s' in '%s' [y/n]?" % (self.uid, self.recordType,))
+        if confirm != "y":
+            return 0
+        
+        self.directory.removeRecord(self.recordType, self.uid)
+        return 1

Added: CalDAVClientLibrary/trunk/src/admin/xmlaccounts/directory.py
===================================================================
--- CalDAVClientLibrary/trunk/src/admin/xmlaccounts/directory.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/admin/xmlaccounts/directory.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,86 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from admin.xmlaccounts import recordtypes
+from admin.xmlaccounts import tags
+from admin.xmlaccounts.record import XMLRecord
+
+from xml.etree.ElementTree import Element
+
+class XMLDirectory(object):
+    
+    def __init__(self):
+        self.realm = ""
+        self.records = {}
+        for type in recordtypes.RECORD_TYPES:
+            self.records[type] = []
+    
+    def addRecord(self, record):
+        self.records[record.recordType].append(record)
+        
+    def containsRecord(self, recordType, uid):
+        for record in self.records[recordType]:
+            if record.uid == uid:
+                return True
+        else:
+            return False
+        
+    def containsGUID(self, guid):
+        for type in recordtypes.RECORD_TYPES:
+            for record in self.records[type]:
+                if record.guid == guid:
+                    return True
+        return False
+        
+    def getRecord(self, recordType, uid):
+        for record in self.records[recordType]:
+            if record.uid == uid:
+                return record
+        else:
+            return None
+        
+    def removeRecord(self, recordType, uid):
+        for record in self.records[recordType]:
+            if record.uid == uid:
+                self.records[recordType].remove(record)
+                return True
+        else:
+            return False
+        
+    def parseXML(self, node):
+        
+        if node.tag == tags.ELEMENT_ACCOUNTS:
+            self.realm = node.get(tags.ATTRIBUTE_REALM, "")
+
+            for child in node.getchildren():
+                record = XMLRecord()
+                record.parseXML(child)
+                self.records[record.recordType].append(record)
+                
+            # Now resolve group and proxy references
+            
+
+    def writeXML(self):
+        root = Element(tags.ELEMENT_ACCOUNTS)
+        if self.realm:
+            root.set(tags.ATTRIBUTE_REALM, self.realm)
+        for type in recordtypes.RECORD_TYPES:
+            self.writeXMLRecords(root, self.records[type])
+        return root
+
+    def writeXMLRecords(self, root, records):
+        for record in records:
+            root.append(record.writeXML())

Added: CalDAVClientLibrary/trunk/src/admin/xmlaccounts/manage.py
===================================================================
--- CalDAVClientLibrary/trunk/src/admin/xmlaccounts/manage.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/admin/xmlaccounts/manage.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,48 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+#import admin.xmlaccounts.commands
+from admin.xmlaccounts.commands import registered
+
+import sys
+
+def usage():
+    cmds = registered.keys()
+    cmds.sort()
+    print """USAGE: manage CMD [OPTIONS]
+
+CMD: one of:
+%s
+    
+OPTIONS: specific to each command, use --help with the
+command to see what options are supported.
+""" % ("\n".join(["\t%s" % (cmd,) for cmd in cmds]),)
+
+def runit():
+    # Dispatch a command based on the first argument
+    if len(sys.argv) == 1:
+        usage()
+        sys.exit(0)
+    
+    if registered.has_key(sys.argv[1]):
+        sys.exit(registered[sys.argv[1]]().execute(sys.argv[2:]))
+    else:
+        print "No command called '%s' is available." % (sys.argv[1],)
+        usage()
+        sys.exit(0)
+
+if __name__ == '__main__':
+    runit()

Added: CalDAVClientLibrary/trunk/src/admin/xmlaccounts/record.py
===================================================================
--- CalDAVClientLibrary/trunk/src/admin/xmlaccounts/record.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/admin/xmlaccounts/record.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,106 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from admin.xmlaccounts import recordtypes
+from admin.xmlaccounts import tags
+
+from protocol.utils.xmlhelpers import SubElementWithData
+
+from xml.etree.ElementTree import Element
+
+class XMLRecord(object):
+    
+    def __init__(self):
+        self.recordType = None
+        self.repeat = 0
+        self.uid = None
+        self.guid = None
+        self.password = None
+        self.name = None
+        self.members = set()
+        self.calendarUserAddresses = set()
+        self.autoSchedule = False
+        self.enabledForCalendaring = True
+        self.proxies = set()
+        self.proxyFor = set()
+    
+    def parseXML(self, node):
+        self.recordType = recordtypes.TAGS_TO_RECORD_TYPES[node.tag]
+        self.repeat = int(node.get(tags.ATTRIBUTE_REPEAT, "0"))
+        for child in node.getchildren():
+            if child.tag == tags.ELEMENT_UID:
+                self.uid = child.text
+            elif child.tag == tags.ELEMENT_GUID:
+                self.guid = child.text
+            elif child.tag == tags.ELEMENT_PASSWORD:
+                self.password = child.text
+            elif child.tag == tags.ELEMENT_NAME:
+                self.name = child.text
+            elif child.tag == tags.ELEMENT_MEMBERS:
+                self._parseMembers(child, self.members)
+            elif child.tag == tags.ELEMENT_CUADDR:
+                self.calendarUserAddresses.add(child.text)
+            elif child.tag == tags.ELEMENT_AUTOSCHEDULE:
+                # Only Resources & Locations
+                if self.recordType not in (recordtypes.recordType_resources, recordtypes.recordType_locations,):
+                    raise ValueError("<auto-schedule> element only allowed for Resources and Locations: %s" % (child.tag,))
+                self.autoSchedule = True
+            elif child.tag == tags.ELEMENT_DISABLECALENDAR:
+                # Only Groups
+                if self.recordType not in (recordtypes.recordType_users, recordtypes.recordType_groups,):
+                    raise ValueError("<disable-calendar> element only allowed for Groups: %s" % (child.tag,))
+                self.enabledForCalendaring = False
+            elif child.tag == tags.ELEMENT_PROXIES:
+                # Only Resources & Locations
+                if self.recordType not in (recordtypes.recordType_resources, recordtypes.recordType_locations,):
+                    raise ValueError("<proxies> element only allowed for Resources and Locations: %s" % (child.tag,))
+                self._parseMembers(child, self.proxies)
+            else:
+                raise RuntimeError("Unknown account attribute: %s" % (child.tag,))
+
+    def _parseMembers(self, node, addto):
+        for child in node.getchildren():
+            if child.tag == tags.ELEMENT_MEMBER:
+                recordType = child.get(tags.ATTRIBUTE_RECORDTYPE, recordtypes.recordType_users)
+                addto.add((recordType, child.text))
+
+    def writeXML(self):
+        
+        root = Element(recordtypes.RECORD_TYPES_TO_TAGS[self.recordType])
+        if self.repeat:
+            root.set(tags.ATTRIBUTE_REPEAT, str(self.repeat))
+        
+        SubElementWithData(root, tags.ELEMENT_UID, self.uid)
+        SubElementWithData(root, tags.ELEMENT_GUID, self.guid)
+        SubElementWithData(root, tags.ELEMENT_PASSWORD, self.password)
+        SubElementWithData(root, tags.ELEMENT_NAME, self.name)
+        if self.recordType == recordtypes.recordType_groups:
+            members = SubElementWithData(root, tags.ELEMENT_MEMBERS)
+            for member in self.members:
+                SubElementWithData(members, tags.ELEMENT_MEMBER, member[1], {tags.ATTRIBUTE_RECORDTYPE:member[0]})
+        if self.calendarUserAddresses:
+            for cuaddr in self.calendarUserAddresses:
+                SubElementWithData(root, tags.ELEMENT_CUADDR, cuaddr)
+        if self.recordType in (recordtypes.recordType_resources, recordtypes.recordType_locations,) and self.autoSchedule:
+            SubElementWithData(root, tags.ELEMENT_AUTOSCHEDULE)
+        if self.recordType in (recordtypes.recordType_users, recordtypes.recordType_groups,) and not self.enabledForCalendaring:
+            SubElementWithData(root, tags.ELEMENT_DISABLECALENDAR)
+        if self.recordType in (recordtypes.recordType_resources, recordtypes.recordType_locations,):
+            proxies = SubElementWithData(root, tags.ELEMENT_PROXIES)
+            for proxy in self.proxies:
+                SubElementWithData(proxies, tags.ELEMENT_MEMBER, proxy[1], {tags.ATTRIBUTE_RECORDTYPE:proxy[0]})
+        
+        return root

Added: CalDAVClientLibrary/trunk/src/admin/xmlaccounts/recordtypes.py
===================================================================
--- CalDAVClientLibrary/trunk/src/admin/xmlaccounts/recordtypes.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/admin/xmlaccounts/recordtypes.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,44 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from admin.xmlaccounts import tags
+
+recordType_users     = "users"
+recordType_groups    = "groups"
+recordType_locations = "locations"
+recordType_resources = "resources"            
+recordType_all       = "all"            
+
+RECORD_TYPES = (
+    recordType_users,
+    recordType_groups,
+    recordType_locations,
+    recordType_resources,            
+)
+
+RECORD_TYPES_TO_TAGS = {
+    recordType_users     : tags.ELEMENT_USER,
+    recordType_groups    : tags.ELEMENT_GROUP,
+    recordType_locations : tags.ELEMENT_LOCATION,
+    recordType_resources : tags.ELEMENT_RESOURCE,            
+}
+
+TAGS_TO_RECORD_TYPES = {
+    tags.ELEMENT_USER     : recordType_users,
+    tags.ELEMENT_GROUP    : recordType_groups,
+    tags.ELEMENT_LOCATION : recordType_locations,
+    tags.ELEMENT_RESOURCE : recordType_resources,            
+}

Added: CalDAVClientLibrary/trunk/src/admin/xmlaccounts/tags.py
===================================================================
--- CalDAVClientLibrary/trunk/src/admin/xmlaccounts/tags.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/admin/xmlaccounts/tags.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,36 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+ELEMENT_ACCOUNTS        = "accounts"
+ELEMENT_USER            = "user"
+ELEMENT_GROUP           = "group"
+ELEMENT_LOCATION        = "location"
+ELEMENT_RESOURCE        = "resource"
+
+ELEMENT_UID             = "uid"
+ELEMENT_GUID            = "guid"
+ELEMENT_PASSWORD        = "password"
+ELEMENT_NAME            = "name"
+ELEMENT_MEMBERS         = "members"
+ELEMENT_MEMBER          = "member"
+ELEMENT_CUADDR          = "cuaddr"
+ELEMENT_AUTOSCHEDULE    = "auto-schedule"
+ELEMENT_DISABLECALENDAR = "disable-calendar"
+ELEMENT_PROXIES         = "proxies"
+
+ATTRIBUTE_REALM         = "realm"
+ATTRIBUTE_REPEAT        = "repeat"
+ATTRIBUTE_RECORDTYPE    = "type"

Added: CalDAVClientLibrary/trunk/src/admin/xmlaccounts/tests/__init__.py
===================================================================
--- CalDAVClientLibrary/trunk/src/admin/xmlaccounts/tests/__init__.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/admin/xmlaccounts/tests/__init__.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,16 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+

Added: CalDAVClientLibrary/trunk/src/admin/xmlaccounts/tests/test_directory.py
===================================================================
--- CalDAVClientLibrary/trunk/src/admin/xmlaccounts/tests/test_directory.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/admin/xmlaccounts/tests/test_directory.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,80 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from StringIO import StringIO
+from admin.xmlaccounts.directory import XMLDirectory
+from protocol.utils.xmlhelpers import BetterElementTree
+from xml.etree.ElementTree import XML
+
+import unittest
+
+class TestDirectory(unittest.TestCase):
+    
+    def checkXML(self, x):
+        
+        x = x.replace("\n", "\r\n")
+            
+        # Parse the XML data
+        a = XMLDirectory()
+        a.parseXML(XML(x))
+        
+        # Generate the XML data
+        node = a.writeXML()
+        os = StringIO()
+        xmldoc = BetterElementTree(node)
+        xmldoc.writeUTF8(os)
+        
+        # Verify data
+        self.assertEqual(os.getvalue(), x)
+
+    def test_accounts(self):
+        
+        self.checkXML("""<?xml version='1.0' encoding='utf-8'?>
+<accounts realm="Test Realm">
+  <user>
+    <uid>admin</uid>
+    <guid>12345</guid>
+    <password>admin</password>
+    <name>Super User</name>
+  </user>
+  <user>
+    <uid>test</uid>
+    <guid />
+    <password>test</password>
+    <name>Test User</name>
+    <cuaddr>mailto:testuser at example.com</cuaddr>
+  </user>
+  <group>
+    <uid>users</uid>
+    <guid>123456</guid>
+    <password>users</password>
+    <name>Users Group</name>
+    <members>
+      <member type="users">test</member>
+    </members>
+  </group>
+  <location>
+    <uid>mercury</uid>
+    <guid>1234567</guid>
+    <password>mercury</password>
+    <name>Mecury Conference Room, Building 1, 2nd Floor</name>
+    <auto-schedule />
+    <proxies>
+      <member type="users">test</member>
+    </proxies>
+  </location>
+</accounts>
+""")

Added: CalDAVClientLibrary/trunk/src/admin/xmlaccounts/tests/test_record.py
===================================================================
--- CalDAVClientLibrary/trunk/src/admin/xmlaccounts/tests/test_record.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/admin/xmlaccounts/tests/test_record.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,82 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from StringIO import StringIO
+from admin.xmlaccounts.record import XMLRecord
+from protocol.utils.xmlhelpers import BetterElementTree
+from xml.etree.ElementTree import XML
+
+import unittest
+
+class TestRecord(unittest.TestCase):
+    
+    def checkXML(self, x):
+        
+        x = x.replace("\n", "\r\n")
+            
+        # Parse the XML data
+        a = XMLRecord()
+        a.parseXML(XML(x))
+        
+        # Generate the XML data
+        node = a.writeXML()
+        os = StringIO()
+        xmldoc = BetterElementTree(node)
+        xmldoc.writeUTF8(os)
+        
+        # Verify data
+        self.assertEqual(os.getvalue(), x)
+
+    def test_user(self):
+        
+        self.checkXML("""<?xml version='1.0' encoding='utf-8'?>
+<user>
+  <uid>test</uid>
+  <guid />
+  <password>test</password>
+  <name>Test User</name>
+  <cuaddr>mailto:testuser at example.com</cuaddr>
+</user>
+""")
+
+    def test_group(self):
+        
+        self.checkXML("""<?xml version='1.0' encoding='utf-8'?>
+<group>
+  <uid>users</uid>
+  <guid>12345</guid>
+  <password>users</password>
+  <name>Users Group</name>
+  <members>
+    <member type="users">test</member>
+  </members>
+</group>
+""")
+
+    def test_location(self):
+        
+        self.checkXML("""<?xml version='1.0' encoding='utf-8'?>
+<location>
+  <uid>mercury</uid>
+  <guid>12345</guid>
+  <password>mercury</password>
+  <name>Mecury Conference Room, Building 1, 2nd Floor</name>
+  <auto-schedule />
+  <proxies>
+    <member type="users">test</member>
+  </proxies>
+</location>
+""")

Added: CalDAVClientLibrary/trunk/src/admin/xmlaccounts/tests/utils.py
===================================================================
--- CalDAVClientLibrary/trunk/src/admin/xmlaccounts/tests/utils.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/admin/xmlaccounts/tests/utils.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,41 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from StringIO import StringIO
+from admin.xmlaccounts.record import XMLRecord
+from protocol.utils.xmlhelpers import BetterElementTree
+from xml.etree.ElementTree import XML
+
+import unittest
+
+class TestCommon(unittest.TestCase):
+    
+    def checkXML(self, x):
+        
+        x = x.replace("\n", "\r\n")
+            
+        # Parse the XML data
+        a = XMLRecord()
+        a.parseXML(XML(x))
+        
+        # Generate the XML data
+        node = a.writeXML()
+        os = StringIO()
+        xmldoc = BetterElementTree(node)
+        xmldoc.writeUTF8(os)
+        
+        # Verify data
+        self.assertEqual(os.getvalue(), x)

Added: CalDAVClientLibrary/trunk/src/browser/__init__.py
===================================================================
--- CalDAVClientLibrary/trunk/src/browser/__init__.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/browser/__init__.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,15 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##

Added: CalDAVClientLibrary/trunk/src/browser/baseshell.py
===================================================================
--- CalDAVClientLibrary/trunk/src/browser/baseshell.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/browser/baseshell.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,208 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from browser import utils
+from browser.command import CommandError
+from browser.command import UnknownCommand
+from protocol.url import URL
+from protocol.webdav.definitions import davxml
+import os
+import readline
+import traceback
+
+class BaseShell(object):
+    
+    def __init__(self, history_name):
+        
+        self.prefix = ""
+        self.commands = {}
+        self.history = []
+        self.history_name = history_name
+        self.preserve_history = False
+        self.last_wd_complete = ("", ())
+        
+        self.readHistory()
+
+    def readHistory(self):
+        try:
+            readline.read_history_file(os.path.expanduser("~/.%s" % (self.history_name,)))
+        except IOError:
+            pass
+
+    def saveHistory(self):
+        readline.write_history_file(os.path.expanduser("~/.%s" % (self.history_name,)))
+
+    def registerCommands(self, cmds):
+        raise NotImplementedError
+
+    def registerCommand(self, command):
+        for cmd in command.getCmds():
+            self.commands[cmd] = command
+        command.setShell(self)
+
+    def run(self):
+        
+        # Preserve existing history
+        if self.preserve_history:
+            old_history = [readline.get_history_item(index) for index in xrange(readline.get_current_history_length())]
+            readline.clear_history()
+            map(readline.add_history, self.history)
+
+        readline.set_completer(self.complete)
+        readline.parse_and_bind("bind ^I rl_complete")
+
+        while True:
+            cmdline = raw_input("%s > " % (self.prefix,))
+            self.last_wd_complete = ("", ())
+            if not cmdline:
+                continue
+
+            # Try to dispatch command
+            try:
+                self.execute(cmdline)
+            except SystemExit, e:
+                print "Exiting shell: %s" % (e.message,)
+                break
+            except UnknownCommand, e:
+                print "Command '%s' unknown." % (e.message,)
+            except Exception, e:
+                traceback.print_exc()
+
+        # Restore previous history
+        if self.preserve_history:
+            self.saveHistory()
+            readline.clear_history()
+            map(readline.add_history, old_history)
+
+    def execute(self, cmdline):
+        
+        # Check for history recall
+        if cmdline == "!!" and self.history:
+            cmdline = self.history[-1]
+            print cmdline
+            if readline.get_current_history_length():
+                readline.replace_history_item(readline.get_current_history_length() - 1, cmdline)
+        elif cmdline.startswith("!"):
+            try:
+                index = int(cmdline[1:])
+                if index> 0 and index <= len(self.history):
+                    cmdline = self.history[index-1]
+                    print cmdline
+                    if readline.get_current_history_length():
+                        readline.replace_history_item(readline.get_current_history_length() - 1, cmdline)
+                else:
+                    raise ValueError()
+            except ValueError:
+                print "%s: event not found" % (cmdline,)
+                return
+            
+        # split the command line into command and options
+        splits = cmdline.split(" ", 1)
+        cmd = splits[0]
+        options = splits[1] if len(splits) == 2 else ""
+        
+        # Find matching command
+        try:
+            if cmd not in self.commands:
+                self.history.append(cmdline)
+                raise UnknownCommand(cmd)
+            else:
+                self.commands[cmd].execute(cmd, options)
+        finally:
+            # Store in history
+            self.history.append(cmdline)
+    
+    def help(self, cmd=None):
+        
+        if cmd:
+            if cmd in self.commands:
+                cmds = ((cmd, self.commands[cmd]),)
+                full_help = True
+            else:
+                raise CommandError("Command could not be found: %s" % (cmd,))
+        else:
+            cmds = self.commands.keys()
+            cmds.sort()
+            cmds = [(cmd, self.commands[cmd]) for cmd in cmds]
+            full_help = False
+        
+        if full_help:
+            if self.commands[cmd].hasHelp(cmd):
+                print self.commands[cmd].help(cmd)
+        else:
+            results = []
+            for name, cmd in cmds:
+                if cmd.hasHelp(name):
+                    results.append(cmd.helpListing(name))
+            utils.printTwoColumnList(results)
+
+    def complete(self, text, state):
+        
+        # If there is no space in the text we complete a command
+        #print "complete: %s %d" % (text, state)
+        results = []
+        check = readline.get_line_buffer()[:readline.get_endidx()].lstrip()
+        checklen = len(check)
+        if " " not in check:
+            for cmd in self.commands:
+                if cmd[:checklen] == check:
+                    results.append(cmd)
+        else:
+            cmd, rest = check.split(" ", 1)
+            if cmd in self.commands:
+                results = self.commands[cmd].complete(rest)
+
+        return results[state]
+
+    def wdcomplete(self, text):
+        
+        #print "\nwdcomplete: %s" % (text,)
+
+        # Look at cache and return that
+        if self.last_wd_complete[0] == text:
+            return self.last_wd_complete[1]
+
+        # Look for relative vs absolute
+        if text[0] == "/":
+            dirname, _ignore_child = os.path.split(text)
+            path = dirname
+            if not path.endswith("/"):
+                path += "/"
+            pathlen = 0
+        else:
+            path = self.wd
+            pathlen = len(path) + (0 if path.endswith("/") else 1)
+            dirname, _ignore_child = os.path.split(text)
+            if dirname:
+                path = os.path.join(path, dirname)
+            if not path.endswith("/"):
+                path += "/"
+
+        #print "pdc: %s, %s, %s, %s" % (self.wd, path, dirname, child)
+        resource = URL(url=path)
+
+        props = (davxml.resourcetype,)
+        results = self.account.session.getPropertiesOnHierarchy(resource, props)
+        #print results.keys()
+        results = [result[pathlen:] for result in results.iterkeys() if len(result) > pathlen]
+        #print results
+        if text:
+            textlen = len(text)
+            results = [result for result in results if result[:textlen] == text]
+            #print results
+        
+        self.last_wd_complete = (text, results,)
+        return results

Added: CalDAVClientLibrary/trunk/src/browser/command.py
===================================================================
--- CalDAVClientLibrary/trunk/src/browser/command.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/browser/command.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,62 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+class Command(object):
+    
+    def __init__(self):
+        
+        self.shell = None
+        self.cmds = ()
+        
+    def execute(self, name, options):
+        raise NotImplementedError
+
+    def usage(self, name):
+        raise NotImplementedError
+
+    def hasHelp(self, name):
+        return name in self.cmds
+
+    def help(self, name):
+        result = "Command: %s\n" % (name,)
+        result += "Description: %s\n" % (self.helpDescription(),)
+        result += self.usage(name)
+        return result
+        
+    def helpListing(self, name):
+        return (name, self.helpDescription())
+        
+    def helpDescription(self):
+        return ""
+    
+    def setShell(self, shell):
+        self.shell = shell
+        
+    def getCmds(self):
+        return self.cmds
+
+    def complete(self, text):
+        return ()
+
+class WrongOptions(Exception):
+    pass
+
+class UnknownCommand(Exception):
+    pass
+
+class CommandError(Exception):
+    pass
+

Added: CalDAVClientLibrary/trunk/src/browser/commands/__init__.py
===================================================================
--- CalDAVClientLibrary/trunk/src/browser/commands/__init__.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/browser/commands/__init__.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,31 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+__all__ = [
+    "acl",
+    "cat",
+    "cd",
+    "help",
+    "history",
+    "logging",
+    "ls",
+    "principal",
+    "props",
+    "proxies",
+    "quit",
+    "server",
+    "whoami",
+]

Added: CalDAVClientLibrary/trunk/src/browser/commands/acl.py
===================================================================
--- CalDAVClientLibrary/trunk/src/browser/commands/acl.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/browser/commands/acl.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,372 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from browser.command import Command
+from browser.command import WrongOptions
+from protocol.url import URL
+from protocol.webdav.definitions import davxml
+from browser.subshell import SubShell
+from browser import commands
+from protocol.webdav.ace import ACE
+from browser import utils
+from protocol.caldav.definitions import caldavxml
+from xml.etree.ElementTree import QName
+import readline
+import os
+import getopt
+
+class Cmd(Command):
+    
+    def __init__(self):
+        super(Command, self).__init__()
+        self.cmds = ("acl",)
+        self.subshell = None
+        
+    def execute(self, name, options):
+        
+        interactive = False
+        path = None
+
+        try:
+            opts, args = getopt.getopt(options.split(), 'i')
+        except getopt.GetoptError, e:
+            print str(e)
+            print self.usage(name)
+            raise WrongOptions
+            
+        for name, _ignore_value in opts:
+            
+            if name == "-i":
+                interactive = True
+            else:
+                print "Unknown option: %s" % (name,)
+                print self.usage(name)
+                raise WrongOptions
+        
+        if len(args) > 1:
+            print "Wrong number of arguments: %d" % (len(args),)
+            print self.usage(name)
+            raise WrongOptions
+        elif args:
+            path = args[0]
+            if not path.startswith("/"):
+                path = os.path.join(self.shell.wd, path)
+        else:
+            path = self.shell.wd
+        if not path.endswith("/"):
+            path += "/"
+        resource = URL(url=path)
+
+        results, bad = self.shell.account.session.getProperties(resource, (davxml.acl,))
+        if davxml.acl in bad:
+            print "Could not retrieve DAV:acl property, status=%d" % (bad[davxml.acl],)
+        else:
+            if interactive:
+                self.doInteractiveMode(resource, results[davxml.acl])
+            else:
+                aces = ACE.parseFromACL(results[davxml.acl])
+                print utils.printACEList(aces, self.shell.account)
+            
+        return True
+
+    def doInteractiveMode(self, resource, acls):
+        
+        print "Entering ACL edit mode on resource: %s" % (resource.relativeURL(),)
+        if not self.subshell:
+            self.subshell = SubShell(self.shell, "ACL", (
+                commands.help.Cmd(),
+                commands.logging.Cmd(),
+                commands.quit.Cmd(),
+                Add(),
+                Change(),
+                Remove(),
+                List(),
+            ))
+        self.subshell.resource = resource
+        self.subshell.account = self.shell.account
+        self.subshell.run()
+        
+    def usage(self, name):
+        return """Usage: %s [OPTIONS] [PATH]
+PATH is a relative or absolute path.
+
+Options:
+-i    interactive mode for adding, changing and deleting ACLs.
+    if not present, existing ACLs will be printed.
+""" % (name,)
+
+    def helpDescription(self):
+        return "Manage the access privileges of a directory or file."
+
+class CommonACLCommand(Command):
+    
+    def displayACEList(self):
+        # First list the current set
+        results, bad = self.shell.shell.account.session.getProperties(self.shell.resource, (davxml.acl,))
+        if davxml.acl in bad:
+            print "Could not retrieve DAV:acl property, status=%d" % (bad[davxml.acl],)
+            return None
+        else:
+            aces = ACE.parseFromACL(results[davxml.acl])
+            print utils.printACEList(aces, self.shell.shell.account)
+            return aces
+
+    def createACE(self, oldace=None):
+
+        ace = ACE()
+        print "Principal Type:"
+        print "  1. Principal path"
+        print "  2. All"
+        print "  3. Authenticated"
+        print "  4. Unauthenticated"
+        print "  5. Property"
+        insert = None
+        if oldace:
+            mapper = {
+                str(davxml.href):             "1",
+                str(davxml.all):              "2",
+                str(davxml.authenticated):    "3",
+                str(davxml.unauthenticated):  "4",
+                str(davxml.property):         "5",
+            }
+            insert = mapper.get(oldace.principal)
+        choice = utils.numericInput("Select type: ", 1, 5, insert=insert)
+        if choice == "q":
+            return None
+        
+        if choice == 1:
+            href = utils.textInput("Enter principal path: ", insert=oldace.data if oldace else None)
+            principal = self.shell.shell.account.getPrincipal(URL(url=href))
+            ace.principal = str(davxml.href)
+            ace.data = principal.principalURL.relativeURL()
+        elif choice == 2:
+            ace.principal = str(davxml.all)
+        elif choice == 3:
+            ace.principal = str(davxml.authenticated)
+        elif choice == 4:
+            ace.principal = str(davxml.unauthenticated)
+        elif choice == 5:
+            prop = utils.textInput("Enter property qname: ", insert=str(oldace.data) if oldace else None)
+            ace.principal = str(davxml.property)
+            ace.data = QName(prop)
+        
+        invert = utils.yesNoInput("Invert principal [y/n]: ", insert=("y" if oldace.invert else "n") if oldace else None)
+        ace.invert = (invert == "y")
+        
+        grant = utils.choiceInput("Grant or Deny privileges [g/d]: ", ("g", "d",), insert=("g" if oldace.grant else "d") if oldace else None)
+        ace.grant = (grant == "g")
+        
+        print "Privileges:"
+        print "  a. {DAV}read"
+        print "  b. {DAV}write"
+        print "  c. {DAV}write-properties"
+        print "  d. {DAV}write-content"
+        print "  e. {DAV}read-acl"
+        print "  f. {DAV}read-current-user-privilege-set"
+        print "  g. {DAV}write-acl"
+        print "  h. {DAV}bind"
+        print "  i. {DAV}unbind"
+        print "  j. {DAV}all"
+        print "  k. {CALDAV}read-free-busy"
+        print "  l. {CALDAV}schedule"
+        print "  q. quit without changes"
+        choice = utils.multiChoiceInput(
+                     "Select multiple items: ",
+                     [char for char in "abcdefghijklq"],
+                 )
+        if "q" in choice:
+            return None
+        
+        mappedPrivs = {
+            'a': davxml.read,
+            'b': davxml.write,
+            'c': davxml.write_properties,
+            'd': davxml.write_content,
+            'e': davxml.read_acl,
+            'f': davxml.read_current_user_privilege_set,
+            'g': davxml.write_acl,
+            'h': davxml.bind,
+            'i': davxml.unbind,
+            'j': davxml.all,
+            'k': caldavxml.read_free_busy,
+            'l': caldavxml.schedule,
+        }
+        ace.privs = ()
+        for char in choice:
+            ace.privs += (mappedPrivs[char],)
+        
+        return ace
+        
+class Add(CommonACLCommand):
+    def __init__(self):
+        super(Command, self).__init__()
+        self.cmds = ("add",)
+    
+    def execute(self, name, options):
+        
+        # First list the current set
+        aces = self.displayACEList()
+        if aces:
+            # Ask user which one to delete
+            while True:
+                result = raw_input("Add ACL before [1 - %d] or cancel [q]: " % (len(aces) + 1,))
+                if readline.get_current_history_length():
+                    readline.remove_history_item(readline.get_current_history_length() - 1)
+                if not result:
+                    continue
+                if result[0] == "q":
+                    break
+                try:
+                    number = int(result)
+                    if number > len(aces):
+                        number = len(aces)
+                except ValueError:
+                    print "Invalid input, try again."
+                    continue
+                
+                # Try and get the new ace
+                ace = self.createACE()
+                if not ace:
+                    break
+                aces.insert(number, ace)
+
+                # Now remove those that cannot be edited
+                aces = [ace for ace in aces if ace.canChange()]
+                
+                # Now execute
+                self.shell.shell.account.session.setACL(self.shell.resource, aces)
+                break
+                
+    def usage(self, name):
+        return """Usage: %s
+""" % (name,)
+
+    def helpDescription(self):
+        return "Add ACL to existing resource."
+
+class Change(CommonACLCommand):
+    def __init__(self):
+        super(Command, self).__init__()
+        self.cmds = ("change",)
+    
+    def execute(self, name, options):
+
+        # First list the current set
+        aces = self.displayACEList()
+        if aces:
+            # Ask user which one to delete
+            while True:
+                result = raw_input("Change ACL at [1 - %d] or cancel [q]: " % (len(aces),))
+                if readline.get_current_history_length():
+                    readline.remove_history_item(readline.get_current_history_length() - 1)
+                if not result:
+                    continue
+                if result[0] == "q":
+                    break
+                try:
+                    number = int(result)
+                except ValueError:
+                    print "Invalid input, try again."
+                    continue
+                
+                # Check that the targeted ace is editable
+                if not aces[number - 1].canChange():
+                    print "You cannot change a protected or inherited ace."
+                    break
+                
+                # Try and get the new ace
+                ace = self.createACE(oldace=aces[number - 1])
+                if not ace:
+                    break
+                aces[number - 1] = ace
+
+                # Now remove those that cannot be edited
+                aces = [ace for ace in aces if ace.canChange()]
+                
+                # Now execute
+                self.shell.shell.account.session.setACL(self.shell.resource, aces)
+                break
+
+    def usage(self, name):
+        return """Usage: %s
+""" % (name,)
+
+    def helpDescription(self):
+        return "Change ACL on existing resource."
+
+class Remove(CommonACLCommand):
+    def __init__(self):
+        super(Command, self).__init__()
+        self.cmds = ("remove",)
+    
+    def execute(self, name, options):
+        
+        # First list the current set
+        aces = self.displayACEList()
+        if aces:
+            # Ask user which one to delete
+            while True:
+                result = raw_input("Remove ACL [1 - %d] or cancel [q]: " % (len(aces),))
+                if readline.get_current_history_length():
+                    readline.remove_history_item(readline.get_current_history_length() - 1)
+                if not result:
+                    continue
+                if result[0] == "q":
+                    break
+                try:
+                    number = int(result)
+                except ValueError:
+                    print "Invalid input, try again."
+                    continue
+                
+                # Check that the targeted ace is editable
+                if not aces[number-1].canChange():
+                    print "You cannot remove a protected or inherited ace."
+                    break
+                
+                # Remove the one we are removing
+                del aces[number-1]
+                
+                # Now remove those that cannot be edited
+                aces = [ace for ace in aces if ace.canChange()]
+                
+                # Now execute
+                self.shell.shell.account.session.setACL(self.shell.resource, aces)
+                break
+                
+    def usage(self, name):
+        return """Usage: %s
+""" % (name,)
+
+    def helpDescription(self):
+        return "Remove ACL on existing resource."
+
+class List(CommonACLCommand):
+    def __init__(self):
+        super(Command, self).__init__()
+        self.cmds = ("list",)
+    
+    def execute(self, name, options):
+        
+        self.displayACEList()
+        return True
+
+    def usage(self, name):
+        return """Usage: %s
+""" % (name,)
+
+    def helpDescription(self):
+        return "List current ACLs on existing resource."

Added: CalDAVClientLibrary/trunk/src/browser/commands/cat.py
===================================================================
--- CalDAVClientLibrary/trunk/src/browser/commands/cat.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/browser/commands/cat.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,55 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from browser.command import Command
+from browser.command import WrongOptions
+from protocol.url import URL
+import os
+import getopt
+
+class Cmd(Command):
+    
+    def __init__(self):
+        super(Command, self).__init__()
+        self.cmds = ("cat",)
+        
+    def execute(self, name, options):
+        opts, args = getopt.getopt(options.split(), '')
+        if len(opts) or len(args) != 1:
+            print self.usage(name)
+            raise WrongOptions()
+
+        path = args[0]
+
+        if not path.startswith("/"):
+            path = os.path.join(self.shell.wd, path)
+        resource = URL(url=path)
+
+        data, _ignore_etag = self.shell.account.session.readData(resource)
+        print data
+
+        return True
+
+    def complete(self, text):
+        return self.shell.wdcomplete(text)
+
+    def usage(self, name):
+        return """Usage: %s PATH
+PATH is a relative or absolute path.
+""" % (name,)
+
+    def helpDescription(self):
+        return "Display contents of a file or directory."

Added: CalDAVClientLibrary/trunk/src/browser/commands/cd.py
===================================================================
--- CalDAVClientLibrary/trunk/src/browser/commands/cd.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/browser/commands/cd.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,61 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from browser.command import Command
+from browser.command import WrongOptions
+import os
+import getopt
+
+class Cmd(Command):
+    
+    def __init__(self):
+        super(Command, self).__init__()
+        self.cmds = ("cd",)
+        
+    def execute(self, name, options):
+        opts, args = getopt.getopt(options.split(), '')
+        if len(opts) or len(args) != 1:
+            print self.usage(name)
+            raise WrongOptions()
+
+        newpath = args[0]
+        oldpath = self.shell.wd
+        result = True
+
+        if newpath == "..":
+            result = self.shell.setWD(os.path.dirname(oldpath))
+        elif newpath == ".":
+            pass
+        elif newpath.startswith("/"):
+            result = self.shell.setWD(newpath)
+        else:
+            result = self.shell.setWD(os.path.normpath(os.path.join(oldpath, newpath)))
+
+        if not result:
+            print "%s: %s No such directory" % (name, options,)
+            
+        return result
+
+    def complete(self, text):
+        return self.shell.wdcomplete(text)
+
+    def usage(self, name):
+        return """Usage: %s PATH
+PATH is a relative or absolute path.
+""" % (name,)
+
+    def helpDescription(self):
+        return "Change working directory."

Added: CalDAVClientLibrary/trunk/src/browser/commands/help.py
===================================================================
--- CalDAVClientLibrary/trunk/src/browser/commands/help.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/browser/commands/help.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,44 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from browser.command import Command
+from browser.command import WrongOptions
+import getopt
+
+class Cmd(Command):
+    
+    def __init__(self):
+        super(Command, self).__init__()
+        self.cmds = ("help", "?",)
+        
+    def execute(self, name, options):
+        opts, args = getopt.getopt(options.split(), '')
+        if len(opts) or len(args) > 1:
+            print self.usage(name)
+            raise WrongOptions()
+        self.shell.help(cmd = (None if len(args) == 0 else args[0]))
+        return True
+
+    def usage(self, name):
+        return """Usage: %s [CMD]
+CMD is the name of a command.
+""" % (name,)
+
+    def hasHelp(self, name):
+        return name in ("help",)
+
+    def helpDescription(self):
+        return "Displays help about a command."

Added: CalDAVClientLibrary/trunk/src/browser/commands/history.py
===================================================================
--- CalDAVClientLibrary/trunk/src/browser/commands/history.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/browser/commands/history.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,41 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from browser.command import Command
+from browser.command import WrongOptions
+
+class Cmd(Command):
+    
+    def __init__(self):
+        super(Command, self).__init__()
+        self.cmds = ("history", )
+        
+    def execute(self, name, options):
+        if options:
+            print self.usage(name)
+            raise WrongOptions()
+        
+        format = "%%0%ds %%s" % (len(self.shell.history),)
+        for ctr, cmd in enumerate(self.shell.history):
+            print format % (ctr+1, cmd,)
+        return True
+
+    def usage(self, name):
+        return """Usage: %s
+""" % (name,)
+
+    def helpDescription(self):
+        return "Displays the history of all commands used in this session."

Added: CalDAVClientLibrary/trunk/src/browser/commands/logging.py
===================================================================
--- CalDAVClientLibrary/trunk/src/browser/commands/logging.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/browser/commands/logging.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,56 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from browser.command import Command
+from browser.command import WrongOptions
+import getopt
+
+class Cmd(Command):
+    
+    def __init__(self):
+        super(Command, self).__init__()
+        self.cmds = ("logging", )
+        
+    def execute(self, name, options):
+        opts, args = getopt.getopt(options.split(), '')
+        if len(opts) or len(args) > 1:
+            print self.usage(name)
+            raise WrongOptions()
+        if args and args[0] not in ("on", "off",):
+            print self.usage(name)
+            raise WrongOptions()
+        
+        if args:
+            state = args[0]
+        else:
+            state = ("off" if self.shell.account.session.loghttp else "on")
+        if state == "on":
+            self.shell.account.session.loghttp = True
+            print "HTTP logging turned on"
+        else:
+            self.shell.account.session.loghttp = False
+            print "HTTP logging turned off"
+        return True
+
+    def usage(self, name):
+        return """Usage: %s [on|off]
+on  - turn HTTP protocol logging on
+off - turn HTTP protocol logging off
+without either argument - toggle the state of logging
+""" % (name,)
+
+    def helpDescription(self):
+        return "Changes the current state of HTTP logging."

Added: CalDAVClientLibrary/trunk/src/browser/commands/ls.py
===================================================================
--- CalDAVClientLibrary/trunk/src/browser/commands/ls.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/browser/commands/ls.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,93 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from browser.command import Command
+from browser.command import WrongOptions
+from protocol.webdav.definitions import davxml
+from protocol.url import URL
+import os
+import getopt
+
+class Cmd(Command):
+    
+    def __init__(self):
+        super(Command, self).__init__()
+        self.cmds = ("ls",)
+        
+    def execute(self, name, options):
+        
+        longlist = False
+        path = None
+
+        opts, args = getopt.getopt(options.split(), 'l')
+
+        for name, _ignore_value in opts:
+            
+            if name == "-l":
+                longlist = True
+            else:
+                print "Unknown option: %s" % (name,)
+                print self.usage(name)
+                raise WrongOptions
+        
+        if len(args) > 1:
+            print "Wrong number of arguments: %d" % (len(args),)
+            print self.usage(name)
+            raise WrongOptions
+        elif args:
+            path = args[0]
+            if not path.startswith("/"):
+                path = os.path.join(self.shell.wd, path)
+        else:
+            path = self.shell.wd
+        if not path.endswith("/"):
+            path += "/"
+        resource = URL(url=path)
+
+        props = (davxml.resourcetype,)
+        if longlist:
+            props += (davxml.getcontentlength, davxml.getlastmodified,)
+        results = self.shell.account.session.getPropertiesOnHierarchy(resource, props)
+        items = results.keys()
+        items.sort()
+        for rurl in items:
+            if rurl == path:
+                continue
+            if longlist:
+                props = results[rurl]
+                size = props.get(davxml.getcontentlength, "-")
+                if not size:
+                    size = "0"
+                modtime = props.get(davxml.getlastmodified, "-")
+                print "% 8s %s %s" % (size, modtime, rurl[len(path):])
+            else:
+                print rurl[len(path):]
+            
+        return True
+
+    def complete(self, text):
+        return self.shell.wdcomplete(text)
+
+    def usage(self, name):
+        return """Usage: %s [OPTIONS] [PATH]
+PATH is a relative or absolute path.
+
+Options:
+-l   long listing
+""" % (name,)
+
+    def helpDescription(self):
+        return "List the contents of a directory."

Added: CalDAVClientLibrary/trunk/src/browser/commands/principal.py
===================================================================
--- CalDAVClientLibrary/trunk/src/browser/commands/principal.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/browser/commands/principal.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,100 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from browser.command import Command
+from browser.command import WrongOptions
+from protocol.url import URL
+from browser import utils
+import getopt
+
+class Cmd(Command):
+    
+    def __init__(self):
+        super(Command, self).__init__()
+        self.cmds = ("principal", )
+        
+    def execute(self, name, options):
+        refresh = False
+        resolve = True
+        principal = None
+        print_proxies = False
+
+        opts, args = getopt.getopt(options.split(), 'fnp:x')
+
+        for name, value in opts:
+            
+            if name == "-f":
+                refresh = True
+            elif name == "-n":
+                resolve = False
+            elif name == "-p":
+                principal = self.shell.account.getPrincipal(URL(url=value), refresh=refresh)
+            elif name == "-x":
+                print_proxies = True
+            else:
+                print "Unknown option: %s" % (name,)
+                print self.usage(name)
+                raise WrongOptions
+
+        if len(args) > 0:
+            print "Wrong number of arguments: %d" % (len(args),)
+            print self.usage(name)
+            raise WrongOptions
+
+        if not principal:
+            principal = self.shell.account.getPrincipal(refresh=refresh)
+
+        print """
+    Principal Path    : %s
+    Display Name      : %s
+    Principal URL     : %s
+    Alternate URLs    : %s
+    Group Members     : %s
+    Memberships       : %s
+    Calendar Homes    : %s
+    Outbox URL        : %s
+    Inbox URL         : %s
+    Calendar Addresses: %s
+""" % (
+          principal.principalPath,
+          principal.getSmartDisplayName(),
+          principal.principalURL,
+          utils.printList(principal.alternateURIs),
+          utils.printPrincipalPaths(self.shell.account, principal.memberset, resolve, refresh),
+          utils.printPrincipalPaths(self.shell.account, principal.memberships, resolve, refresh),
+          utils.printList(principal.homeset),
+          principal.outboxURL,
+          principal.inboxURL,
+          utils.printList(principal.cuaddrs),
+      ),
+
+        if print_proxies:
+            utils.printProxyPrincipals(self.shell.account, principal, True, True, resolve, False, refresh)
+
+        return True
+
+    def usage(self, name):
+        return """Usage: %s [OPTIONS]
+Options:
+    -f force reload of all cached principals to be returned
+    -p principal path to request proxies for [OPTIONAL]
+        if not present, the current user's principal is used.
+    -n do not resolve references to other principals.
+    -x print proxy details as well.
+""" % (name,)
+
+    def helpDescription(self):
+        return "Displays the current server login id."

Added: CalDAVClientLibrary/trunk/src/browser/commands/props.py
===================================================================
--- CalDAVClientLibrary/trunk/src/browser/commands/props.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/browser/commands/props.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,95 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from browser.command import Command
+from browser.command import WrongOptions
+from protocol.url import URL
+from browser import utils
+import os
+import getopt
+
+class Cmd(Command):
+    
+    def __init__(self):
+        super(Command, self).__init__()
+        self.cmds = ("props",)
+        
+    def execute(self, name, options):
+        
+        names = False
+        all = False
+        path = None
+
+        opts, args = getopt.getopt(options.split(), 'an')
+
+        for name, _ignore_value in opts:
+            
+            if name == "-a":
+                all = True
+            elif name == "-n":
+                names = True
+            else:
+                print "Unknown option: %s" % (name,)
+                print self.usage(name)
+                raise WrongOptions
+        
+        if len(args) > 1:
+            print "Wrong number of arguments: %d" % (len(args),)
+            print self.usage(name)
+            raise WrongOptions
+        elif args:
+            path = args[0]
+            if not path.startswith("/"):
+                path = os.path.join(self.shell.wd, path)
+        else:
+            path = self.shell.wd
+        if not path.endswith("/"):
+            path += "/"
+        resource = URL(url=path)
+
+        if names:
+            results = self.shell.account.session.getPropertyNames(resource)
+            print "    Properties: %s" % (utils.printList(results),)
+        else:
+            if all:
+                props = None
+            else:
+                props = self.shell.account.session.getPropertyNames(resource)
+            results, bad = self.shell.account.session.getProperties(resource, props)
+            print "OK Properties:"
+            utils.printProperties(results)
+            if bad:
+                print "Failed Properties:"
+                utils.printProperties(bad)
+            
+        return True
+
+    def complete(self, text):
+        return self.shell.wdcomplete(text)
+
+    def usage(self, name):
+        return """Usage: %s [OPTIONS] [PATH]
+PATH is a relative or absolute path.
+
+Options:
+-n    list property names only
+-a    list all properties via allprop
+    if neither of the above are set then property names are first listed, and then values of those looked up.
+    only one of -n and -a can be set.
+""" % (name,)
+
+    def helpDescription(self):
+        return "List the properties of a directory or file."

Added: CalDAVClientLibrary/trunk/src/browser/commands/proxies.py
===================================================================
--- CalDAVClientLibrary/trunk/src/browser/commands/proxies.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/browser/commands/proxies.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,245 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from browser.command import Command
+from browser.command import WrongOptions
+from protocol.url import URL
+from browser import utils
+from browser.subshell import SubShell
+from browser import commands
+import getopt
+
+class Cmd(Command):
+    
+    def __init__(self):
+        super(Command, self).__init__()
+        self.cmds = ("proxies", )
+        self.subshell = None
+        
+    def execute(self, name, options):
+
+        interactive = False
+        read = False
+        write = False
+        principal = self.shell.account.getPrincipal()
+
+        try:
+            opts, args = getopt.getopt(options.split(), 'irwp:')
+        except getopt.GetoptError, e:
+            print str(e)
+            print self.usage(name)
+            raise WrongOptions
+
+        for name, value in opts:
+            
+            if name == "-i":
+                interactive = True
+            elif name == "-r":
+                read = True
+            elif name == "-w":
+                write = True
+            elif name == "-p":
+                principal = self.shell.account.getPrincipal(URL(url=value))
+            else:
+                print "Unknown option: %s" % (name,)
+                print self.usage(name)
+                raise WrongOptions
+        
+        if len(args) > 0:
+            print "Wrong number of arguments: %d" % (len(args),)
+            print self.usage(name)
+            raise WrongOptions
+
+        if interactive:
+            self.doInteractiveMode(principal)
+        else:
+            utils.printProxyPrincipals(self.shell.account, principal, read or not write, write or not read, True)
+
+        return True
+
+    def doInteractiveMode(self, principal):
+        
+        print "Entering Proxy edit mode on principal: %s (%s)" % (principal.getSmartDisplayName(), principal.principalURL)
+        if not self.subshell:
+            self.subshell = SubShell(self.shell, "Proxy", (
+                commands.help.Cmd(),
+                commands.logging.Cmd(),
+                commands.quit.Cmd(),
+                Add(),
+                Remove(),
+                List(),
+            ))
+        self.subshell.principal = principal
+        self.subshell.account = self.shell.account
+        self.subshell.run()
+        
+    def usage(self, name):
+        return """Usage: %s [OPTIONS]
+PRINCIPAL - principal path to request proxies for.
+ 
+Options:
+    -i interactive mode for adding, changing and deleting proxies.
+    -r read proxies [OPTIONAL]
+    -w write proxies [OPTIONAL]
+        if neither is present, both are displayed.
+        
+    -p principal path to request proxies for [OPTIONAL]
+        if not present, the current user's principal is used.
+""" % (name,)
+
+    def helpDescription(self):
+        return "Displays the delegates for the chosen user."
+
+class CommonProxiesCommand(Command):
+
+    def parseOptions(self, name, options):
+        read = False
+        write = False
+
+        try:
+            opts, args = getopt.getopt(options.split(), 'rw')
+        except getopt.GetoptError, e:
+            print str(e)
+            print self.usage(name)
+            raise WrongOptions
+
+        for name, _ignore_value in opts:
+            
+            if name == "-r":
+                read = True
+                if write:
+                    print "Only one of -r and -w may be specified."
+                    print self.usage(name)
+                    raise WrongOptions
+            elif name == "-w":
+                write = True
+                if read:
+                    print "Only one of -r and -w may be specified."
+                    print self.usage(name)
+                    raise WrongOptions
+            else:
+                print "Unknown option: %s" % (name,)
+                print self.usage(name)
+                raise WrongOptions
+        
+        if not read and not write:
+            print "One of -r and -w must be specified."
+            print self.usage(name)
+            raise WrongOptions
+
+        if len(args) > 0:
+            print "Wrong number of arguments: %d" % (len(args),)
+            print self.usage(name)
+            raise WrongOptions
+        
+        return read
+
+    def printProxyList(self, read):
+        
+        if read:
+            principals = self.shell.principal.getReadProxies()
+        else:
+            principals = self.shell.principal.getWriteProxies()
+        if principals:
+            print utils.printPrincipalList(principals)
+        else:
+            print "There are no proxies of the specified type."
+        return principals
+
+class Add(CommonProxiesCommand):
+    def __init__(self):
+        super(Command, self).__init__()
+        self.cmds = ("add",)
+    
+    def execute(self, name, options):
+
+        read = self.parseOptions(name, options)
+
+        principals = self.printProxyList(read)
+
+        choice = utils.textInput("New principal [q - quit]: ")
+        if choice == "q":
+            return None
+        principal = self.shell.account.getPrincipal(URL(url=choice))
+        if principal:
+            principals.append(principal)
+            principals = [principal.principalURL for principal in principals]
+            if read:
+                self.shell.principal.setReadProxies(principals)
+            else:
+                self.shell.principal.setWriteProxies(principals)
+
+    def usage(self, name):
+        return """Usage: %s [OPTIONS]
+    
+Options:
+    -r add to read-only proxy list
+    -w add to read-write proxy list
+        one of -r or -w must be present.
+""" % (name,)
+
+    def helpDescription(self):
+        return "Add proxies on principal."
+        
+class Remove(CommonProxiesCommand):
+    def __init__(self):
+        super(Command, self).__init__()
+        self.cmds = ("remove",)
+    
+    def execute(self, name, options):
+
+        read = self.parseOptions(name, options)
+
+        principals = self.printProxyList(read)
+
+        choice = utils.numericInput("Select principal: ", 1, len(principals), allow_q=True)
+        if choice == "q":
+            return None
+        del principals[choice-1]
+        principals = [principal.principalURL for principal in principals]
+        if read:
+            self.shell.principal.setReadProxies(principals)
+        else:
+            self.shell.principal.setWriteProxies(principals)
+
+    def usage(self, name):
+        return """Usage: %s [OPTIONS]
+    
+Options:
+    -r remove from read-only proxy list
+    -w remove from read-write proxy list
+        one of -r or -w must be present.
+""" % (name,)
+
+    def helpDescription(self):
+        return "Remove proxies on principal."
+        
+class List(CommonProxiesCommand):
+    def __init__(self):
+        super(Command, self).__init__()
+        self.cmds = ("list",)
+    
+    def execute(self, name, options):
+        
+        utils.printProxyPrincipals(self.shell.account, self.shell.principal, True, True, True, True)
+        return True
+
+    def usage(self, name):
+        return """Usage: %s
+""" % (name,)
+
+    def helpDescription(self):
+        return "List current ACLs on existing resource."

Added: CalDAVClientLibrary/trunk/src/browser/commands/quit.py
===================================================================
--- CalDAVClientLibrary/trunk/src/browser/commands/quit.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/browser/commands/quit.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,37 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from browser.command import Command
+from browser.command import WrongOptions
+
+class Cmd(Command):
+    
+    def __init__(self):
+        super(Command, self).__init__()
+        self.cmds = ("quit", "exit",)
+        
+    def execute(self, name, options):
+        if options:
+            print self.usage(name)
+            raise WrongOptions()
+        raise SystemExit("quitting")
+
+    def usage(self, name):
+        return """Usage: %s
+""" % (name,)
+
+    def helpDescription(self):
+        return "Terminates this session."

Added: CalDAVClientLibrary/trunk/src/browser/commands/server.py
===================================================================
--- CalDAVClientLibrary/trunk/src/browser/commands/server.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/browser/commands/server.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,38 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from browser.command import Command
+from browser.command import WrongOptions
+
+class Cmd(Command):
+    
+    def __init__(self):
+        super(Command, self).__init__()
+        self.cmds = ("server", )
+        
+    def execute(self, name, options):
+        if options:
+            print self.usage(name)
+            raise WrongOptions()
+        print self.shell.server
+        return True
+
+    def usage(self, name):
+        return """Usage: %s
+""" % (name,)
+
+    def helpDescription(self):
+        return "Displays the current server."

Added: CalDAVClientLibrary/trunk/src/browser/commands/whoami.py
===================================================================
--- CalDAVClientLibrary/trunk/src/browser/commands/whoami.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/browser/commands/whoami.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,38 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from browser.command import Command
+from browser.command import WrongOptions
+
+class Cmd(Command):
+    
+    def __init__(self):
+        super(Command, self).__init__()
+        self.cmds = ("whoami", )
+        
+    def execute(self, name, options):
+        if options:
+            print self.usage(name)
+            raise WrongOptions()
+        print self.shell.user
+        return True
+
+    def usage(self, name):
+        return """Usage: %s
+""" % (name,)
+
+    def helpDescription(self):
+        return "Displays the current server login id."

Added: CalDAVClientLibrary/trunk/src/browser/shell.py
===================================================================
--- CalDAVClientLibrary/trunk/src/browser/shell.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/browser/shell.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,110 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from browser.baseshell import BaseShell
+from browser.command import Command
+from client.account import CalDAVAccount
+from getpass import getpass
+from protocol.url import URL
+import browser.commands
+import atexit
+import getopt
+import sys
+
+class Shell(BaseShell):
+    
+    def __init__(self, server, user, pswd, logging):
+        
+        super(Shell, self).__init__("caldav_client")
+        self.prefix = self.wd = "/"
+        self.server = server
+        self.user = user
+        self.pswd = pswd
+    
+        self.registerCommands()
+        
+        # Create the account
+        ssl = server.startswith("https://")
+        server = server[8:] if ssl else server[7:]
+        paths = "/principals/users/%s/" % (self.user,)
+        self.account = CalDAVAccount(server, ssl=ssl, user=self.user, pswd=self.pswd, root=paths, principal=paths, logging=logging)
+        
+        atexit.register(self.saveHistory)
+
+    def registerCommands(self):
+        module = browser.commands
+        for item in module.__all__:
+            mod = __import__("browser.commands." + item, globals(), locals(), ["Cmd",])
+            cmd_class = mod.Cmd
+            if type(cmd_class) is type and issubclass(cmd_class, Command):
+                self.registerCommand(cmd_class())
+
+    def setWD(self, newwd):
+        
+        # Check that the new one exists
+        resource = (newwd if newwd.endswith("/") else newwd + "/")
+        if not self.account.session.testResource(URL(url=resource)):
+            return False
+        self.prefix = self.wd = newwd
+        return True
+
+def usage():
+    return """Usage: shell [OPTIONS]
+
+Options:
+-l              start with HTTP logging on.
+--server=HOST   url of the server include http/https scheme and port [REQUIRED].
+--user=USER     user name to login as - will be prompted if not prsent [OPTIONAL].
+--pswd=PSWD     password for user - will be prompted if not prsent [OPTIONAL].
+"""
+
+def runit():
+    logging = False
+    server = None
+    user = None
+    pswd = None
+
+    opts, _ignore_args = getopt.getopt(sys.argv[1:], 'lh', ["help", "server=", "user=", "pswd="])
+    
+    for name, value in opts:
+        
+        if name == "-l":
+            logging = True
+        elif name == "--server":
+            server = value
+        elif name == "--user":
+            user = value
+        elif name == "--pswd":
+            pswd = value
+        else:
+            print usage()
+            raise SystemExit()
+
+    if not server or not (server.startswith("http://") or server.startswith("https://")):
+        print usage()
+        raise SystemExit()
+    
+    if not user:
+        user = raw_input("User: ")
+    if not pswd:
+        pswd = getpass("Password: ")
+
+    shell = Shell(server, user, pswd, logging)
+    shell.run()
+
+if __name__ == '__main__':
+
+    runit()


Property changes on: CalDAVClientLibrary/trunk/src/browser/shell.py
___________________________________________________________________
Name: svn:executable
   + *

Added: CalDAVClientLibrary/trunk/src/browser/subshell.py
===================================================================
--- CalDAVClientLibrary/trunk/src/browser/subshell.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/browser/subshell.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,34 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from browser.command import Command
+from browser.baseshell import BaseShell
+
+class SubShell(BaseShell):
+    
+    def __init__(self, shell, prefix, cmds):
+        
+        super(SubShell, self).__init__("caldav_client.%s" % (prefix,))
+        self.shell = shell
+        self.prefix = prefix
+        self.preserve_history = True
+        self.registerCommands(cmds)
+
+    def registerCommands(self, cmds):
+        for cmd in cmds:
+            if isinstance(cmd, Command):
+                self.registerCommand(cmd)
+

Added: CalDAVClientLibrary/trunk/src/browser/utils.py
===================================================================
--- CalDAVClientLibrary/trunk/src/browser/utils.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/browser/utils.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,188 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.url import URL
+from protocol.webdav.definitions import davxml
+import readline
+import types
+
+
+def printPrincipalPaths(account, principals, resolve, refresh):
+
+    result = ""
+    if resolve:
+        results = [account.getPrincipal(item, refresh=refresh) for item in principals]
+        results.sort(key=lambda x: x.getSmartDisplayName())
+        strlen = reduce(lambda x,y: max(x, len(y.getSmartDisplayName()) + 1), results, 0)
+        results = ["%- *s (%s)" % (strlen, principal.getSmartDisplayName(), principal.principalURL) for principal in results]
+    else:
+        results = [item.relativeURL() for item in principals]
+        results.sort()
+
+    if len(results) == 1:
+        return results[0]
+    else:
+        for item in results:
+            result += "\n        %s" % (item,)
+    
+    return result
+
+def printPrincipals(account, principals, resolve, refresh):
+
+    result = ""
+    if resolve:
+        results = list(principals)
+        if refresh:
+            for principal in results:
+                principal.loadDetails(True)
+        results.sort(key=lambda x: x.getSmartDisplayName())
+        strlen = reduce(lambda x,y: max(x, len(y.getSmartDisplayName()) + 1), results, 0)
+        results = ["%- *s (%s)" % (strlen, principal.getSmartDisplayName(), principal.principalURL) for principal in results]
+    else:
+        results = [item.principalURL for item in principals]
+        results.sort()
+
+    if len(results) == 1:
+        return results[0]
+    else:
+        for item in results:
+            result += "\n        %s" % (item,)
+    
+    return result
+
+def printProxyPrincipals(account, principal, read=True, write=True, resolve=True, refresh_main=False, refresh=False):
+    if read:
+        print "    Read-Only Proxies: %s" % printPrincipals(account, principal.getReadProxies(refresh_main), resolve, refresh)
+        refresh_main = False
+    if write:
+        print "    Read-Write Proxies: %s" % printPrincipals(account, principal.getWriteProxies(refresh_main), resolve, refresh)
+
+def printProperties(items):
+    sorted = items.keys()
+    sorted.sort()
+    for key in sorted:
+        value = items[key]
+        if type(value) in (types.StringType, types.UnicodeType, types.IntType):
+            print "    %s: %s" % (key, value,)
+        elif type(value) in (types.ListType, types.TupleType,):
+            print "    %s: %s" % (key, printList(value),)
+        else:
+            print "    %s: %s" % (key, value,)
+
+def printList(items):
+    result = ""
+    if len(items) == 1:
+        return items[0]
+    else:
+        sorted = list(items)
+        sorted.sort()
+        for item in sorted:
+            result += "\n        %s" % (item,)
+        return result
+
+def printTwoColumnList(items, indent=0):
+    
+    strlen = reduce(lambda x,y: max(x, len(y[0]) + 1), items, 0)
+    sorted = list(items)
+    sorted.sort(key=lambda x: x[0])
+    for col1, col2 in sorted:
+        print "%s%- *s - %s" % (" "*indent, strlen, col1, col2,)
+
+def printPrincipalList(principals):
+    
+    result = ""
+    for ctr, principal in enumerate(principals):
+        result += "\n% 2d. %s (%s)" % (ctr+1, principal.getSmartDisplayName(), principal.principalURL)
+    return result
+
+def printACEList(aces, account):
+    
+    result = ""
+    for ctr, ace in enumerate(aces):
+        result += "\n% 2d. %s" % (ctr+1, printACE(ace, account))
+    return result
+
+def printACE(ace, account):
+    
+    principalText = ace.principal
+    if principalText == str(davxml.href):
+        principal = account.getPrincipal(URL(url=ace.data))
+        principalText = "%s (%s)" % (principal.getSmartDisplayName(), principal.principalURL)
+    elif principalText == str(davxml.all):
+        principalText = "ALL"
+    elif principalText == str(davxml.authenticated):
+        principalText = "AUTHENTICATED"
+    elif principalText == str(davxml.unauthenticated):
+        principalText = "UNAUTHENTICATED"
+    elif principalText == str(davxml.property):
+        principalText = "PROPERTY: %s" % (ace.data,)
+    result = "Principal: %s\n" % (principalText,)
+    if ace.invert or ace.protected or ace.inherited:
+        result += "    Status:"
+        if ace.invert:
+            result += " INVERTED"
+        if ace.protected:
+            result += " PROTECTED"
+        if ace.inherited:
+            result += " INHERITED"
+        result += "\n"
+    result += "    Grant: " if ace.grant else "    Deny: "
+    result += ", ".join(ace.privs)
+    result += "\n"
+    return result
+
+def textInput(title, insert=None):
+    if insert:
+        title = "%s [%s]:" % (title, insert,)
+    result = raw_input(title)
+    if readline.get_current_history_length():
+        readline.remove_history_item(readline.get_current_history_length() - 1)
+    if not result:
+        result = insert
+    return result
+    
+def numericInput(title, low, high, allow_q = False, insert=None):
+    if allow_q:
+        result = choiceInput(title, [str(num) for num in range(low, high+1)] + ["q",], insert)
+        if result != "q":
+            result = int(result)
+        return result
+    else:
+        return int(choiceInput(title, [str(num) for num in range(low, high+1)], insert))
+    
+def yesNoInput(title, insert=None):
+    return choiceInput(title, ("y", "n"), insert)
+    
+def choiceInput(title, choices, insert=None):
+    while True:
+        result = textInput(title, insert)
+        if not result:
+            continue
+        if result in choices:
+            return result
+        print "Invalid input. Try again."
+    
+def multiChoiceInput(title, choices, insert=None):
+    while True:
+        result = textInput(title, insert)
+        if not result:
+            continue
+        for char in result:
+            if char not in choices:
+                break
+        else:
+            return result
+        print "Invalid input. Try again."

Added: CalDAVClientLibrary/trunk/src/client/__init__.py
===================================================================
--- CalDAVClientLibrary/trunk/src/client/__init__.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/client/__init__.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,16 @@
+##
+# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+

Added: CalDAVClientLibrary/trunk/src/client/account.py
===================================================================
--- CalDAVClientLibrary/trunk/src/client/account.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/client/account.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,32 @@
+##
+# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from client.clientsession import CalDAVSession
+from client.principal import principalCache
+
+class CalDAVAccount(object):
+    
+    def __init__(self, server, port=None, ssl=False, user="", pswd="", principal=None, root=None, logging=False):
+        self.session = CalDAVSession(server, port, ssl, user, pswd, principal, root, logging)
+        self.principal = principalCache.getPrincipal(self.session, self.session.principalPath)
+    
+    def getPrincipal(self, path=None, refresh=False):
+        if path:
+            return principalCache.getPrincipal(self.session, path, refresh=refresh)
+        elif refresh:
+            self.principal = principalCache.getPrincipal(self.session, self.session.principalPath, refresh=refresh)
+
+        return self.principal

Added: CalDAVClientLibrary/trunk/src/client/calendar.py
===================================================================
--- CalDAVClientLibrary/trunk/src/client/calendar.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/client/calendar.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,67 @@
+##
+# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.webdav.definitions import davxml
+from protocol.url import URL
+from protocol.caldav.definitions import caldavxml
+
+class Calendar(object):
+    
+    def __init__(self, path=None, session=None):
+        self.path = path
+        if not path.endswith("/"):
+            self.path += "/"
+        self.session = session
+        self.displayname = None
+        self.description = None
+        self.timezone = None
+
+    def __str__(self):
+        return "Calendar: %s" % (self.path,)
+
+    def __repr__(self):
+        return "Calendar: %s" % (self.path,)
+
+    def exists(self):
+        return self.session.testResource(URL(url=self.path))
+
+    def readCalendar(self):
+        pass
+    def writeCalendar(self, calendar):
+        pass
+
+    def readComponent(self, name=None, uid=None):
+        pass
+    def writeComponent(self, component, name=None):
+        pass
+
+    def getDisplayName(self):
+        if self.displayname is None and self.session:
+            self._getProperties()
+        return self.displayname
+
+    def getDescription(self):
+        if self.description is None and self.session:
+            self._getProperties()
+        return self.description
+    
+    def _getProperties(self):
+        assert(self.session is not None)
+        
+        results, _ignore_bad = self.session.getProperties(URL(url=self.path), (davxml.displayname, caldavxml.calendar_description, caldavxml.calendar_timezone,))
+        self.displayname = results.get(davxml.displayname, "")
+        self.description = results.get(caldavxml.calendar_description, "")
+        self.timezone = results.get(caldavxml.calendar_timezone, None)

Added: CalDAVClientLibrary/trunk/src/client/calendaruseraddress.py
===================================================================
--- CalDAVClientLibrary/trunk/src/client/calendaruseraddress.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/client/calendaruseraddress.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,45 @@
+##
+# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+
+class CalendarUserAddress(object):
+    
+    def __init__(self, cuaddr=None, name=None, attendee=None):
+        self.cuaddr = cuaddr
+        self.name = name
+        if attendee:
+            self.setAttendee(attendee)
+
+    def getCUAddr(self):
+        return self.cuaddr
+
+    def setCUAddr(self, value):
+        self.cuaddr = value
+
+    def getName(self):
+        return self.name
+
+    def setCn(self, value):
+        self.name = value
+        
+    def getFullText(self):
+        return ("%s <%s>" % (self.name, self.cuaddr,)) if self.name else ("<%s>" % (self.cuaddr,))
+    
+    def getAttendeeProperty(self):
+        pass
+    
+    def setAttendee(self, attendee):
+        pass

Added: CalDAVClientLibrary/trunk/src/client/clientsession.py
===================================================================
--- CalDAVClientLibrary/trunk/src/client/clientsession.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/client/clientsession.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,675 @@
+##
+# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.webdav.session import Session
+from protocol.webdav.propfind import PropFind
+from protocol.webdav.definitions import davxml
+from protocol.caldav.definitions import headers
+from protocol.http.data.string import ResponseDataString
+from protocol.url import URL
+from protocol.webdav.definitions import statuscodes
+from protocol.webdav.propfindparser import PropFindParser
+from protocol.webdav.principalmatch import PrincipalMatch
+from protocol.http.authentication.basic import Basic
+from protocol.http.authentication.digest import Digest
+from protocol.webdav.proppatch import PropPatch
+from xml.etree.ElementTree import Element
+from protocol.webdav.get import Get
+from protocol.webdav.propnames import PropNames
+from protocol.webdav.propall import PropAll
+from protocol.webdav.acl import ACL
+import types
+import httplib
+
+class CalDAVSession(Session):
+    
+    class logger(object):
+        
+        def write(self, data):
+            print data.replace("\r\n", "\n"),
+
+    def __init__(self, server, port=None, ssl=False, user="", pswd="", principal=None, root=None, logging=False):
+        super(CalDAVSession, self).__init__(server, port, ssl, log=CalDAVSession.logger())
+
+        self.loghttp = logging
+
+        self.user = user
+        self.pswd = pswd
+        
+        # Initialize state
+        self.connect = None
+
+        # Paths
+        self.rootPath = URL(url=root)
+        self.principalPath = URL(url=principal)
+        
+        self._initCalDAVState()
+
+    def _initCalDAVState(self):
+
+        # We need to cache the server capabilities and properties
+        if not self.principalPath:
+            self._discoverPrincipal()
+        
+    def _discoverPrincipal(self):
+        
+        hrefs = self.getHrefListProperty(self.rootPath, davxml.principal_collection_set)
+        if not hrefs:
+            return
+
+        # For each principal collection find one that matches self
+        for href in hrefs:
+
+            results = self.getSelfHrefs(href)
+            if results:
+                self.principalPath = results[0]
+                if self.log:
+                    self.log.write("Found principal path: %s" % (self.principalPath.absoluteURL(),))
+                return
+    
+    def testResource(self, rurl):
+
+        assert(isinstance(rurl, URL))
+
+        request = PropFind(self, rurl.relativeURL(), headers.Depth0, (davxml.resourcetype,))
+    
+        # Process it
+        self.runSession(request)
+        
+        return request.getStatusCode() == statuscodes.MultiStatus
+
+    def getPropertyNames(self, rurl):
+
+        assert(isinstance(rurl, URL))
+
+        results = ()
+
+        # Create WebDAV propfind
+        request = PropNames(self, rurl.relativeURL(), headers.Depth0)
+        result = ResponseDataString()
+        request.setOutput(result)
+    
+        # Process it
+        self.runSession(request)
+    
+        # If its a 207 we want to parse the XML
+        if request.getStatusCode() == statuscodes.MultiStatus:
+
+            parser = PropFindParser()
+            parser.parseData(result.getData())
+    
+            # Look at each propfind result
+            for item in parser.getResults().itervalues():
+
+                # Get child element name (decode URL)
+                name = URL(url=item.getResource(), decode=True)
+            
+                # Must match rurl
+                if name.equalRelative(rurl):
+
+                    results = tuple([name for name in item.getNodeProperties().iterkeys()])
+
+        else:
+            self.handleHTTPError(request)
+    
+        return results
+
+    def getProperties(self, rurl, props):
+
+        assert(isinstance(rurl, URL))
+
+        results = {}
+        bad = None
+
+        # Create WebDAV propfind
+        if props:
+            request = PropFind(self, rurl.relativeURL(), headers.Depth0, props)
+        else:
+            request = PropAll(self, rurl.relativeURL(), headers.Depth0)
+        result = ResponseDataString()
+        request.setOutput(result)
+    
+        # Process it
+        self.runSession(request)
+    
+        # If its a 207 we want to parse the XML
+        if request.getStatusCode() == statuscodes.MultiStatus:
+
+            parser = PropFindParser()
+            parser.parseData(result.getData())
+    
+            # Look at each propfind result
+            for item in parser.getResults().itervalues():
+
+                # Get child element name (decode URL)
+                name = URL(url=item.getResource(), decode=True)
+            
+                # Must match rurl
+                if name.equalRelative(rurl):
+                    for name, value in item.getTextProperties().iteritems():
+                        results[name] = value
+                    for name, value in item.getHrefProperties().iteritems():
+                        if name not in results:
+                            results[name] = value
+                    for name, value in item.getNodeProperties().iteritems():
+                        if name not in results:
+                            results[name] = value
+                    bad = item.getBadProperties()
+        else:
+            self.handleHTTPError(request)
+    
+        return results, bad
+
+    def getPropertiesOnHierarchy(self, rurl, props):
+
+        assert(isinstance(rurl, URL))
+
+        results = {}
+
+        # Create WebDAV propfind
+        request = PropFind(self, rurl.relativeURL(), headers.Depth1, props)
+        result = ResponseDataString()
+        request.setOutput(result)
+    
+        # Process it
+        self.runSession(request)
+    
+        # If its a 207 we want to parse the XML
+        if request.getStatusCode() == statuscodes.MultiStatus:
+
+            parser = PropFindParser()
+            parser.parseData(result.getData())
+    
+            # Look at each propfind result
+            for item in parser.getResults().itervalues():
+
+                # Get child element name (decode URL)
+                name = URL(url=item.getResource(), decode=True)
+                propresults = {}
+                results[name.relativeURL()] = propresults
+            
+                for prop in props:
+
+                    if item.getTextProperties().has_key(str(prop)):
+                        propresults[prop] = item.getTextProperties().get(str(prop))
+
+                    elif item.getNodeProperties().has_key(str(prop)):
+                        propresults[prop] = item.getNodeProperties()[str(prop)]
+        else:
+            self.handleHTTPError(request)
+    
+        return results
+
+    def getHrefListProperty(self, rurl, propname):
+
+        assert(isinstance(rurl, URL))
+
+        results = ()
+
+        # Create WebDAV propfind
+        request = PropFind(self, rurl.relativeURL(), headers.Depth0, (propname,))
+        result = ResponseDataString()
+        request.setOutput(result)
+    
+        # Process it
+        self.runSession(request)
+    
+        # If its a 207 we want to parse the XML
+        if request.getStatusCode() == statuscodes.MultiStatus:
+
+            parser = PropFindParser()
+            parser.parseData(result.getData())
+    
+            # Look at each propfind result and extract any Hrefs
+            for item in parser.getResults().itervalues():
+
+                # Get child element name (decode URL)
+                name = URL(url=item.getResource(), decode=True)
+            
+                # Must match rurl
+                if name.equalRelative(rurl):
+
+                    if item.getNodeProperties().has_key(str(propname)):
+                        
+                        propnode = item.getNodeProperties()[str(propname)]
+                        results += tuple([URL(url=href.text, decode=True) for href in propnode.findall(str(davxml.href)) if href.text])
+        else:
+            self.handleHTTPError(request)
+    
+        return results
+
+    # Do principal-match report with self on the passed in url
+    def getSelfProperties(self, rurl, props):
+        
+        assert(isinstance(rurl, URL))
+
+        results = {}
+    
+        # Create WebDAV principal-match
+        request = PrincipalMatch(self, rurl.relativeURL(), props)
+        result = ResponseDataString()
+        request.setOutput(result)
+    
+        # Process it
+        self.runSession(request)
+    
+        # If its a 207 we want to parse the XML
+        if request.getStatusCode() == statuscodes.MultiStatus:
+
+            parser = PropFindParser()
+            parser.parseData(result.getData())
+    
+            # Look at each principal-match result and return first one that is appropriate
+            for item in parser.getResults().itervalues():
+
+                for prop in props:
+
+                    if item.getNodeProperties().has_key(str(prop)):
+
+                        href = item.getNodeProperties()[str(prop)].find(str(davxml.href))
+                        
+                        if href is not None:
+                            results[prop] = URL(url=href.text, decode=True)
+                
+                # We'll take the first one, whatever that is
+                break
+
+        else:
+            self.handleHTTPError(request)
+            return None
+        
+        return results
+
+    # Do principal-match report with self on the passed in url
+    def getSelfHrefs(self, rurl):
+
+        assert(isinstance(rurl, URL))
+
+        results = ()
+
+        # Create WebDAV principal-match
+        request = PrincipalMatch(self, rurl.realtiveURL(), (davxml.principal_URL,))
+        result = ResponseDataString()
+        request.setOutput(result)
+    
+        # Process it
+        self.runSession(request)
+    
+        # If its a 207 we want to parse the XML
+        if request.getStatusCode() == statuscodes.MultiStatus:
+
+            parser = PropFindParser()
+            parser.parseData(result.getData())
+    
+            # Look at each propfind result and extract any Hrefs
+            for item in parser.getResults().itervalues():
+
+                # Get child element name (decode URL)
+                name = URL(url=item.getResource(), decode=True)
+                results += (name.path,)
+
+        else:
+            self.handleHTTPError(request)
+            return None
+        
+        return results
+    
+    # Do principal-match report with self on the passed in url
+    def getSelfPrincipalResource(self, rurl):
+        
+        assert(isinstance(rurl, URL))
+
+        hrefs = self.getHrefListProperty(rurl, davxml.principal_collection_set)
+        if not hrefs:
+            return None
+        
+        # For each principal collection find one that matches self
+        for href in hrefs:
+
+            results = self.getSelfHrefs(href)
+            if results:
+                return results[0]
+        
+        return None
+
+    def setProperties(self, rurl, props):
+
+        assert(isinstance(rurl, URL))
+
+        results = ()
+
+        # Convert property data into something sensible
+        converted = []
+        for name, value in props:
+            node = None
+            if isinstance(value, types.StringType):
+                node = Element(name)
+                node.text = value
+            elif isinstance(value, URL):
+                node = Element(davxml.href)
+                node.text = value.absoluteURL()
+            elif isinstance(value, types.ListType) or isinstance(value, types.TupleType):
+                hrefs = []
+                for item in value:
+                    if isinstance(item, URL):
+                        href = Element(davxml.href)
+                        href.text = item.relativeURL()
+                        hrefs.append(href)
+                    else:
+                        break
+                else:
+                    node = Element(name)
+                    map(node.append, hrefs)
+            
+            if node is not None:
+                converted.append(node)
+
+        # Create WebDAV propfind
+        request = PropPatch(self, rurl.relativeURL(), converted)
+        result = ResponseDataString()
+        request.setOutput(result)
+    
+        # Process it
+        self.runSession(request)
+    
+        # If its a 207 we want to parse the XML
+        if request.getStatusCode() == statuscodes.MultiStatus:
+
+            parser = PropFindParser()
+            parser.parseData(result.getData())
+    
+            # Look at each propfind result
+            for item in parser.getResults().itervalues():
+
+                # Get child element name (decode URL)
+                name = URL(url=item.getResource(), decode=True)
+            
+                # Must match rurl
+                if name.equalRelative(rurl):
+
+                    for prop in item.getNodeProperties():
+                        results += (prop,)
+
+        else:
+            self.handleHTTPError(request)
+    
+        return results
+
+    def setACL(self, rurl, aces):
+        
+        assert(isinstance(rurl, URL))
+
+        # Create WebDAV ACL
+        request = ACL(self, rurl.relativeURL(), aces)
+    
+        # Process it
+        self.runSession(request)
+        
+        if request.getStatusCode() not in (statuscodes.OK, statuscodes.Created, statuscodes.NoContent):
+            self.handleHTTPError(request)
+
+    def readData(self, rurl):
+
+        assert(isinstance(rurl, URL))
+
+        # Create WebDAV GET
+        request = Get(self, rurl.relativeURL())
+        dout = ResponseDataString()
+        request.setData(dout)
+    
+        # Process it
+        self.runSession(request)
+    
+        # Check response status
+        if request.getStatusCode() != statuscodes.OK:
+            self.handleHTTPError(request)
+            return None
+    
+        # Look for ETag
+        if request.getNewETag() is not None:
+
+            etag = request.getNewETag()
+    
+            # Handle server bug: ETag value MUST be quoted per HTTP/1.1 S3.11
+            if not etag.startswith('"'):
+                etag = "\"%s\"" % (etag,)
+        else:
+            etag = None
+    
+        # Return data as a string and etag
+        return dout.getData(), etag
+
+    def openSession(self):
+        # Create connection
+        if self.ssl:
+            self.connect = httplib.HTTPSConnection(self.server, self.port)
+        else:
+            self.connect = httplib.HTTPConnection(self.server, self.port)
+        self.connect.connect()
+        
+        # Write to log file
+        if self.loghttp and self.log:
+            self.log.write("\n        <-------- BEGIN HTTP CONNECTION -------->\n")
+            self.log.write("Server: %s\n" % (self.server,))
+
+    def closeSession(self):
+        if self.connect:
+            self.connect.close()
+            self.connect = None
+
+            # Write to log file
+            if self.loghttp and self.log:
+                self.log.write("\n        <-------- END HTTP CONNECTION -------->\n")
+    
+    def runSession(self, request):
+        
+        ctr = 5
+        while ctr:
+            ctr -= 1
+            
+            self.doSession(request)
+            
+            if request and request.isRedirect():
+                location = request.getResponseHeader(headers.Location)
+                if location:
+                    u = URL(location)
+                    if not u.scheme or u.scheme in ("http", "https",):
+                        # Get new server and base RURL
+                        different_server = (self.server != u.server) if u.server else False
+                        
+                        # Use new host in this session
+                        if different_server:
+                            self.setServer(u.server)
+                        
+                        # Reset the request with new info
+                        request.setURL(u.relativeURL())
+                        request.clearResponse()
+    
+                        # Write to log file
+                        if self.loghttp and self.log:
+                            self.log.write("\n        <-------- HTTP REDIRECT -------->\n")
+                            self.log.write("Location: %s\n" % (location,))
+    
+                        # Recyle through loop
+                        continue
+
+            # Exit when redirect does not occur
+            break
+        
+    def doSession(self, request):
+        # Do initialisation if not already done
+        if not self.initialised:
+        
+            if not self.initialise(self.server, self.rootPath.relativeURL()):
+            
+                # Break connection with server
+                self.closeConnection()
+                return
+
+        # Do the request if present
+        if request:
+        
+            # Handle delayed authorization
+            first_time = True
+            while True:
+            
+                # Run the request actions - this will make any connection that is needed
+                self.sendRequest(request)
+                
+                # Check for auth failure if none before
+                if request.getStatusCode() == statuscodes.Unauthorized:
+                
+                    # If we had authorization before, then chances are auth details are wrong - so delete and try again with new auth
+                    if self.hasAuthorization():
+                    
+                        self.authorization = None
+                        
+                        # Display error so user knows why the prompt occurs again - but not the first time
+                        # as we might have a digest re-auth.
+                        if not first_time:
+                            self.displayHTTPError(request)
+                    
+    
+                    # Get authorization object (prompt the user) and redo the request
+                    self.authorization, cancelled = self.getAuthorizor(first_time, request.getResponseHeaders(headers.WWWAuthenticate))
+                    
+                    # Check for auth cancellation
+                    if cancelled:
+                        self.authorization = None
+                    
+                    else:
+                        first_time = False
+                        
+                        request.clearResponse()
+                        
+                        # Repeat the request loop with new authorization
+                        continue
+
+                # If we get here we are complete with auth loop
+                break
+
+        # Now close it - eventually we will do keep-alive support
+    
+        # Break connection with server
+        self.closeConnection()
+
+    def doRequest(self, request):
+    
+        # Write request headers
+        self.connect.putrequest(request.method, request.url, skip_host=True, skip_accept_encoding=True)
+        hdrs = request.getRequestHeaders()
+        for header, value in hdrs:
+            self.connect.putheader(header, value)
+        self.connect.endheaders()
+
+        # Write to log file
+        if self.loghttp and self.log:
+            self.log.write("\n        <-------- BEGIN HTTP REQUEST -------->\n")
+            self.log.write("%s\n" % (request.getRequestStartLine(),))
+            for header, value in hdrs:
+                self.log.write("%s: %s\n" % (header, value))
+            self.log.write("\n")
+    
+        # Write the data
+        self.writeRequestData(request)
+    
+        # Blank line in log between 
+        if self.loghttp and self.log:
+            self.log.write("\n        <-------- BEGIN HTTP RESPONSE -------->\n")
+
+        # Get response
+        response = self.connect.getresponse()
+        
+        # Get response headers
+        request.setResponseStatus(response.version, response.status, response.reason)
+        request.setResponseHeaders(response.msg.headers)
+        if self.loghttp and self.log:
+            self.log.write("HTTP/%s %s %s\r\n" % (
+                {11:"1.1",10:"1.0",9:"0.9"}.get(response.version, "?"),
+                response.status,
+                response.reason
+            ))
+            for hdr in response.msg.headers:
+                self.log.write(hdr)
+
+        # Now get the data
+        self.readResponseData(request, response)
+    
+        # Trailer in log 
+        if self.loghttp and self.log:
+            self.log.write("\n        <-------- END HTTP RESPONSE -------->\n")
+
+    def handleHTTPError(self, request):
+        print "Ignoring error"
+
+    def getAuthorizor(self, first_time, wwwhdrs):
+        
+        for item in wwwhdrs:
+            if item.lower().startswith("basic"):
+                return Basic(self.user, self.pswd), False
+            elif item.lower().startswith("digest"):
+                return Digest(self.user, self.pswd, wwwhdrs), False
+        else:
+            return None, True
+
+    def writeRequestData(self, request):
+    
+        # Write the data if any present
+        if request.hasRequestData():
+        
+            stream = request.getRequestData()
+            if stream:
+                # Tell data we are using it
+                stream.start()
+                
+                # Buffered write from stream
+                more = True
+                while more:
+                    data, more = stream.read()
+                    if data:
+                        self.connect.send(data)
+                        
+                        if self.loghttp and self.log:
+                            self.log.write(data)
+
+                # Tell data we are done using it
+                stream.stop()
+    
+    def readResponseData(self, request, response):
+    
+        # Check for data and write it
+        data = response.read()
+        
+        if request.hasResponseData():
+            stream = request.getResponseData()
+            stream.start()
+            stream.write(data)
+            stream.stop()
+        else:
+            response.read()
+            
+        if self.loghttp and self.log:
+            self.log.write(data)    
+
+    def setServerType(self, type):
+        self.type = type
+    
+    def setServerDescriptor(self, txt):
+        self.descriptor = txt
+    
+    def setServerCapability(self, txt):
+        self.capability = txt
+

Added: CalDAVClientLibrary/trunk/src/client/fullclient.py
===================================================================
--- CalDAVClientLibrary/trunk/src/client/fullclient.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/client/fullclient.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,46 @@
+##
+# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from client.account import CalDAVAccount
+
+if __name__ == '__main__':
+    account = CalDAVAccount("", ssl=True, user="", pswd="", root="", principal="")
+
+    print account.getPrincipal()
+
+#    memberships = [CalDAVPrincipal(account.session, path) for path in account.getPrincipal().memberships]
+#    for member in memberships:
+#        member.loadDetails()
+#    memberships = [member.displayname for member in memberships]
+#    print "Memberships: %s" % (memberships,)
+
+#    calendars = account.getPrincipal().listCalendars()
+#    for calendar in calendars:
+#        print "%s:" % (calendar,)
+#        txt = calendar.getDisplayName()
+#        if txt:
+#            print "  Display Name: %s" % (txt,)
+#        txt = calendar.getDescription()
+#        if txt:
+#            print "  Description: %s" % (txt,)
+
+#    fbset = account.getPrincipal().listFreeBusySet()
+#    print fbset
+#    account.getPrincipal().cleanFreeBusySet()
+
+    proxies = account.getPrincipal().getReadProxies()
+    for proxy in proxies:
+        print proxy

Added: CalDAVClientLibrary/trunk/src/client/principal.py
===================================================================
--- CalDAVClientLibrary/trunk/src/client/principal.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/client/principal.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,227 @@
+##
+# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.caldav.definitions import caldavxml
+from client.calendar import Calendar
+from protocol.url import URL
+from protocol.webdav.definitions import davxml
+from protocol.caldav.definitions import headers
+import types
+
+
+class PrincipalCache(object):
+    
+    def __init__(self):
+        self.cache = {}
+        
+    def getPrincipal(self, session, path, refresh=False):
+        if path.toString() not in self.cache:
+            principal = CalDAVPrincipal(session, path)
+            principal.loadDetails()
+            self.cache[path.toString()] = principal
+            self.cache[principal.principalURL.toString()] = principal
+            for uri in principal.alternateURIs:
+                self.cache[uri.toString()] = principal
+        elif refresh:
+            self.cache[path.toString()].loadDetails(refresh=True)
+        return self.cache[path.toString()]
+
+principalCache = PrincipalCache()
+
+class CalDAVPrincipal(object):
+    
+    def __init__(self, session, path):
+        
+        self.session = session
+        self.principalPath = path
+        self._initFields()
+
+    def __str__(self):
+        return """
+    Principal Path    : %s
+    Display Name      : %s
+    Principal URL     : %s
+    Alternate URLs    : %s
+    Group Members     : %s
+    Memberships       : %s
+    Calendar Homes    : %s
+    Outbox URL        : %s
+    Inbox URL         : %s
+    Calendar Addresses: %s
+""" % (
+          self.principalPath,
+          self.displayname,
+          self.principalURL,
+          self.alternateURIs,
+          self.memberset,
+          self.memberships,
+          self.homeset,
+          self.outboxURL,
+          self.inboxURL,
+          self.cuaddrs
+      )
+
+    def _initFields(self):
+        self.loaded = False
+        self.valid = False
+        self.displayname = "Invalid Principal"
+        self.principalURL = ""
+        self.alternateURIs = ()
+        self.memberset = ()
+        self.memberships = ()
+        self.homeset = ()
+        self.outboxURL = ""
+        self.inboxURL = ""
+        self.cuaddrs = ()
+        
+        self.proxyFor = None
+        self.proxyreadURL = ""
+        self.proxywriteURL = ""
+        
+    def loadDetails(self, refresh=False):
+        if self.loaded and not refresh:
+            return
+        self._initFields()
+
+        results, _ignore_bad = self.session.getProperties(
+            self.principalPath,
+            (
+                davxml.resourcetype,
+                davxml.displayname,
+                davxml.principal_URL,
+                davxml.alternate_URI_set,
+                davxml.group_member_set,
+                davxml.group_membership,
+                caldavxml.calendar_home_set,
+                caldavxml.schedule_outbox_URL,
+                caldavxml.schedule_inbox_URL,
+                caldavxml.calendar_user_address_set,
+            ),
+        )
+        if results:
+            # First check that we have a valid principal and see if its a proxy principal too
+            type = results.get(davxml.resourcetype, None)
+            self.valid = type.find(str(davxml.principal)) is not None
+            if (self.session.hasDAVVersion(headers.calendar_proxy) and
+                (type.find(str(caldavxml.calendar_proxy_read)) is not None or
+                 type.find(str(caldavxml.calendar_proxy_write)) is not None)):
+                parentPath = self.principalPath.dirname()
+                self.proxyFor = principalCache.getPrincipal(self.session, parentPath, refresh)
+                
+            if self.valid:
+                self.displayname = results.get(davxml.displayname, None)
+                self.principalURL = results.get(davxml.principal_URL, None)
+                self.alternateURIs = results.get(davxml.alternate_URI_set, None)
+                self.memberset = results.get(davxml.group_member_set, None)
+                self.memberships = results.get(davxml.group_membership, None)
+                self.homeset = results.get(caldavxml.calendar_home_set, None)
+                self.outboxURL = results.get(caldavxml.schedule_outbox_URL, None)
+                self.inboxURL = results.get(caldavxml.schedule_inbox_URL, None)
+                self.cuaddrs = results.get(caldavxml.calendar_user_address_set, None)
+
+        # Get proxy resource details if proxy support is available
+        if self.session.hasDAVVersion(headers.calendar_proxy) and not self.proxyFor:
+            results = self.session.getPropertiesOnHierarchy(self.principalPath, (davxml.resourcetype,))
+            for path, items in results.iteritems():
+                if davxml.resourcetype in items:
+                    rtype = items[davxml.resourcetype]
+                    if rtype.find(str(caldavxml.calendar_proxy_read)) is not None:
+                        self.proxyreadURL = URL(url=path)
+                    elif rtype.find(str(caldavxml.calendar_proxy_write)) is not None:
+                        self.proxywriteURL = URL(url=path)
+                        
+        self.loaded = True
+
+    def getSmartDisplayName(self):
+        if self.proxyFor:
+            return "%s#%s" % (self.proxyFor.displayname, self.displayname,)
+        else:
+            return self.displayname
+
+    def listCalendars(self, root=None):
+        calendars = []
+        home = self.homeset[0] if type(self.homeset) in (types.TupleType,) else self.homeset
+        if not home.path.endswith("/"):
+            home.path += "/"
+
+        results = self.session.getPropertiesOnHierarchy(home, (davxml.resourcetype,))
+        for path, items in results.iteritems():
+            if davxml.resourcetype in items:
+                rtype = items[davxml.resourcetype]
+                if rtype.find(str(davxml.collection)) is not None and rtype.find(str(caldavxml.calendar)) is not None:
+                    calendars.append(Calendar(path=path, session=self.session))
+        return calendars
+
+    def listFreeBusySet(self):
+        return self._getFreeBusySet()
+    
+    def addToFreeBusySet(self, calendars):
+        current = self._getFreeBusySet()
+        for calendar in calendars:
+            current.append(calendar)
+        self._setFreeBusySet(current)
+    
+    def removeFromFreeBusySet(self, calendars):
+        calendar_paths = [calendar.path for calendar in calendars]
+        current = self._getFreeBusySet()
+        current = [cal for cal in current if cal.path not in calendar_paths]
+        self._setFreeBusySet(current)
+
+    def cleanFreeBusySet(self):
+        fbset = self.listFreeBusySet()
+        badfbset = []
+        for calendar in fbset:
+            if not calendar.exists():
+                badfbset.append(calendar)
+    
+        if badfbset:
+            self.removeFromFreeBusySet(badfbset)
+
+    def _getFreeBusySet(self):
+        hrefs = self.session.getHrefListProperty(self.inboxURL, caldavxml.calendar_free_busy_set)
+        return [Calendar(href.relativeURL(), session=self.session) for href in hrefs]
+    
+    def _setFreeBusySet(self, calendars):
+        hrefs = [URL(url=calendar.path) for calendar in calendars]
+        self.session.setProperties(self.inboxURL, ((caldavxml.calendar_free_busy_set, hrefs),))
+
+    def getReadProxies(self, refresh=True):
+        if not self.proxyreadURL:
+            return ()
+        
+        principal = principalCache.getPrincipal(self.session, self.proxyreadURL, refresh=refresh)
+        return [principalCache.getPrincipal(self.session, member) for member in principal.memberset]
+            
+    
+    def setReadProxies(self, principals):
+        if not self.proxyreadURL:
+            return ()
+        
+        self.session.setProperties(self.proxyreadURL, ((davxml.group_member_set, principals),))            
+    
+    def getWriteProxies(self, refresh=True):
+        if not self.proxywriteURL:
+            return ()
+        
+        principal = principalCache.getPrincipal(self.session, self.proxywriteURL, refresh=refresh)
+        return [principalCache.getPrincipal(self.session, member) for member in principal.memberset]
+
+    def setWriteProxies(self, principals):
+        if not self.proxywriteURL:
+            return ()
+        
+        self.session.setProperties(self.proxywriteURL, ((davxml.group_member_set, principals),))            
+    

Added: CalDAVClientLibrary/trunk/src/client/simple.py
===================================================================
--- CalDAVClientLibrary/trunk/src/client/simple.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/client/simple.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,61 @@
+##
+# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.webdav.session import Session
+from protocol.webdav.options import Options
+import httplib
+
+def run(session, request):
+    
+    # Create connection
+    if session.ssl:
+        connect = httplib.HTTPSConnection(session.server, session.port)
+    else:
+        connect = httplib.HTTPConnection(session.server, session.port)
+    connect.set_debuglevel(1)
+
+    # Do headers
+    connect.putrequest(request.method, request.url, skip_host=True, skip_accept_encoding=True)
+    hdrs = request.getRequestHeaders()
+    for header, value in hdrs.iteritems():
+        connect.putheader(header, value)
+    connect.endheaders()
+    
+    # Do request body
+    stream = request.getRequestDataStream()
+    if stream:
+        stream.start()
+        more = True
+        while more:
+            data, more = stream.read()
+            if data:
+                connect.send(data)
+        stream.stop()
+
+    # Get response
+    response = connect.getresponse()
+    
+    # Get response headers
+    request.setResponseStatus(response.version, response.status, response.reason)
+    request.setResponseHeaders(response.getheaders())
+    
+    # Get response body
+
+if __name__ == '__main__':
+    session = Session("www.mulberrymail.com")
+    request = Options(session, "/")
+    
+    run(session, request)

Added: CalDAVClientLibrary/trunk/src/protocol/__init__.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/__init__.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/__init__.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,15 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##

Added: CalDAVClientLibrary/trunk/src/protocol/caldav/__init__.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/caldav/__init__.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/caldav/__init__.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,28 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.webdav.propfindparser import PropFindParser
+from protocol.webdav.definitions import davxml
+from protocol.caldav.definitions import caldavxml
+
+PropFindParser.textProperties.add(caldavxml.calendar_description)
+PropFindParser.textProperties.add(caldavxml.calendar_timezone)
+
+PropFindParser.hrefListProperties.add(caldavxml.calendar_home_set)
+PropFindParser.hrefListProperties.add(caldavxml.calendar_user_address_set)
+PropFindParser.hrefListProperties.add(caldavxml.calendar_free_busy_set)
+PropFindParser.hrefProperties.add(caldavxml.schedule_outbox_URL)
+PropFindParser.hrefProperties.add(caldavxml.schedule_inbox_URL)

Added: CalDAVClientLibrary/trunk/src/protocol/caldav/definitions/__init__.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/caldav/definitions/__init__.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/caldav/definitions/__init__.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,15 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##

Added: CalDAVClientLibrary/trunk/src/protocol/caldav/definitions/caldavxml.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/caldav/definitions/caldavxml.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/caldav/definitions/caldavxml.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,83 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from xml.etree.ElementTree import QName
+
+CalDAVNamespace = "urn:ietf:params:xml:ns:caldav"
+
+# RFC4791
+
+mkcalendar           = QName(CalDAVNamespace, "mkcalendar")
+mkcalendar_response  = QName(CalDAVNamespace, "mkcalendar-response")
+
+calendar             = QName(CalDAVNamespace, "calendar")
+
+calendar_description             = QName(CalDAVNamespace, "calendar-description")
+calendar_timezone                = QName(CalDAVNamespace, "calendar-timezone")
+supported_calendar_component_set = QName(CalDAVNamespace, "supported-calendar-component-set")
+supported_calendar_data          = QName(CalDAVNamespace, "supported-calendar-data")
+max_resource_size                = QName(CalDAVNamespace, "max-resource-size")
+min_date_time                    = QName(CalDAVNamespace, "min-date-time")
+max_date_time                    = QName(CalDAVNamespace, "max-date-time")
+max_instances                    = QName(CalDAVNamespace, "max-instances")
+max_attendees_per_instance       = QName(CalDAVNamespace, "max-attendees-per-instance")
+
+read_free_busy       = QName(CalDAVNamespace, "read-free-busy")
+calendar_home_set    = QName(CalDAVNamespace, "calendar-home-set")
+
+supported_collation  = QName(CalDAVNamespace, "supported-collation")
+
+calendar_query       = QName(CalDAVNamespace, "calendar-query")
+calendar_data        = QName(CalDAVNamespace, "calendar-data")
+comp                 = QName(CalDAVNamespace, "comp")
+allcomp              = QName(CalDAVNamespace, "allcomp")
+prop                 = QName(CalDAVNamespace, "prop")
+expand               = QName(CalDAVNamespace, "expand")
+limit_recurrence_set = QName(CalDAVNamespace, "limit-recurrence-set")
+limit_freebusy_set   = QName(CalDAVNamespace, "limit-freebusy-set")
+filter               = QName(CalDAVNamespace, "filter")
+comp_filter          = QName(CalDAVNamespace, "comp-filter")
+prop_filter          = QName(CalDAVNamespace, "prop-filter")
+param_filter         = QName(CalDAVNamespace, "param-filter")
+is_not_defined       = QName(CalDAVNamespace, "is-not-defined")
+text_match           = QName(CalDAVNamespace, "text-match")
+timezone             = QName(CalDAVNamespace, "timezone")
+time_range           = QName(CalDAVNamespace, "time-range")
+
+calendar_multiget    = QName(CalDAVNamespace, "calendar-multiget")
+
+free_busy_query      = QName(CalDAVNamespace, "free-busy-query")
+
+# draft caldav-schedule
+calendar_free_busy_set    = QName(CalDAVNamespace, "calendar-free-busy-set")
+originator                = QName(CalDAVNamespace, "originator")
+recipient                 = QName(CalDAVNamespace, "recipient")
+schedule                  = QName(CalDAVNamespace, "schedule")
+
+schedule_inbox_URL        = QName(CalDAVNamespace, "schedule-inbox-URL")
+schedule_outbox_URL       = QName(CalDAVNamespace, "schedule-outbox-URL")
+calendar_user_address_set = QName(CalDAVNamespace, "calendar-user-address-set")
+
+schedule_response         = QName(CalDAVNamespace, "schedule-response")
+response                  = QName(CalDAVNamespace, "timezone")
+request_status            = QName(CalDAVNamespace, "request-status")
+
+# Extensions
+
+CalendarServerNamespace   = "http://calendarserver.org/ns/"
+
+calendar_proxy_read       = QName(CalendarServerNamespace, "calendar-proxy-read")
+calendar_proxy_write      = QName(CalendarServerNamespace, "calendar-proxy-write")

Added: CalDAVClientLibrary/trunk/src/protocol/caldav/definitions/headers.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/caldav/definitions/headers.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/caldav/definitions/headers.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,30 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.webdav.definitions.headers import * #@UnusedWildImport
+
+# RFC4791
+CalendarAccess   = "calendar-access"
+
+# draft caldav-schedule
+CalendarSchedule = "calendar-schedule"
+
+Originator = "Originator"
+Recipient  = "Recipient"
+
+# Extensions
+
+calendar_proxy = "calendar-proxy"

Added: CalDAVClientLibrary/trunk/src/protocol/caldav/definitions/methods.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/caldav/definitions/methods.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/caldav/definitions/methods.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,21 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.webdav.definitions.methods import * #@UnusedWildImport
+
+# RFC4791 - CalDAV Request Methods
+
+MKCALENDAR = "MKCALENDAR";

Added: CalDAVClientLibrary/trunk/src/protocol/caldav/makecalendar.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/caldav/makecalendar.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/caldav/makecalendar.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,76 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.webdav.requestresponse import RequestResponse
+from protocol.caldav.definitions import methods
+from StringIO import StringIO
+from protocol.http.data.string import RequestDataString
+from xml.etree.ElementTree import Element
+from protocol.caldav.definitions import caldavxml
+from xml.etree.ElementTree import SubElement
+from protocol.webdav.definitions import davxml
+from protocol.utils.xmlhelpers import BetterElementTree
+
+class MakeCalendar(RequestResponse):
+
+    def __init__(self, session, url, displayname=None, description=None, timezone=None):
+        super(MakeCalendar, self).__init__(session, methods.MKCALENDAR, url)
+        self.displayname = displayname
+        self.description = description
+        self.timezone = timezone
+        
+        self.initRequestData()
+
+    def initRequestData(self):
+        if self.displayname or self.description or self.timezone:
+            # Write XML info to a string
+            os = StringIO()
+            self.generateXML(os)
+            self.request_data = RequestDataString(os.getvalue(), "text/xml charset=utf-8")
+    
+    def generateXML(self, os):
+        # Structure of document is:
+        #
+        # <CALDAV:mkcalendar>
+        #   <DAV:prop>
+        #     <<each property as elements>>
+        #   </DAV:prop>
+        # </CALDAV:mkcalendar>
+
+        # <CALDAV:mkcalendar> element
+        mkcalendar = Element(caldavxml.mkcalendar)
+
+        # <DAV:prop> element
+        prop = SubElement(mkcalendar, davxml.prop)
+        
+        # <DAV:displayname> element
+        if self.displayname:
+            displayname = SubElement(prop, davxml.displayname)
+            displayname.text = self.displayname
+        
+        # <CalDAV:calendar-description> element
+        if self.description:
+            description = SubElement(prop, caldavxml.calendar_description)
+            description.text = self.description
+        
+        # <CalDAV:timezone> element
+        if self.timezone:
+            timezone = SubElement(prop, caldavxml.calendar_timezone)
+            timezone.text = self.timezone
+        
+        # Now we have the complete document, so write it out (no indentation)
+        xmldoc = BetterElementTree(mkcalendar)
+        xmldoc.writeUTF8(os)

Added: CalDAVClientLibrary/trunk/src/protocol/caldav/multiget.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/caldav/multiget.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/caldav/multiget.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,71 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.webdav.report import Report
+from StringIO import StringIO
+from protocol.http.data.string import RequestDataString
+from protocol.caldav.definitions import caldavxml
+from xml.etree.ElementTree import Element
+from xml.etree.ElementTree import SubElement
+from protocol.webdav.definitions import davxml
+from protocol.utils.xmlhelpers import BetterElementTree
+
+class Multiget(Report):
+
+    def __init__(self, session, url, hrefs, props=()):
+        super(Multiget, self).__init__(session, url)
+        self.props = props
+        self.hrefs = hrefs
+        pass
+
+    def initRequestData(self):
+        if self.displayname or self.description or self.timezone:
+            # Write XML info to a string
+            os = StringIO()
+            self.generateXML(os)
+            self.request_data = RequestDataString(os.getvalue(), "text/xml charset=utf-8")
+    
+    def generateXML(self, os):
+        # Structure of document is:
+        #
+        # <CalDAV:calendar-multiget>
+        #   <DAV:prop>
+        #     <<names of each property as elements>>
+        #   </DAV:prop>
+        #   <DAV:href>...</DAV:href>
+        #   ...
+        # </CalDAV:calendar-multiget>
+        
+        # <CalDAV:calendar-multiget> element
+        multiget = Element(caldavxml.calendar_multiget)
+        
+        if self.props:
+            # <DAV:prop> element
+            prop = SubElement(multiget, davxml.prop)
+            
+            # Now add each property
+            for propname in self.props:
+                # Add property element taking namespace into account
+                SubElement(prop, propname)
+        
+        # Now add each href
+        for href in self.hrefs:
+            # Add href elements
+            SubElement(multiget, davxml.href).text = href
+        
+        # Now we have the complete document, so write it out (no indentation)
+        xmldoc = BetterElementTree(multiget)
+        xmldoc.writeUTF8(os)

Added: CalDAVClientLibrary/trunk/src/protocol/caldav/tests/__init__.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/caldav/tests/__init__.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/caldav/tests/__init__.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,16 @@
+##
+# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+

Added: CalDAVClientLibrary/trunk/src/protocol/caldav/tests/test_makecalendar.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/caldav/tests/test_makecalendar.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/caldav/tests/test_makecalendar.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,111 @@
+##
+# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.webdav.session import Session
+from protocol.caldav.makecalendar import MakeCalendar
+from StringIO import StringIO
+import unittest
+
+class TestRequest(unittest.TestCase):
+    
+    def test_Method(self):
+        
+        server = Session("www.example.com")
+        request = MakeCalendar(server, "/")
+        self.assertEqual(request.getMethod(), "MKCALENDAR")
+    
+class TestRequestHeaders(unittest.TestCase):
+    pass
+
+class TestRequestBody(unittest.TestCase):
+ 
+    def test_GenerateXMLDisplayname(self):
+        
+        server = Session("www.example.com")
+        request = MakeCalendar(server, "/", "home")
+        os = StringIO()
+        request.generateXML(os)
+        self.assertEqual(os.getvalue(), """<?xml version='1.0' encoding='utf-8'?>
+<ns0:mkcalendar xmlns:ns0="urn:ietf:params:xml:ns:caldav">
+  <ns1:prop xmlns:ns1="DAV:">
+    <ns1:displayname>home</ns1:displayname>
+  </ns1:prop>
+</ns0:mkcalendar>
+""".replace("\n", "\r\n")
+)
+
+    def test_GenerateXMLMultipleProperties(self):
+        
+        server = Session("www.example.com")
+        request = MakeCalendar(server, "/", "home", "my personal calendar")
+        os = StringIO()
+        request.generateXML(os)
+        self.assertEqual(os.getvalue(), """<?xml version='1.0' encoding='utf-8'?>
+<ns0:mkcalendar xmlns:ns0="urn:ietf:params:xml:ns:caldav">
+  <ns1:prop xmlns:ns1="DAV:">
+    <ns1:displayname>home</ns1:displayname>
+    <ns0:calendar-description>my personal calendar</ns0:calendar-description>
+  </ns1:prop>
+</ns0:mkcalendar>
+""".replace("\n", "\r\n")
+)
+
+    def test_GenerateXMLCDATAProperty(self):
+        
+        server = Session("www.example.com")
+        timezone = """BEGIN:VCALENDAR
+PRODID:-//Example Corp.//CalDAV Client//EN
+VERSION:2.0
+BEGIN:VTIMEZONE
+TZID:US-Eastern
+LAST-MODIFIED:19870101T000000Z
+BEGIN:STANDARD
+DTSTART:19671029T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+TZNAME:Eastern Standard Time (US & Canada)
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19870405T020000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+TZNAME:Eastern Daylight Time (US & Canada)
+END:DAYLIGHT
+END:VTIMEZONE
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        request = MakeCalendar(server, "/", timezone=timezone)
+        os = StringIO()
+        request.generateXML(os)
+        self.assertEqual(os.getvalue(), """<?xml version='1.0' encoding='utf-8'?>
+<ns0:mkcalendar xmlns:ns0="urn:ietf:params:xml:ns:caldav">
+  <ns1:prop xmlns:ns1="DAV:">
+    <ns0:calendar-timezone>%s</ns0:calendar-timezone>
+  </ns1:prop>
+</ns0:mkcalendar>
+""".replace("\n", "\r\n") % (timezone.replace("&", "&amp;"),)
+)
+
+class TestResponse(unittest.TestCase):
+    pass
+
+class TestResponseHeaders(unittest.TestCase):
+    pass
+
+class TestResponseBody(unittest.TestCase):
+    pass

Added: CalDAVClientLibrary/trunk/src/protocol/caldav/tests/test_multiget.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/caldav/tests/test_multiget.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/caldav/tests/test_multiget.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,105 @@
+##
+# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.webdav.session import Session
+from protocol.caldav.multiget import Multiget
+from StringIO import StringIO
+from protocol.webdav.definitions import davxml
+import unittest
+
+class TestRequest(unittest.TestCase):
+    
+    def test_Method(self):
+        
+        server = Session("www.example.com")
+        request = Multiget(server, "/", ())
+        self.assertEqual(request.getMethod(), "REPORT")
+    
+class TestRequestHeaders(unittest.TestCase):
+    pass
+
+class TestRequestBody(unittest.TestCase):
+ 
+    def test_GenerateXMLOneHrefOnly(self):
+        
+        server = Session("www.example.com")
+        request = Multiget(server, "/", ("/a",))
+        os = StringIO()
+        request.generateXML(os)
+        self.assertEqual(os.getvalue(), """<?xml version='1.0' encoding='utf-8'?>
+<ns0:calendar-multiget xmlns:ns0="urn:ietf:params:xml:ns:caldav">
+  <ns1:href xmlns:ns1="DAV:">/a</ns1:href>
+</ns0:calendar-multiget>
+""".replace("\n", "\r\n")
+)
+
+    def test_GenerateXMLMultipleHrefsOnly(self):
+        
+        server = Session("www.example.com")
+        request = Multiget(server, "/", ("/a", "/b",))
+        os = StringIO()
+        request.generateXML(os)
+        self.assertEqual(os.getvalue(), """<?xml version='1.0' encoding='utf-8'?>
+<ns0:calendar-multiget xmlns:ns0="urn:ietf:params:xml:ns:caldav">
+  <ns1:href xmlns:ns1="DAV:">/a</ns1:href>
+  <ns1:href xmlns:ns1="DAV:">/b</ns1:href>
+</ns0:calendar-multiget>
+""".replace("\n", "\r\n")
+)
+
+    def test_GenerateXMLMultipleHrefsOneProperty(self):
+        
+        server = Session("www.example.com")
+        request = Multiget(server, "/", ("/a", "/b",), (davxml.getetag,))
+        os = StringIO()
+        request.generateXML(os)
+        self.assertEqual(os.getvalue(), """<?xml version='1.0' encoding='utf-8'?>
+<ns0:calendar-multiget xmlns:ns0="urn:ietf:params:xml:ns:caldav">
+  <ns1:prop xmlns:ns1="DAV:">
+    <ns1:getetag />
+  </ns1:prop>
+  <ns1:href xmlns:ns1="DAV:">/a</ns1:href>
+  <ns1:href xmlns:ns1="DAV:">/b</ns1:href>
+</ns0:calendar-multiget>
+""".replace("\n", "\r\n")
+)
+
+    def test_GenerateXMLMultipleHrefsMultipleProperties(self):
+        
+        server = Session("www.example.com")
+        request = Multiget(server, "/", ("/a", "/b",), (davxml.getetag, davxml.displayname,))
+        os = StringIO()
+        request.generateXML(os)
+        self.assertEqual(os.getvalue(), """<?xml version='1.0' encoding='utf-8'?>
+<ns0:calendar-multiget xmlns:ns0="urn:ietf:params:xml:ns:caldav">
+  <ns1:prop xmlns:ns1="DAV:">
+    <ns1:getetag />
+    <ns1:displayname />
+  </ns1:prop>
+  <ns1:href xmlns:ns1="DAV:">/a</ns1:href>
+  <ns1:href xmlns:ns1="DAV:">/b</ns1:href>
+</ns0:calendar-multiget>
+""".replace("\n", "\r\n")
+)
+
+class TestResponse(unittest.TestCase):
+    pass
+
+class TestResponseHeaders(unittest.TestCase):
+    pass
+
+class TestResponseBody(unittest.TestCase):
+    pass

Added: CalDAVClientLibrary/trunk/src/protocol/http/__init__.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/http/__init__.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/http/__init__.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,16 @@
+##
+# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+

Added: CalDAVClientLibrary/trunk/src/protocol/http/authentication/__init__.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/http/authentication/__init__.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/http/authentication/__init__.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,16 @@
+##
+# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+

Added: CalDAVClientLibrary/trunk/src/protocol/http/authentication/authenticator.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/http/authentication/authenticator.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/http/authentication/authenticator.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,20 @@
+##
+# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+class Authenticator(object):
+
+    def addHeaders(self, hdrs, request):
+        raise NotImplementedError

Added: CalDAVClientLibrary/trunk/src/protocol/http/authentication/basic.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/http/authentication/basic.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/http/authentication/basic.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,36 @@
+##
+# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.http.authentication.authenticator import Authenticator
+from protocol.http.definitions import headers
+
+class Basic(Authenticator):
+
+    def __init__(self, user, pswd):
+        self.user = user
+        self.pswd = pswd
+
+    def setDetails(self, user, pswd):
+        self.user = user
+        self.pswd = pswd
+
+    def addHeaders(self, hdrs, request):
+        # Generate the base64 encoded string
+        encode = self.user + ":" + self.pswd
+        base64 = encode.encode("base64").strip()
+    
+        # Generate header
+        hdrs.append((headers.Authorization, "Basic %s" % (base64,)))

Added: CalDAVClientLibrary/trunk/src/protocol/http/authentication/digest.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/http/authentication/digest.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/http/authentication/digest.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,218 @@
+##
+# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.http.authentication.authenticator import Authenticator
+from protocol.http.util import parsetoken
+from protocol.http.definitions import headers
+from StringIO import StringIO
+import sha
+import md5
+
+class Digest(Authenticator):
+
+    def __init__(self, user, pswd, www_authenticate):
+        self.fields = {}
+        self.fields['username'] = user
+        self.fields['password'] = pswd
+        self.parseAuthenticateHeader(www_authenticate)
+        self.stale = False
+        self.clientCount = 0
+
+    def setDetails(self, user, pswd, www_authenticate):
+        self.fields['username'] = user
+        self.fields['password'] = pswd
+        self.parseAuthenticateHeader(www_authenticate)
+
+    def addHeaders(self, hdrs, request):
+        # Generate response
+        self.generateResponse(request)
+        
+        # Generate header
+        os = StringIO()
+        os.write("Digest username=\"%s\"," % (self.fields['username'],))
+        os.write(" realm=\"%s\"," % (self.fields['realm'],))
+        os.write(" nonce=\"%s\"," % (self.fields['nonce'],))
+        os.write(" uri=\"%s\"," % (request.getURL(),))
+        if "qop" in self.fields:
+            os.write(" qop=auth,")
+            os.write(" nc=\"%s\"" % (self.fields['nc'],))
+            os.write(" cnonce=\"%s\"" % (self.fields['cnonce'],))
+        os.write(" response=\"%s\"" % (self.response,))
+        
+        if "algorithm" in self.fields:
+            os.write(", algorithm=\"%s\"" % (self.fields['algorithm'],))
+        if "opaque" in self.fields:
+            os.write(", opaque=\"%s\"" % (self.fields['opaque'],))
+        
+        hdrs.append((headers.Authorization, os.getvalue()))
+
+    def parseAuthenticateHeader(self, hdrs):
+        for hdr in hdrs:
+
+            # Strip any space
+            hdr = hdr.strip()
+            
+            # Must have Digest token
+            if hdr[:7].lower() != "digest ":
+                continue
+            else:
+                hdr = hdr[7:]
+            
+            # Get each name/value pair
+            while True:
+                name, hdr = parsetoken(hdr, " \t=")
+
+                if not name or not hdr:
+                    return
+                name = name.lower()
+                
+                value, hdr = parsetoken(hdr, ", ")
+                if not value:
+                    return
+                
+                if name in ("realm", "domain", "nonce", "opaque", "algorithm", "qop"):
+                    self.fields[name] = value
+
+                elif name == "stale":
+                    self.stale = (value.lower() != "false")
+
+                else:
+                    # Unknown token - ignore
+                    pass
+                
+                # Punt over space
+                hdr = hdr.strip()
+            
+            break
+
+    algorithms = {
+        'md5': md5.new,
+        'md5-sess': md5.new,
+        'sha': sha.new,
+    }
+    
+    # DigestCalcHA1
+    @staticmethod
+    def calcHA1(
+        pszAlg,
+        pszUserName,
+        pszRealm,
+        pszPassword,
+        pszNonce,
+        pszCNonce,
+        preHA1=None
+    ):
+        """
+        @param pszAlg: The name of the algorithm to use to calculate the Digest.
+            Currently supported are md5 md5-sess and sha.
+    
+        @param pszUserName: The username
+        @param pszRealm: The realm
+        @param pszPassword: The password
+        @param pszNonce: The nonce
+        @param pszCNonce: The cnonce
+    
+        @param preHA1: If available this is a str containing a previously
+            calculated HA1 as a hex string. If this is given then the values for
+            pszUserName, pszRealm, and pszPassword are ignored.
+        """
+    
+        if (preHA1 and (pszUserName or pszRealm or pszPassword)):
+            raise TypeError(("preHA1 is incompatible with the pszUserName, "
+                             "pszRealm, and pszPassword arguments"))
+    
+        if preHA1 is None:
+            # We need to calculate the HA1 from the username:realm:password
+            m = Digest.algorithms[pszAlg]()
+            m.update(pszUserName)
+            m.update(":")
+            m.update(pszRealm)
+            m.update(":")
+            m.update(pszPassword)
+            HA1 = m.digest()
+        else:
+            # We were given a username:realm:password
+            HA1 = preHA1.decode('hex')
+    
+        if pszAlg == "md5-sess":
+            m = Digest.algorithms[pszAlg]()
+            m.update(HA1)
+            m.update(":")
+            m.update(pszNonce)
+            m.update(":")
+            m.update(pszCNonce)
+            HA1 = m.digest()
+    
+        return HA1.encode('hex')
+    
+    # DigestCalcResponse
+    @staticmethod
+    def calcResponse(
+        HA1,
+        algo,
+        pszNonce,
+        pszNonceCount,
+        pszCNonce,
+        pszQop,
+        pszMethod,
+        pszDigestUri,
+        pszHEntity,
+    ):
+        m = Digest.algorithms[algo]()
+        m.update(pszMethod)
+        m.update(":")
+        m.update(pszDigestUri)
+        if pszQop == "auth-int":
+            m.update(":")
+            m.update(pszHEntity)
+        HA2 = m.digest().encode('hex')
+    
+        m = Digest.algorithms[algo]()
+        m.update(HA1)
+        m.update(":")
+        m.update(pszNonce)
+        m.update(":")
+        if pszNonceCount and pszCNonce: # pszQop:
+            m.update(pszNonceCount)
+            m.update(":")
+            m.update(pszCNonce)
+            m.update(":")
+            m.update(pszQop)
+            m.update(":")
+        m.update(HA2)
+        respHash = m.digest().encode('hex')
+        return respHash
+
+    def generateResponse(self, request):
+        self.response = Digest.calcResponse(
+            Digest.calcHA1(
+                self.fields.get("algorithm", "md5"),
+                self.fields.get("username", ""),
+                self.fields.get("realm", ""),
+                self.fields.get("password", ""),
+                self.fields.get("nonce", ""),
+                self.fields.get("cnonce", ""),
+            ),
+            self.fields.get("algorithm", "md5"),
+            self.fields.get("nonce", ""),
+            self.fields.get("nc", ""),
+            self.fields.get("cnonce", ""),
+            self.fields.get("qop", ""),
+            request.method,
+            request.url,
+            None,
+        )
+

Added: CalDAVClientLibrary/trunk/src/protocol/http/authentication/tests/__init__.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/http/authentication/tests/__init__.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/http/authentication/tests/__init__.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,16 @@
+##
+# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+

Added: CalDAVClientLibrary/trunk/src/protocol/http/authentication/tests/test_basic.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/http/authentication/tests/test_basic.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/http/authentication/tests/test_basic.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,29 @@
+##
+# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.http.authentication.basic import Basic
+from protocol.http.definitions import headers
+
+import unittest
+
+class TestBasic(unittest.TestCase):
+    
+    def testBasic(self):
+        
+        auther = Basic("user", "pswd")
+        hdrs = []
+        auther.addHeaders(hdrs, None)
+        self.assertTrue((headers.Authorization, "Basic dXNlcjpwc3dk") in hdrs)

Added: CalDAVClientLibrary/trunk/src/protocol/http/data/__init__.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/http/data/__init__.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/http/data/__init__.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,16 @@
+##
+# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+

Added: CalDAVClientLibrary/trunk/src/protocol/http/data/data.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/http/data/data.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/http/data/data.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,46 @@
+##
+# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+
+class Data(object):
+    
+    def __init__(self):
+        pass
+        
+    def start(self):
+        pass
+    
+    def stop(self):
+        pass
+
+class RequestData(Data):
+
+    def getContentLength(self):
+        return self.content_length
+
+    def getContentType(self):
+        return self.content_type
+    
+    def read(self):
+        raise NotImplementedError
+
+class ResponseData(Data):
+    
+    def write(self, data):
+        raise NotImplementedError
+
+    def clear(self):
+        raise NotImplementedError

Added: CalDAVClientLibrary/trunk/src/protocol/http/data/file.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/http/data/file.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/http/data/file.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,68 @@
+##
+# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.http.data.data import ResponseData
+from protocol.http.data.data import RequestData
+import stat
+import os
+
+class RequestDataFile(RequestData):
+    
+    def __init__(self, fname, content_type):
+
+        # Cache file name
+        self.fname = fname
+    
+        # Determine size of stream
+        self.content_length = os.stat(self.fname)[stat.ST_SIZE]
+
+        self.content_type = content_type
+
+    def start(self):
+        # Create an input file stream
+        self.stream = open(self.fname, "r")
+
+    def stop(self):
+        self.stream.close()
+        self.stream = None
+
+    def read(self):
+        data = self.stream.read(8192)
+        if data:
+            return data, True
+        else:
+            return data, False
+
+class ResponseDataFile(ResponseData):
+
+    def __init__(self, fname):
+        self.fname = fname
+
+    def start(self):
+        # Create an input file stream
+        self.stream = open(self.fname, "w")
+
+    def stop(self):
+        self.stream.close()
+        self.stream = None
+
+    def write(self, data):
+        self.stream.write(data)
+
+    def clear(self):
+        # Throw out existing data and start from scratch
+        self.stop()
+        self.start()

Added: CalDAVClientLibrary/trunk/src/protocol/http/data/string.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/http/data/string.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/http/data/string.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,49 @@
+##
+# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.http.data.data import ResponseData
+from protocol.http.data.data import RequestData
+from StringIO import StringIO
+
+class RequestDataString(RequestData):
+    
+    def __init__(self, text, content_type):
+
+        # Cache file name
+        self.text = text
+    
+        # Determine size of stream
+        self.content_length = len(text)
+
+        self.content_type = content_type
+
+    def read(self):
+        return self.text, False
+        
+class ResponseDataString(ResponseData):
+
+    def __init__(self):
+        self.stream = StringIO()
+
+    def getData(self):
+        return self.stream.getvalue()
+
+    def write(self, data):
+        self.stream.write(data)
+
+    def clear(self):
+        # Throw out existing data and start from scratch
+        self.stream = StringIO()

Added: CalDAVClientLibrary/trunk/src/protocol/http/definitions/__init__.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/http/definitions/__init__.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/http/definitions/__init__.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,16 @@
+##
+# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+

Added: CalDAVClientLibrary/trunk/src/protocol/http/definitions/headers.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/http/definitions/headers.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/http/definitions/headers.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,45 @@
+##
+# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+# HTTP protocol headers
+
+# RFC2616 4.5 - General Header fields (only the ones we need)
+
+Connection              = "Connection"
+ConnectionClose         = "close"
+Date                    = "Date"
+TransferEncoding        = "Transfer-Encoding"
+TransferEncodingChunked = "chunked"
+
+# RFC2616 5.3 - Request Header fields (only the ones we need)
+
+Authorization = "Authorization"
+Host          = "Host"
+IfMatch       = "If-Match"
+IfNoneMatch   = "If-None-Match"
+
+# RFC2616 6.2 - Response Header fields (only the ones we need)
+
+ETag            = "ETag"
+Location        = "Location"
+Server          = "Server"
+WWWAuthenticate = "WWW-Authenticate"
+
+# RFC2616 7.1 - Entity Header fields (only the ones we need)
+
+Allow         = "Allow"
+ContentLength = "Content-Length"
+ContentType   = "Content-Type"

Added: CalDAVClientLibrary/trunk/src/protocol/http/definitions/methods.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/http/definitions/methods.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/http/definitions/methods.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,27 @@
+##
+# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+
+# RFC2616 5.1.1 - Request Methods
+
+OPTIONS = "OPTIONS"
+GET     = "GET"
+HEAD    = "HEAD"
+POST    = "POST"
+PUT     = "PUT"
+DELETE  = "DELETE"
+TRACE   = "TRACE"
+CONNECT = "CONNECT"

Added: CalDAVClientLibrary/trunk/src/protocol/http/definitions/statuscodes.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/http/definitions/statuscodes.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/http/definitions/statuscodes.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,58 @@
+##
+# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+
+Unknown = 0
+Continue = 100
+SwitchingProtocols = 101
+OK = 200
+Created = 201
+Accepted = 202
+NonAuthoritativeInformation = 203
+NoContent = 204
+ResetContent = 205
+PartialContent = 206
+MultipleChoices = 300
+MovedPermanently = 301
+Found = 302
+SeeOther = 303
+NotModified = 304
+UseProxy = 305
+TemporaryRedirect = 307
+BadRequest = 400
+Unauthorized = 401
+PaymentRequired = 402
+Forbidden = 403
+NotFound = 404
+MethodNotAllowed = 405
+NotAcceptable = 406
+ProxyAuthenticationRequired = 407
+RequestTimeout = 408
+Conflict = 409
+Gone = 410
+LengthRequired = 411
+PreconditionFailed = 412
+RequestEntityTooLarge = 413
+RequestURITooLarge = 414
+UnsupportedMediaType = 415
+RequestedRangeNotSatisfiable = 416
+ExpectationFailed = 417
+InternalServerError = 500
+NotImplemented = 501
+BadGateway = 502
+ServiceUnavailable = 503
+GatewayTimeout = 504
+HTTPVersionNotSupported = 505

Added: CalDAVClientLibrary/trunk/src/protocol/http/requestresponse.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/http/requestresponse.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/http/requestresponse.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,274 @@
+##
+# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from StringIO import StringIO
+from protocol.http.definitions import headers
+from protocol.http.definitions import methods
+from protocol.http.definitions import statuscodes
+
+class ResponseError(Exception):
+    pass
+
+class RequestResponse(object):
+    
+    def __init__(self, session, method, url, etag=None, etag_match=False):
+        self._initResponse()
+        self.session = session
+        self.method = method
+        self.url = url
+        self.etag = etag
+        self.etag_match = etag_match
+
+    def _initResponse(self):
+        self.session = None
+        self.request_data = None
+        self.response_data = None
+        self.method = methods.GET
+        self.url = None
+        self.etag = None
+        self.etag_match = False
+        self.status_code = statuscodes.Unknown
+        self.status_reason = None
+        self.headers = {}
+        self.connection_close = False
+        self.content_length = 0
+        self.chunked = False
+        self.completed = False
+
+    def setSession(self, session):
+        self.session = session
+    def getSession(self):
+        return self.session
+
+    def getMethod(self):
+        return self.method
+
+    def setURL(self, ruri):
+        self.url = ruri
+    def getURL(self):
+        return self.url
+
+    def setETag(self, etag, etag_match):
+        self.etag = etag
+        self.etag_match = etag_match
+
+    def queuedForSending(self, session):
+        self.session = session
+    
+    def setData(self, request_data, response_data):
+        self.request_data = request_data
+        self.response_data = response_data
+
+    def hasRequestData(self):
+        return self.request_data != None
+
+    def hasResponseData(self):
+        return self.response_data != None
+
+    def getRequestData(self):
+        if self.request_data:
+            return self.request_data
+        else:
+            return None
+
+    def getResponseData(self):
+        if self.response_data:
+            return self.response_data
+        else:
+            return None
+    
+    def getRequestStartLine(self):
+        return "%s %s HTTP/1.1" % (self.method, self.url,)
+
+    def getRequestHeaders(self):
+        # This will be overridden by sub-classes that add headers - those classes should
+        # call this class's implementation to write out the basic set of headers
+        result = []
+        self.addHeaders(result)
+        return tuple(result)
+
+    def generateRequestHeader(self):
+        os = StringIO()
+        os.write("%s\r\n" % (self.getRequestStartLine(),))
+        for header, value in self.getRequestHeaders():
+            os.write("%s: %s\r\n" % (header, value,))
+        os.write("\r\n")
+        return os.getvalue()
+
+    def addHeaders(self, hdrs):
+
+        # Write host
+        hdrs.append((headers.Host, self.session.server))
+        
+        # Do ETag matching
+        if self.etag:
+            if self.etag_match:
+                hdrs.append((headers.IfMatch, self.etag))
+            else:
+                hdrs.append((headers.IfNoneMatch, self.etag))
+    
+        # Do session global headers
+        self.session.addHeaders(hdrs, self)
+        
+        # Check for content
+        self.addContentHeaders(hdrs)
+
+    def addContentHeaders(self, hdrs):
+        # Check for content
+        if self.hasRequestData():
+            hdrs.append((headers.ContentLength, self.request_data.getContentLength()))
+            hdrs.append((headers.ContentType, self.request_data.getContentType()))
+
+    def setResponseStatus(self, version, status, reason):
+        self.status_code = status
+        self.status_reason = reason
+
+    def setResponseHeaders(self, hdrs):
+        for header in hdrs:
+            splits = header.split(":")
+            self.headers.setdefault(splits[0].strip().lower(), []).append(splits[1].strip())
+
+        # Now cache some useful header values
+        self.cacheHeaders()
+
+    def clearResponse(self):
+        self.etag_match = False
+        self.status_code = statuscodes.Unknown
+        self.status_reason = None
+        self.headers = {}
+        self.connection_close = False
+        self.content_length = 0
+        self.chunked = False
+        self.completed = False
+    
+        if self.response_data:
+            self.response_data.clear()
+
+    def getStatusCode(self):
+        return self.status_code
+    def getStatusReason(self):
+        return self.status_reason
+
+    def getConnectionClose(self):
+        return self.connection_close
+
+    def getContentLength(self):
+        return self.content_length
+    def getChunked(self):
+        return self.chunked
+
+    def setComplete(self):
+        self.completed = True
+    def getCompleted(self):
+        return self.completed
+
+    def hasResponseHeader(self, hdr):
+        return self.headers.has_key(hdr.lower())
+    
+    def getResponseHeader(self, hdr):
+        if self.headers.has_key(hdr.lower()):
+            return self.headers[hdr.lower()][0]
+        else:
+            return ""
+    
+    def getResponseHeaders(self, hdr=None):
+        if hdr:
+            if self.headers.has_key(hdr.lower()):
+                return self.headers[hdr.lower()]
+            else:
+                return ()
+        else:
+            return self.headers
+    
+    def isRedirect(self):
+        # Only these are allowed
+        return self.status_code in (statuscodes.MovedPermanently, statuscodes.Found, statuscodes.TemporaryRedirect)
+
+    def parseStatusLine(self, line):
+        
+        # Must have 'HTTP/' version at start
+        if line[0:5] != "HTTP/":
+            raise ResponseError("status line incorrect at start")
+        
+        # Must have version '1.1 '
+        if line[5:9] != "1.1 ":
+            raise ResponseError("incorrect http version in status line")
+        
+        # Must have three digits followed by nothing or one space
+        if not line[9:12].isdigit() or (len(line) > 12 and line[12] != " "):
+            raise ResponseError("invalid status response code syntax")
+        
+        # Read in the status code
+        self.status_code = int(line[9:12])
+        
+        # Remainder is reason
+        if len(line) > 13:
+            self.status_reason = line[13:]
+
+    def readFoldedLine(self, instream, line1, line2, log):
+        # If line2 already has data, transfer that into line1
+        if line2 or line1:
+            line1 = line2
+        else:
+            # Fill first line
+            line1 = instream.readline()
+            if not line1:
+                return False, line1, line2
+            line1 = line1.rstrip("\r\n")
+    
+            if log:
+                log.write("%s\n" % (line1,))
+    
+        # Terminate on blank line which is end of headers
+        if not line1:
+            return True, line1, line2
+    
+        # Now loop looking ahead at the next line to see if it is folded
+        while True:
+            # Get next line
+            line2 = instream.readline()
+            if not line2:
+                return True, line1, line2
+            line2 = line2.rstrip("\r\n")
+    
+            if log:
+                log.write("%s\n" % (line2,))
+            
+            # Does it start with a space => folded
+            if line2 and line2[0].isspace():
+                # Copy folded line (without space) to current line and cycle for more
+                line1 += line2[1:]
+            else:
+                # Not folded - just exit loop
+                break
+    
+        return True, line1, line2
+
+    def cacheHeaders(self):
+        # Connection
+        if self.headers.has_key(headers.Connection):
+            value = self.headers[headers.Connection][0]
+            self.connection_close = (value.lower() == headers.ConnectionClose)
+
+        # Content-Length
+        if self.headers.has_key(headers.ContentLength):
+            value = self.headers[headers.ContentLength][0]
+            self.content_length = int(value)
+        
+        # Transfer encoding
+        if self.headers.has_key(headers.TransferEncoding):
+            value = self.headers[headers.TransferEncoding][0]
+            self.chunked = (value == headers.TransferEncodingChunked)

Added: CalDAVClientLibrary/trunk/src/protocol/http/session.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/http/session.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/http/session.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,122 @@
+##
+# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from httplib import InvalidURL
+import httplib
+
+class Session(object):
+    
+    STATE_OPEN   = 0
+    STATE_CLOSED = 1
+    
+    def __init__(self, server, port=None, ssl=False, log=None):
+        
+        self.server = server
+        self.port = port
+        self.ssl = ssl
+        if not self.port:
+            self.port = httplib.HTTPS_PORT if ssl else httplib.HTTP_PORT
+        self.authorization = None
+        self.connection_state = Session.STATE_CLOSED
+        self.log = log
+        
+    def hasAuthorization(self):
+        return self.authorization != None
+    
+    def getAuthorization(self):
+        return self.authorization
+    
+    def addHeaders(self, hdrs, request):
+        if self.hasAuthorization():
+            self.getAuthorization().addHeaders(hdrs, request)
+    
+    def setServer(self, server, port=None):
+        
+        if port is None:
+            i = server.rfind(':')
+            j = server.rfind(']')
+            if i > j:
+                try:
+                    port = int(server[i+1:])
+                except ValueError:
+                    raise InvalidURL("nonnumeric port: '%s'" % server[i+1:])
+                server = server[:i]
+            else:
+                port = httplib.HTTPS_PORT if self.ssl else httplib.HTTP_PORT
+            if server and server[0] == '[' and server[-1] == ']':
+                server = server[1:-1]
+
+        if self.server != server:
+            self.server = server
+            self.port = port
+            
+            # Always clear out authorization when host changes
+            self.authorization = None
+    
+    def sendRequest(self, request):
+        try:
+
+            # First need a connection
+            self.needConnection();
+            
+            # Now do the connection
+            self.doRequest(request);
+            
+            # Check the final connection state and close if that's what the server wants
+            if request.getConnectionClose():
+                self.closeConnection()
+
+        except Exception:
+
+            # log.err(e)
+            self.connection_state = Session.STATE_CLOSED
+            raise
+    
+    def handleHTTPError(self, request):
+        raise NotImplementedError
+    
+    def displayHTTPError(self, request):
+        raise NotImplementedError
+    
+    def isConnectionOpen(self):
+        return self.connection_state == Session.STATE_OPEN
+
+    def needConnection(self):
+        if not self.isConnectionOpen():
+            self.openConnection()
+
+    def openConnection(self):
+        if not self.isConnectionOpen():
+            self.openSession()
+            self.connection_state = Session.STATE_OPEN
+
+    def closeConnection(self):
+        if self.isConnectionOpen():
+            self.closeSession();
+            self.connection_state = Session.STATE_CLOSED
+
+    def openSession(self):
+        raise NotImplementedError
+    
+    def closeSession(self):
+        raise NotImplementedError
+    
+    def runSession(self, request):
+        raise NotImplementedError
+        
+    def doRequest(self, request):
+        raise NotImplementedError
+    
\ No newline at end of file

Added: CalDAVClientLibrary/trunk/src/protocol/http/tests/__init__.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/http/tests/__init__.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/http/tests/__init__.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,16 @@
+##
+# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+

Added: CalDAVClientLibrary/trunk/src/protocol/http/tests/test_requestresponse.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/http/tests/test_requestresponse.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/http/tests/test_requestresponse.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,89 @@
+##
+# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.http.requestresponse import RequestResponse
+from protocol.http.session import Session
+from protocol.http.data.string import RequestDataString
+from protocol.http.authentication.basic import Basic
+from protocol.http.definitions import methods
+
+import unittest
+
+class TestRequestHeaders(unittest.TestCase):
+    
+    def test_NoEtag(self):
+        
+        server = Session("www.example.com")
+        request = RequestResponse(server, methods.GET, "/")
+        self.assertEqual(request.generateRequestHeader(), """GET / HTTP/1.1
+Host: www.example.com
+
+""".replace("\n", "\r\n")
+)
+ 
+    def test_EtagMatch(self):
+        
+        server = Session("www.example.com")
+        request = RequestResponse(server, methods.GET, "/", "\"etag\"", True)
+        self.assertEqual(request.generateRequestHeader(), """GET / HTTP/1.1
+Host: www.example.com
+If-Match: "etag"
+
+""".replace("\n", "\r\n")
+)
+ 
+    def test_EtagNoneMatch(self):
+        
+        server = Session("www.example.com")
+        request = RequestResponse(server, methods.GET, "/", "\"etag\"", False)
+        self.assertEqual(request.generateRequestHeader(), """GET / HTTP/1.1
+Host: www.example.com
+If-None-Match: "etag"
+
+""".replace("\n", "\r\n")
+)
+ 
+    def test_Content(self):
+        
+        server = Session("www.example.com")
+        request = RequestResponse(server, methods.GET, "/")
+        rawdata = "Here is some data\r\non multiple lines."
+        data = RequestDataString(rawdata, "text/plain")
+        request.setData(data, None)
+        self.assertEqual(request.generateRequestHeader(), """GET / HTTP/1.1
+Host: www.example.com
+Content-Length: %d
+Content-Type: text/plain
+
+""".replace("\n", "\r\n") % (len(rawdata),)
+)
+
+    def test_ContentAndAuthorization(self):
+        
+        server = Session("www.example.com")
+        server.authorization = Basic("user", "pswd")
+        request = RequestResponse(server, methods.GET, "/")
+        rawdata = "Here is some data\r\non multiple lines."
+        data = RequestDataString(rawdata, "text/plain")
+        request.setData(data, None)
+        self.assertEqual(request.generateRequestHeader(), """GET / HTTP/1.1
+Host: www.example.com
+Authorization: Basic dXNlcjpwc3dk
+Content-Length: %d
+Content-Type: text/plain
+
+""".replace("\n", "\r\n") % (len(rawdata),)
+)

Added: CalDAVClientLibrary/trunk/src/protocol/http/tests/test_util.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/http/tests/test_util.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/http/tests/test_util.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,107 @@
+##
+# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.http.util import parsequoted
+from protocol.http.util import parsetoken
+from protocol.http.util import parseStatusLine
+
+import unittest
+
+class TestParseQuoted(unittest.TestCase):
+    
+    def testParseQuotedOK(self):
+        
+        data = {
+            "\"\""                                  : ("",                         ""),
+            "\"quoted\""                            : ("quoted",                   ""),
+            "\"quoted words\""                      : ("quoted words",             ""),
+            "\"quoting a \\\"word\\\"\""            : ("quoting a \"word\"",       ""),
+            "\"\" after"                            : ("",                         "after"),
+            "\"quoted\" after"                      : ("quoted",                   "after"),
+            "\"quoted words\" after"                : ("quoted words",             "after"),
+            "\"quoting a \\\"word\\\"\" after"      : ("quoting a \"word\"",       "after"),
+            "\"quoting a \\\"word\\\" after\" after": ("quoting a \"word\" after", "after"),
+            "\"quoted\"after"                       : ("quoted",                   "after"),
+            "\""                                    : ("",                         ""),
+            "\"unterminated"                        : ("unterminated",             ""),
+            "\"unterminated words"                  : ("unterminated words",       ""),
+            "\"unterminated a \\\"word\\\""         : ("unterminated a \"word\"",  ""),
+         }
+        
+        for input, result in data.iteritems():
+            self.assertEqual(parsequoted(input), result)
+        
+    def testParseQuotedBAD(self):
+        
+        data = (
+            "",
+            "unquoted",
+            "unquoted \"quoted\"",
+        )
+        
+        for input in data:
+            self.assertRaises(AssertionError, parsequoted, input)
+
+class TestParseToken(unittest.TestCase):
+    
+    def testParseTokenOK(self):
+        
+        data = {
+            ""                                      : ("",          ""),
+            "unquoted"                              : ("unquoted",  ""),
+            "unquoted words"                        : ("unquoted",  "words"),
+            "unquoted  words"                       : ("unquoted",  "words"),
+            "unquoting a \"word\""                  : ("unquoting", "a \"word\""),
+            "unquoted\twords"                       : ("unquoted",  "words"),
+            "unquoting\ta \"word\""                 : ("unquoting", "a \"word\""),
+            "unquoted: words"                       : ("unquoted",  "words"),
+            "unquoting: a \"word\""                 : ("unquoting", "a \"word\""),
+
+            "\"\""                                  : ("",                         ""),
+            "\"quoted\""                            : ("quoted",                   ""),
+            "\"quoted words\""                      : ("quoted words",             ""),
+            "\"quoting a \\\"word\\\"\""            : ("quoting a \"word\"",       ""),
+            "\"\" after"                            : ("",                         "after"),
+            "\"quoted\" after"                      : ("quoted",                   "after"),
+            "\"quoted words\" after"                : ("quoted words",             "after"),
+            "\"quoting a \\\"word\\\"\" after"      : ("quoting a \"word\"",       "after"),
+            "\"quoting a \\\"word\\\" after\" after": ("quoting a \"word\" after", "after"),
+            "\"quoted\"after"                       : ("quoted",                   "after"),
+            "\""                                    : ("",                         ""),
+            "\"unterminated"                        : ("unterminated",             ""),
+            "\"unterminated words"                  : ("unterminated words",       ""),
+            "\"unterminated a \\\"word\\\""         : ("unterminated a \"word\"",  ""),
+        }
+        
+        for input, result in data.iteritems():
+            self.assertEqual(parsetoken(input, " \t:"), result)
+
+class TestParseStatusLine(unittest.TestCase):
+    
+    def testParseTokenOK(self):
+        self.assertEqual(parseStatusLine("HTTP/1.1 200 OK"), 200)
+    
+    def testParseTokenBadStatus(self):
+        self.assertEqual(parseStatusLine("HTTP/1.2 2001 OK"), 0)
+    
+    def testParseTokenBadVersion(self):
+        self.assertEqual(parseStatusLine("HTTP/1.2 200 OK"), 0)
+    
+    def testParseTokenBadNumber(self):
+        self.assertEqual(parseStatusLine("HTTP/1.1 OK"), 0)
+    
+    def testParseTokenBad(self):
+        self.assertEqual(parseStatusLine("HTTP/1.1"), 0)

Added: CalDAVClientLibrary/trunk/src/protocol/http/util.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/http/util.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/http/util.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,74 @@
+##
+# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+
+def parsetoken(text, delimiters=" \t"):
+    
+    if not text:
+        return "", ""
+
+    if text[0] == '"':
+        return parsequoted(text, delimiters)
+    else:
+        for pos, c in enumerate(text):
+            if c in delimiters:
+                token = text[0:pos]
+                break
+        else:
+            return text, ""
+        
+        return token, lstripdelimiters(text[pos:], delimiters)
+        
+def parsequoted(text, delimiters=" \t"):
+    
+    assert(text)
+    assert(text[0] == '"')
+    
+    pos = 1
+    while True:
+        next_pos = text.find('"', pos)
+        if next_pos == -1:
+            return text[1:].replace("\\\\", "\\").replace("\\\"", "\""), ""
+        if text[next_pos - 1] == '\\':
+            pos = next_pos + 1
+        else:
+            return (
+                text[1:next_pos].replace("\\\\", "\\").replace("\\\"", "\""),
+                lstripdelimiters(text[next_pos+1:], delimiters)
+            )
+
+def lstripdelimiters(text, delimiters):
+    for pos, c in enumerate(text):
+        if c not in delimiters:
+            return text[pos:]
+    else:
+        return ""
+
+def parseStatusLine(status):
+
+    status = status.strip()
+
+    # Must have 'HTTP/1.1' version at start
+    if status[0:9] != "HTTP/1.1 ":
+        return 0
+    
+    # Must have three digits followed by nothing or one space
+    if not status[9:12].isdigit() or (len(status) > 12 and status[12] != " "):
+        return 0
+    
+    # Read in the status code
+    return int(status[9:12])
+

Added: CalDAVClientLibrary/trunk/src/protocol/tests/__init__.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/tests/__init__.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/tests/__init__.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,15 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##

Added: CalDAVClientLibrary/trunk/src/protocol/tests/test_url.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/tests/test_url.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/tests/test_url.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,53 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.url import URL
+
+import unittest
+
+class TestURLParse(unittest.TestCase):
+    
+    def verifyParts(self, u, s, scheme, server, path, extended):
+        self.assertEqual(u.toString(), s)
+        self.assertEqual(u.scheme, scheme)
+        self.assertEqual(u.server, server)
+        self.assertEqual(u.path, path)
+        self.assertEqual(u.extended, extended)
+
+    def test_ParsePlain(self):
+        
+        s = "http://www.example.com"
+        u = URL(url=s)
+        self.verifyParts(u, s, "http", "www.example.com", "", "")
+
+    def test_ParsePlainPath(self):
+        
+        s = "http://www.example.com/principals/users"
+        u = URL(url=s)
+        self.verifyParts(u, s, "http", "www.example.com", "/principals/users", "")
+
+    def test_ParsePlainPathExtended(self):
+        
+        s = "http://www.example.com/principals/users?test=true"
+        u = URL(url=s)
+        self.verifyParts(u, s, "http", "www.example.com", "/principals/users", "?test=true")
+
+    def test_ParseMailto(self):
+        
+        s = "mailto:user at example.com"
+        u = URL(url=s)
+        self.verifyParts(u, s, "mailto", "user at example.com", "", "")
+

Added: CalDAVClientLibrary/trunk/src/protocol/url.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/url.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/url.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,219 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+import os
+import urllib
+
+class URL(object):
+    
+    eAbsolute = 0
+    eRelative = 1
+    eLastPath = 2
+
+    URLEscape = '%'
+    URLReserved = "/?:@&="
+    URLUnreserved = ( # Allowable URL chars
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        # 0 - 15
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        # 16 - 31
+        0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0,        # 32 - 47
+        1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0,        # 48 - 63
+        0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,        # 64 - 79
+        1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1,        # 80 - 95
+        0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,        # 96 - 111
+        1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,        # 112 - 127
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        # 128 - 143
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        # 144 - 159
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        # 160 - 175
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        # 176 - 191
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        # 192 - 207
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        # 208 - 223
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        # 224 - 239
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,         # 240 - 255
+    )
+    
+    URLCharacter = ( # Allowable URL chars -- all
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        # 0 - 15
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        # 16 - 31
+        0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1,        # 32 - 47
+        1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,        # 48 - 63
+        0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,        # 64 - 79
+        1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1,        # 80 - 95
+        0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,        # 96 - 111
+        1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,        # 112 - 127
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        # 128 - 143
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        # 144 - 159
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        # 160 - 175
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        # 176 - 191
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        # 192 - 207
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        # 208 - 223
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        # 224 - 239
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        # 240 - 255
+    )
+    
+    URLXCharacter = ( # Allowable URL chars (all)
+          # RFC2732 uses '[...]' for IPv6 addressing - [] are now allowed
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        # 0 - 15
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        # 16 - 31
+        0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,        # 32 - 47
+        1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1,        # 48 - 63
+        1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,        # 64 - 79
+        1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1,        # 80 - 95
+        0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,        # 96 - 111
+        1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1,        # 112 - 127
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        # 128 - 143
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        # 144 - 159
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        # 160 - 175
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        # 176 - 191
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        # 192 - 207
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        # 208 - 223
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        # 224 - 239
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,        # 240 - 255
+    )
+
+    URLSchemeDoubleSlash = ("http", "https", "webcal",)
+
+    def __init__(self, url=None, scheme=None, server=None, path=None, extended=None, decode=False):
+        
+        self.scheme = ""
+        self.server = ""
+        self.path = ""
+        self.extended = ""
+
+        if not url:
+            self.scheme = scheme
+            self.server = server
+            self.path = path
+            if self.path and decode:
+                self.path = urllib.unquote(self.path)
+            self.extended = extended
+            if self.extended and decode:
+                self.extended = urllib.unquote_plus(self.extended)
+        else:
+            self._parse(url, decode)
+    
+    def __str__(self):
+        return "URL: %s" % (self.toString(),)
+
+    def __repr__(self):
+        return "URL: %s" % (self.toString(),)
+
+    def __cmp__(self, other):
+        return cmp(self.toString(), other.toString())
+
+    def absoluteURL(self):
+        return self.toString()
+    
+    def relativeURL(self):
+        return self.toString(conversion=URL.eRelative)
+
+    def toString(self, conversion=eAbsolute, encode=True):
+
+        result = ""
+    
+        # Add scheme & host if not relative
+        if conversion == URL.eAbsolute and self.scheme and self.server:
+            result += self.scheme + ":"
+            if self.scheme in URL.URLSchemeDoubleSlash:
+                result += "//"
+            result += self.server
+    
+        # Get path (or last part of it if required)
+        if self.path and conversion == URL.eLastPath:
+            path = self.path[self.path.rfind("/"):]
+        else:
+            path = self.path
+    
+        # Now encode if required
+        if path:
+            result += (urllib.quote(path) if encode else path)
+        
+        if self.extended:
+            result += (urllib.quote_plus(self.extended, "?=") if encode else self.extended)
+        
+        return result
+
+    def equal(self, comp):
+        # Compare each component
+
+        if self.scheme != comp.scheme:
+            return False
+        
+        if self.server != comp.server:
+            return False
+        
+        # Ignore trailing slash
+        if self.path.rstrip("/") != comp.path.rstrip("/"):
+            return False
+
+        return True
+
+    def equalRelative(self, comp):
+        # Must be relative
+        if comp.server:
+            return False
+    
+        # Just compare paths, ignore trailing slash
+        return self.path.rstrip("/") == comp.path.rstrip("/")
+    
+    def dirname(self):
+        if self.path:
+            newpath = os.path.dirname(self.path.rstrip("/")) + "/"
+        return URL(scheme=self.scheme, server=self.server, path=newpath)
+
+    def _parse(self, url, decode=False):
+    
+        # Strip off main scheme
+        if url.lower().startswith("url:"):
+            url = url[4:]
+    
+        # Special - if it starts with "/" its a relative HTTP url
+        if url[0] == '/':
+            self.scheme = "http"
+            self.server = None
+            self._parsePath(url, decode)
+        else:
+            # Get protocol scheme
+            self.scheme = url[:url.find(":")].lower()
+            url = url[len(self.scheme):]
+        
+            if self.scheme in URL.URLSchemeDoubleSlash:
+    
+                assert(url.startswith("://"))
+    
+                # Look for server
+                splits = url[3:].split("/", 1)
+                self.server = splits[0]
+                if len(splits) == 2:
+                    self._parsePath("/" + splits[1], decode)
+            
+            elif self.scheme in ("mailto", "urn",):
+    
+                assert(url.startswith(":"))
+    
+                # Look for server
+                self.server = url[1:]
+
+    def _parsePath(self, path, decode=False):
+        
+        # Look for extended bits
+        splits = path.split("?", 1)
+        self.path = splits[0]
+        if decode:
+            self.path = urllib.unquote(self.path)
+        if len(splits) == 2:
+            self.extended = "?" + splits[1]
+            if decode:
+                self.extended = urllib.unquote_plus(self.extended)

Added: CalDAVClientLibrary/trunk/src/protocol/utils/__init__.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/utils/__init__.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/utils/__init__.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,15 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##

Added: CalDAVClientLibrary/trunk/src/protocol/utils/xmlhelpers.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/utils/xmlhelpers.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/utils/xmlhelpers.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,107 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from xml.etree.ElementTree import ElementTree
+from xml.etree.ElementTree import Comment
+from xml.etree.ElementTree import _escape_cdata
+from xml.etree.ElementTree import ProcessingInstruction
+from xml.etree.ElementTree import QName
+from xml.etree.ElementTree import fixtag
+from xml.etree.ElementTree import _raise_serialization_error
+from xml.etree.ElementTree import _encode
+from xml.etree.ElementTree import _escape_attrib
+from StringIO import StringIO
+from xml.etree.ElementTree import SubElement
+
+def SubElementWithData(parent, tag, data=None, attrs={}):
+    element = SubElement(parent, tag, attrs)
+    if data:
+        element.text = data
+    return element
+
+class BetterElementTree(ElementTree):
+    
+    def writeUTF8(self, file):
+        assert self._root is not None
+        if not hasattr(file, "write"):
+            file = open(file, "wb")
+        encoding = "utf-8"
+        file.write("<?xml version='1.0' encoding='%s'?>" % encoding)
+        self._prettywrite(file, self._root, encoding, {})
+        file.write("\r\n")
+
+    def _prettywrite(self, file, node, encoding, namespaces, depth=0):
+        # write XML to file
+        tag = node.tag
+        if tag is Comment:
+            file.write("\r\n" + "  " * depth)
+            file.write("<!-- %s -->" % _escape_cdata(node.text, encoding))
+        elif tag is ProcessingInstruction:
+            file.write("\r\n" + "  " * depth)
+            file.write("<?%s?>" % _escape_cdata(node.text, encoding))
+        else:
+            items = node.items()
+            xmlns_items = [] # new namespaces in this scope
+            try:
+                if isinstance(tag, QName) or tag[:1] == "{":
+                    tag, xmlns = fixtag(tag, namespaces)
+                    if xmlns: xmlns_items.append(xmlns)
+            except TypeError:
+                _raise_serialization_error(tag)
+            file.write("\r\n" + "  " * depth)
+            file.write("<" + _encode(tag, encoding))
+            if items or xmlns_items:
+                items.sort() # lexical order
+                for k, v in items:
+                    try:
+                        if isinstance(k, QName) or k[:1] == "{":
+                            k, xmlns = fixtag(k, namespaces)
+                            if xmlns: xmlns_items.append(xmlns)
+                    except TypeError:
+                        _raise_serialization_error(k)
+                    try:
+                        if isinstance(v, QName):
+                            v, xmlns = fixtag(v, namespaces)
+                            if xmlns: xmlns_items.append(xmlns)
+                    except TypeError:
+                        _raise_serialization_error(v)
+                    file.write(" %s=\"%s\"" % (_encode(k, encoding),
+                                               _escape_attrib(v, encoding)))
+                for k, v in xmlns_items:
+                    file.write(" %s=\"%s\"" % (_encode(k, encoding),
+                                               _escape_attrib(v, encoding)))
+            if node.text or len(node):
+                file.write(">")
+                if node.text:
+                    file.write(_escape_cdata(node.text, encoding))
+                for n in node:
+                    self._prettywrite(file, n, encoding, namespaces, depth=depth+1)
+                if not node.text or len(node):
+                    file.write("\r\n" + "  " * depth)
+                file.write("</" + _encode(tag, encoding) + ">")
+            else:
+                file.write(" />")
+            for k, v in xmlns_items:
+                del namespaces[v]
+        if node.tail:
+            file.write(_escape_cdata(node.tail, encoding))
+
+def elementToString(element):
+    os = StringIO()
+    xmldoc = BetterElementTree(element)
+    xmldoc.writeUTF8(os)
+    return os.getvalue()
+

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/__init__.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/__init__.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/__init__.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,32 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.webdav.propfindparser import PropFindParser
+from protocol.webdav.definitions import davxml
+
+PropFindParser.textProperties.add(davxml.creationdate)
+PropFindParser.textProperties.add(davxml.displayname)
+PropFindParser.textProperties.add(davxml.getcontentlanguage)
+PropFindParser.textProperties.add(davxml.getcontentlength)
+PropFindParser.textProperties.add(davxml.getcontenttype)
+PropFindParser.textProperties.add(davxml.getetag)
+PropFindParser.textProperties.add(davxml.getlastmodified)
+
+PropFindParser.hrefListProperties.add(davxml.principal_collection_set)
+PropFindParser.hrefProperties.add(davxml.principal_URL)
+PropFindParser.hrefListProperties.add(davxml.alternate_URI_set)
+PropFindParser.hrefListProperties.add(davxml.group_member_set)
+PropFindParser.hrefListProperties.add(davxml.group_membership)

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/ace.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/ace.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/ace.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,169 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from xml.etree.ElementTree import QName
+from protocol.webdav.definitions import davxml
+from xml.etree.ElementTree import SubElement
+
+class ACE(object):
+
+    def __init__(self):
+
+        self.principal = None
+        self.data = None
+        self.invert = False
+        self.grant = True
+        self.privs = ()
+        self.protected = False
+        self.inherited = False
+
+    def getPrincipal(self):
+        return self.principal
+
+    def setPrincipal(self, principal, data=None):
+        self.principal = principal
+        self.data = data
+
+    def canChange(self):
+        return not self.protected and not self.inherited
+
+    @staticmethod
+    def parseFromACL(aclnode):
+
+        aces = []
+        acenodes = aclnode.findall(str(davxml.ace))
+        for node in acenodes:
+            newace = ACE()
+            newace.parseACE(node)
+            aces.append(newace)
+        return aces
+         
+    def parseACE(self, acenode):
+        
+        assert(acenode and acenode.tag == davxml.ace)
+
+        # Get invert
+        self.invert = False
+        principal_parent = acenode
+        invert = acenode.find(str(davxml.invert))
+        if invert:
+            self.invert = True
+            principal_parent = invert
+
+        # Get the principal
+        principal = principal_parent.find(str(davxml.principal))
+        if not principal or len(principal.getchildren()) != 1:
+            return False
+        
+        # Determine principal info
+        child = principal.getchildren()[0]
+        if child.tag == davxml.href:
+            self.setPrincipal(child.tag, child.text)
+
+        elif child.tag in (davxml.all, davxml.authenticated, davxml.unauthenticated, davxml.self,):
+            self.setPrincipal(child.tag)
+
+        elif child.tag == davxml.property:
+            if len(child.getchildren()) == 1:
+                self.setPrincipal(child.tag, QName(child.getchildren()[0].tag))
+            else:
+                self.setPrincipal(child.tag)
+        
+        # Determine rights
+        self.grant = True
+        child = acenode.find(str(davxml.grant))
+        if not child:
+            child = acenode.find(str(davxml.deny))
+            if child:
+                self.grant = False
+        if child:
+            self.parsePrivileges(child)
+    
+        # Determine protected/inherited state
+        self.protected = acenode.find(str(davxml.protected)) is not None
+        self.inherited = acenode.find(str(davxml.inherited)) is not None
+        
+        return True
+
+    def parsePrivileges(self, parent):
+        
+        assert(parent.tag in (davxml.grant, davxml.deny,))
+        
+        # Parent node contains one of more privilege nodes which we parse
+        self.privs = ()
+        for privilege in parent.getchildren():
+            # Look for privilege
+            if privilege.tag != davxml.privilege or len(privilege.getchildren()) != 1:
+                continue
+
+            # Now get rights within the privilege
+            self.privs += (privilege.getchildren()[0].tag,)
+
+    def generateACE(self, aclnode):
+        # Structure of ace is:
+        #
+        #   <DAV:ace>
+        #     <DAV:principal>...</DAV:principal>
+        #       <DAV:grant>...</DAV:grant>
+        #   </DAV:ace>
+    
+        # <DAV:ace> element
+        ace = SubElement(aclnode, davxml.ace)
+        
+        if self.invert:
+            invert = SubElement(ace, davxml.invert)
+
+        # <DAV:principal> element
+        principal = SubElement(invert if self.invert else ace, davxml.principal)
+        
+        # Principal type
+        if self.principal == davxml.href:
+
+            # <DAV:href> element
+            href = SubElement(principal, davxml.href)
+            href.text = self.data
+
+        elif self.principal in (davxml.all, davxml.authenticated, davxml.unauthenticated, davxml.self,):
+
+            # <DAV:all>/<DAV:authenticated>/<DAV:unauthenticated>/<DAV:self> elements
+            SubElement(principal, self.principal)
+
+        elif self.principal == davxml.property:
+
+            # <DAV:property> element - the UID is the property element name
+            property = SubElement(principal, davxml.property)
+            SubElement(property, self.data)
+        
+        # Do grant rights for each one set
+        if self.grant:
+            # <DAV:grant> element
+            privs = SubElement(ace, davxml.grant)
+        
+        # Do deny rights for each one set
+        else:
+            # <DAV:deny> element
+            privs = SubElement(ace, davxml.deny)
+        
+        for item in self.privs:
+            priv = SubElement(privs, davxml.privilege)
+            SubElement(priv, item)
+
+        # <DAV:protected> and <DAV:inherited>
+        if self.protected:
+            SubElement(ace, davxml.protected)
+        if self.inherited:
+            SubElement(ace, davxml.inherited)
+            
\ No newline at end of file

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/acl.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/acl.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/acl.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,67 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.webdav.requestresponse import RequestResponse
+from protocol.webdav.definitions import methods
+from StringIO import StringIO
+from protocol.http.data.string import RequestDataString
+from protocol.webdav.definitions import davxml
+from xml.etree.ElementTree import Element
+from protocol.utils.xmlhelpers import BetterElementTree
+
+class ACL(RequestResponse):
+
+    def __init__(self, session, url, acls):
+        super(ACL, self).__init__(session, methods.ACL, url)
+        self.acls = acls
+        
+        self.initRequestData()
+
+    def initRequestData(self):
+        # Write XML info to a string
+        os = StringIO()
+        self.generateXML(os)
+        self.request_data = RequestDataString(os.getvalue(), "text/xml charset=utf-8")
+    
+    def generateXML(self, os):
+        # Structure of document is:
+        #
+        # <DAV:acl>
+        #   <DAV:ace>
+        #     <S:inheritable xmlns:S="http:#jakarta.apache.org/slide/"> / <S:non-inheritable xmlns:S="http:#jakarta.apache.org/slide/">
+        #     <DAV:principal>...</DAV:principal>
+        #       <DAV:grant>...</DAV:grant>
+        #   </DAV:ace>
+        #   ...
+        # </DAV:acl>
+        
+        # <DAV:acl> element
+        acl = Element(davxml.acl)
+        
+        # Do for each ACL
+        if self.acls:
+
+            for ace in self.acls:
+                # Cannot do if change not allowed
+                if not ace.canChange():
+                    continue
+                
+                # <DAV:ace> element
+                ace.generateACE(acl)
+        
+        # Now we have the complete document, so write it out (no indentation)
+        xmldoc = BetterElementTree(acl)
+        xmldoc.writeUTF8(os)

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/copy.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/copy.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/copy.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,22 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.webdav.copymovebase import CopyMoveBase
+
+class Copy(CopyMoveBase):
+
+    def __init__(self, session, url_old, absurl_new, overwrite=False):
+        super(Copy, self).__init__(session, url_old, absurl_new, overwrite=overwrite, delete_original=False)

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/copymovebase.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/copymovebase.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/copymovebase.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,45 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.webdav.requestresponse import RequestResponse
+from protocol.webdav.definitions import methods
+from protocol.webdav.definitions import headers
+
+class CopyMoveBase(RequestResponse):
+
+    def __init__(self, session, url_old, absurl_new, overwrite=False, delete_original=True):
+        super(CopyMoveBase, self).__init__(session, methods.MOVE if delete_original else methods.COPY, url_old)
+        self.absurl_new = absurl_new
+        self.overwrite = overwrite
+
+    def setData(self, etag):
+        self.request_data = None
+        self.response_data = None
+
+        # Must have matching ETag
+        if etag:
+            self.etag = etag
+            self.etag_match = True
+
+    def addHeaders(self, hdrs):
+        # Do default
+        super(CopyMoveBase, self).addHeaders(hdrs)
+        
+        # Add Destination header
+        hdrs.append((headers.Destination, self.absurl_new))
+        
+        # Add Overwrite header
+        hdrs.append((headers.Overwrite, headers.OverwriteTrue if self.overwrite else headers.OverwriteFalse))

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/definitions/__init__.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/definitions/__init__.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/definitions/__init__.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,15 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/definitions/davxml.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/definitions/davxml.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/definitions/davxml.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,87 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from xml.etree.ElementTree import QName
+
+DAVNamespace = "DAV:"
+
+propfind       = QName(DAVNamespace, "propfind")
+propname       = QName(DAVNamespace, "propname")
+allprop        = QName(DAVNamespace, "allprop")
+prop           = QName(DAVNamespace, "prop")
+propstat       = QName(DAVNamespace, "propstat")
+propertyupdate = QName(DAVNamespace, "propertyupdate")
+remove         = QName(DAVNamespace, "remove")
+set            = QName(DAVNamespace, "set")
+
+getetag            = QName(DAVNamespace, "getetag")
+creationdate       = QName(DAVNamespace, "creationdate")
+displayname        = QName(DAVNamespace, "displayname")
+getcontentlanguage = QName(DAVNamespace, "getcontentlanguage")
+getcontentlength   = QName(DAVNamespace, "getcontentlength")
+getcontenttype     = QName(DAVNamespace, "getcontenttype")
+getlastmodified    = QName(DAVNamespace, "getlastmodified")
+resourcetype       = QName(DAVNamespace, "resourcetype")
+collection         = QName(DAVNamespace, "collection")
+
+lockinfo      = QName(DAVNamespace, "lockinfo")
+lockscope     = QName(DAVNamespace, "lockscope")
+locktype      = QName(DAVNamespace, "locktype")
+owner         = QName(DAVNamespace, "owner")
+exclusive     = QName(DAVNamespace, "exclusive")
+shared        = QName(DAVNamespace, "shared")
+write         = QName(DAVNamespace, "write")
+
+acl           = QName(DAVNamespace, "acl")
+ace           = QName(DAVNamespace, "ace")
+invert        = QName(DAVNamespace, "invert")
+principal     = QName(DAVNamespace, "principal")
+privilege     = QName(DAVNamespace, "privilege")
+grant         = QName(DAVNamespace, "grant")
+deny          = QName(DAVNamespace, "deny")
+protected     = QName(DAVNamespace, "protected")
+inherited     = QName(DAVNamespace, "inherited")
+
+href                            = QName(DAVNamespace, "href")
+all                             = QName(DAVNamespace, "all")
+authenticated                   = QName(DAVNamespace, "authenticated")
+unauthenticated                 = QName(DAVNamespace, "unauthenticated")
+property                        = QName(DAVNamespace, "property")
+self                            = QName(DAVNamespace, "self")
+read                            = QName(DAVNamespace, "read")
+write                           = QName(DAVNamespace, "write")
+write_properties                = QName(DAVNamespace, "write-properties")
+write_content                   = QName(DAVNamespace, "write-content")
+read_acl                        = QName(DAVNamespace, "read-acl")
+read_current_user_privilege_set = QName(DAVNamespace, "read-current-user-privilege-set")
+write_acl                       = QName(DAVNamespace, "write-acl")
+bind                            = QName(DAVNamespace, "bind")
+unbind                          = QName(DAVNamespace, "unbind")
+all                             = QName(DAVNamespace, "all")
+
+multistatus         = QName(DAVNamespace, "multistatus")
+response            = QName(DAVNamespace, "response")
+responsedescription = QName(DAVNamespace, "responsedescription")
+status              = QName(DAVNamespace, "status")
+
+principal_match     = QName(DAVNamespace, "principal-match")
+
+principal_collection_set = QName(DAVNamespace, "principal-collection-set")
+
+alternate_URI_set   = QName(DAVNamespace, "alternate-URI-set")
+principal_URL       = QName(DAVNamespace, "principal-URL")
+group_member_set    = QName(DAVNamespace, "group-member-set")
+group_membership    = QName(DAVNamespace, "group-membership")

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/definitions/headers.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/definitions/headers.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/definitions/headers.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,39 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+# RFC2518 9 - Request Header fields (only the ones we need)
+
+from protocol.http.definitions.headers import * #@UnusedWildImport
+
+DAV = "DAV"
+DAV1 = "1"
+DAV2 = "2"
+DAVbis = "bis"
+DAVACL = "access-control"        # ACL extension RFC3744
+Depth = "Depth"
+Depth0 = "0"
+Depth1 = "1"
+DepthInfinity = "infinity"
+Destination = "Destination"
+If = "If"
+ForceAuthentication = "Force-Authentication"
+LockToken = "Lock-Token"
+Overwrite = "Overwrite"
+OverwriteTrue = "T"
+OverwriteFalse = "F"
+Timeout = "Timeout"
+TimeoutSeconds = "Second-"
+TimeoutInfinite = "Infinite"

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/definitions/methods.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/definitions/methods.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/definitions/methods.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,29 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.http.definitions.methods import * #@UnusedWildImport
+
+# RFC2518 - WebDAV Request Methods
+
+MKCOL     = "MKCOL"
+MOVE      = "MOVE"
+COPY      = "COPY"
+PROPFIND  = "PROPFIND"
+PROPPATCH = "PROPPATCH"
+LOCK      = "LOCK"
+UNLOCK    = "UNLOCK"
+REPORT    = "REPORT"           # RFC3253
+ACL       = "ACL"              # RFC3744

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/definitions/statuscodes.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/definitions/statuscodes.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/definitions/statuscodes.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,24 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.http.definitions.statuscodes import * #@UnusedWildImport
+
+Processing = 102
+MultiStatus = 207
+UnprocessableEntity = 422
+Locked = 423
+FailedDependency = 424
+InsufficientStorage = 507

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/delete.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/delete.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/delete.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,32 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.webdav.requestresponse import RequestResponse
+from protocol.webdav.definitions import methods
+
+class Delete(RequestResponse):
+
+    def __init__(self, session, url):
+        super(Delete, self).__init__(session, methods.DELETE, url)
+
+    def setData(self, etag=None):
+        self.request_data = None
+        self.response_data = None
+
+        # Must have matching ETag
+        if etag:
+            self.etag = etag
+            self.etag_match = True

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/get.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/get.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/get.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,22 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.webdav.getbase import GetBase
+
+class Get(GetBase):
+
+    def __init__(self, session, url, lock=None):
+        super(Get, self).__init__(session, url, lock=lock, head=False)

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/getbase.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/getbase.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/getbase.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,50 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.webdav.requestresponse import RequestResponse
+from protocol.webdav.definitions import methods
+from protocol.webdav.definitions import headers
+
+
+class GetBase(RequestResponse):
+
+    def __init__(self, session, url, lock=None, head=False):
+        super(GetBase, self).__init__(session, methods.HEAD if head else methods.GET, url, lock=lock)
+        self.head = head
+
+    def setData(self, response_data, etag=None):
+        self.request_data = None
+        self.response_data = response_data
+
+        # Must have matching ETag
+        if etag:
+            self.etag = etag
+            self.etag_match = True
+
+    def getNewETag(self):
+        # Get the ETag header from response headers
+        if self.hasResponseHeader(headers.ETag):
+            return self.getResponseHeader(headers.ETag)
+        else:
+            return None
+
+    def getContentLength(self):
+        # Always zero to prevent attempt to read response
+        return 0 if self.head else self.content_length
+    
+    def getChunked(self):
+        # Always false to prevent attempt to read response
+        return False if self.head else self.chunked

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/head.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/head.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/head.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,22 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.webdav.getbase import GetBase
+
+class Head(GetBase):
+
+    def __init__(self, session, url, lock=None):
+        super(Head, self).__init__(session, url, lock=lock, head=True)

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/lock.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/lock.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/lock.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,137 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.webdav.requestresponse import RequestResponse
+from protocol.webdav.definitions import methods
+from protocol.webdav.definitions import headers
+from StringIO import StringIO
+from protocol.http.data.string import RequestDataString
+from protocol.webdav.definitions import davxml
+from xml.etree.ElementTree import Element
+from protocol.utils.xmlhelpers import BetterElementTree
+
+class Lock(RequestResponse):
+
+    eExclusive = 0
+    eShared    = 1
+
+    eResourceMustExist     = 0
+    eResourceMustNotExist  = 1
+    eResourceMayExist      = 2
+
+    def __init__(self, session, url, depth, scope, owner, timeout, exists=eResourceMustExist):
+        
+        assert(depth in (headers.Depth0, headers.Depth1, headers.DepthInfinity))
+        assert(scope in (Lock.eExclusive, Lock.eShared,))
+        assert(exists in (Lock.eResourceMustExist, Lock.eResourceMustNotExist, Lock.eResourceMayExist))
+
+        super(Lock, self).__init__(session, methods.LOCK, url)
+
+        self.depth = depth
+        self.scope = scope
+        self.owner = owner
+        self.timeout = timeout
+    
+        # Do appropriate etag based on exists
+        if exists == Lock.eResourceMustExist:
+            self.etag = "*"
+            self.etag_match = True
+        elif exists == Lock.eResourceMustNotExist:
+            self.etag = "*"
+            self.etag_match = False
+        elif exists == Lock.eResourceMayExist:
+            pass
+    
+        self.initRequestData()
+
+    def initRequestData(self):
+        # Write XML info to a string
+        os = StringIO()
+        self.generateXML(os)
+        self.request_data = RequestDataString(os.getvalue(), "text/xml charset=utf-8")
+
+    def addHeaders(self, hdrs):
+        # Do default
+        super(Lock, self).addHeaders(hdrs)
+    
+        # Add depth header
+        hdrs.append((headers.Depth, self.depth))
+    
+        # Add timeout header
+        if self.timeout == -1:
+            hdrs.append((headers.Timeout, headers.TimeoutInfinite))
+        elif self.timeout > 0:
+            hdrs.append((headers.Timeout, "%s%d" % (headers.TimeoutSeconds, self.timeout)))
+
+    def generateXML(self, os):
+        # Structure of document is:
+        #
+        # <DAV:lockinfo>
+        #   <DAV:lockscope>
+        #     <DAV:exclusive/> | <DAV:shared/>
+        #   </DAV:lockscope>
+        #   <DAV:locktype>
+        #     <DAV:write/>
+        #   </DAV:locktype>
+        #   <DAV:owner>
+        #     <<owner>>
+        #   </DAV:owner>
+        # </DAV:lockinfo>
+        
+        # <DAV:lockinfo> element
+        lockinfo = Element(davxml.lockinfo)
+        
+        # <DAV:lockscope> element
+        lockscope = Element(davxml.lockscope)
+        lockinfo.append(lockscope)
+        
+        # <DAV:exclusive/> | <DAV:shared/> element
+        lockscope.append(Element(davxml.exclusive if self.scope == Lock.eExclusive else davxml.shared))
+        
+        # <DAV:locktype> element
+        locktype = Element(davxml.locktype)
+        lockinfo.append(locktype)
+        
+        # <DAV:write/> element
+        locktype.append(Element(davxml.write))
+        
+        # <DAV:owner> element is optional
+        if self.owner:
+            # <DAV:owner> element
+            owner = Element(davxml.owner)
+            owner.text = self.owner
+            lockinfo.append(owner)
+    
+        # Now we have the complete document, so write it out (no indentation)
+        BetterElementTree(lockinfo).writeUTF8(os)
+
+    def getLockToken(self):
+    
+        # Get the Lock-Token header from response headers
+        result = ""
+        if self.hasResponseHeader(headers.LockToken):
+
+            # Get Coded-URL
+            codedurl = self.getResponseHeader(headers.LockToken)
+            
+            # Strip leading/trailing <>
+            codeurl = codedurl.strip()
+            if codeurl.startswith("<") and codeurl.endswith(">"):
+                result = codeurl[1:-1]
+            else:
+                result = codeurl
+
+        return result

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/makecollection.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/makecollection.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/makecollection.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,23 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.webdav.requestresponse import RequestResponse
+from protocol.webdav.definitions import methods
+
+class MakeCollection(RequestResponse):
+
+    def __init__(self, session, url):
+        super(MakeCollection, self).__init__(session, methods.MKCOL, url)

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/move.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/move.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/move.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,22 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.webdav.copymovebase import CopyMoveBase
+
+class Move(CopyMoveBase):
+
+    def __init__(self, session, url_old, absurl_new, overwrite=False):
+        super(Move, self).__init__(session, url_old, absurl_new, overwrite=overwrite, delete_original=True)

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/multiresponseparser.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/multiresponseparser.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/multiresponseparser.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,36 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.webdav.xmlresponseparser import XMLResponseParser
+from protocol.webdav.definitions import davxml
+
+class MultiResponseParser(XMLResponseParser):
+
+    def parse(self, multistatus_node):
+        # Must have a node
+        if not multistatus_node:
+            return
+        
+        # Verify that the node is the correct element <DAV:multistatus>
+        if multistatus_node.tag != davxml.multistatus:
+            return
+        
+        # Node is the right type, so iterator over all child response nodes and process each one
+        for response in multistatus_node.getchildren():
+            self.parseResponse(response)
+    
+    def parseResponse(self, response):
+        raise NotImplementedError

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/options.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/options.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/options.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,51 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.webdav.requestresponse import RequestResponse
+from protocol.webdav.definitions import methods
+from protocol.http.util import parsetoken
+from protocol.webdav.definitions import headers
+
+class Options(RequestResponse):
+
+    def __init__(self, session, url):
+        super(Options, self).__init__(session, methods.OPTIONS, url)
+
+    def getAllowed(self):
+
+        methods = ()
+        if self.hasResponseHeader(headers.Allow):
+
+            # Look at each one and accumlate allowed methods
+            for value in self.getResponseHeaders(headers.Allow):
+                while value:
+                    token, value = parsetoken(value, ", \t")
+                    methods += (token,)
+                    
+        return methods
+
+    def isAllowed(self, method):
+        methods = ()
+        if self.hasResponseHeader(headers.Allow):
+
+            # Look at each one and accumlate allowed methods
+            for value in self.getResponseHeaders(headers.Allow):
+                while value:
+                    token, value = parsetoken(value, ", \t")
+                    if method == token:
+                        return True
+                    
+        return False

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/principalmatch.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/principalmatch.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/principalmatch.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,73 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.webdav.propfindbase import PropFindBase
+from protocol.webdav.definitions import headers
+from protocol.webdav.definitions import methods
+from StringIO import StringIO
+from protocol.http.data.string import RequestDataString
+from protocol.webdav.definitions import davxml
+from xml.etree.ElementTree import Element
+from xml.etree.ElementTree import SubElement
+from protocol.utils.xmlhelpers import BetterElementTree
+
+class PrincipalMatch(PropFindBase):
+
+    def __init__(self, session, url, props):
+        super(PrincipalMatch, self).__init__(session, url, headers.Depth0)
+        self.props = props
+        self.method = methods.REPORT
+        
+        self.initRequestData()
+
+    def initRequestData(self):
+        # Write XML info to a string
+        os = StringIO()
+        self.generateXML(os)
+        self.request_data = RequestDataString(os.getvalue(), "text/xml charset=utf-8")
+    
+    def generateXML(self, os):
+        # Structure of document is:
+        #
+        # <DAV:principal-match>
+        #   <DAV:self/>
+        #   <DAV:prop>
+        #     <<names of each property as elements>>
+        #   </DAV:prop>
+        # </DAV:principal-match>
+        
+        # <DAV:principal-match> element
+        principalmatch = Element(davxml.principal_match)
+        
+        # <DAV:self> element
+        SubElement(principalmatch, davxml.self)
+        
+        if self.props:
+
+            # <DAV:prop> element
+            prop = SubElement(principalmatch, davxml.prop)
+            
+            # Now add each property
+            for item in self.props:
+
+                # Add property element taking namespace into account
+    
+                # Look for DAV namespace and reuse that one
+                SubElement(prop, item)
+        
+        # Now we have the complete document, so write it out (no indentation)
+        xmldoc = BetterElementTree(principalmatch)
+        xmldoc.writeUTF8(os)

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/propall.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/propall.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/propall.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,43 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.webdav.propfindbase import PropFindBase
+from xml.etree.ElementTree import Element
+from protocol.webdav.definitions import davxml
+from protocol.utils.xmlhelpers import BetterElementTree
+
+class PropAll(PropFindBase):
+
+    def __init__(self, session, url, depth):
+        super(PropAll, self).__init__(session, url, depth)
+        self.initRequestData()
+
+    def generateXML(self, os):
+        # Structure of document is:
+        #
+        # <DAV:propfind>
+        #   <DAV:allprop/>
+        # </DAV:propfind>
+        
+        # <DAV:propfind> element
+        propfind = Element(davxml.propfind)
+        
+        # <DAV:propname> element
+        propfind.append(Element(davxml.allprop))
+        
+        # Now we have the complete document, so write it out (no indentation)
+        xmldoc = BetterElementTree(propfind)
+        xmldoc.writeUTF8(os)

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/propfind.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/propfind.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/propfind.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,53 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.webdav.propfindbase import PropFindBase
+from xml.etree.ElementTree import Element
+from protocol.webdav.definitions import davxml
+from protocol.utils.xmlhelpers import BetterElementTree
+
+class PropFind(PropFindBase):
+
+    def __init__(self, session, url, depth, props):
+        super(PropFind, self).__init__(session, url, depth)
+        self.props = props
+        
+        self.initRequestData()
+
+    def generateXML(self, os):
+        # Structure of document is:
+        #
+        # <DAV:propfind>
+        #   <DAV:prop>
+        #     <<names of each property as elements>>
+        #   </DAV:prop>
+        # </DAV:propfind>
+        
+        # <DAV:propfind> element
+        propfind = Element(davxml.propfind)
+        
+        # <DAV:prop> element
+        prop = Element(davxml.prop)
+        propfind.append(prop)
+        
+        # Now add each property
+        for propname in self.props:
+            # Add property element taking namespace into account
+            prop.append(Element(propname))
+        
+        # Now we have the complete document, so write it out (no indentation)
+        xmldoc = BetterElementTree(propfind)
+        xmldoc.writeUTF8(os)

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/propfindbase.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/propfindbase.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/propfindbase.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,48 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.webdav.requestresponse import RequestResponse
+from protocol.webdav.definitions import methods
+from protocol.webdav.definitions import headers
+from protocol.http.data.string import RequestDataString
+from StringIO import StringIO
+
+class PropFindBase(RequestResponse):
+
+    def __init__(self, session, url, depth):
+        assert(depth in (headers.Depth0, headers.Depth1, headers.DepthInfinity))
+
+        super(PropFindBase, self).__init__(session, methods.PROPFIND, url)
+        self.depth = depth
+
+    def initRequestData(self):
+        # Write XML info to a string
+        os = StringIO()
+        self.generateXML(os);
+        self.request_data = RequestDataString(os.getvalue(), "text/xml; charset=utf-8");
+    
+    def setOutput(self, response_data):
+        self.response_data = response_data
+
+    def addHeaders(self, hdrs):
+        # Do default
+        super(PropFindBase, self).addHeaders(hdrs)
+        
+        # Add depth header
+        hdrs.append((headers.Depth, self.depth))
+
+    def generateXML(self, os):
+        raise NotImplementedError

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/propfindparser.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/propfindparser.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/propfindparser.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,147 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.webdav.multiresponseparser import MultiResponseParser
+from protocol.webdav.definitions import davxml
+from protocol.http.util import parseStatusLine
+from xml.etree.ElementTree import QName
+from protocol.url import URL
+
+class PropFindParser(MultiResponseParser):
+
+    textProperties = set()
+    hrefProperties = set()
+    hrefListProperties = set()
+
+    class PropFindResult(object):
+        
+        def __init__(self):
+            self.textProperties = {}
+            self.hrefProperties = {}
+            self.nodeProperties = {}
+            self.badProperties = {}
+
+        def setResource(self, resource):
+            self.resource = resource
+        def getResource(self):
+            return self.resource
+        
+        def addTextProperty(self, name, value):
+            self.textProperties[name] = value
+        def getTextProperties(self):
+            return self.textProperties
+
+        def addHrefProperty(self, name, value):
+            self.hrefProperties[name] = value
+        def getHrefProperties(self):
+            return self.hrefProperties
+
+        def addNodeProperty(self, name, node):
+            self.nodeProperties[name] = node
+        def getNodeProperties(self):
+            return self.nodeProperties
+
+        def addBadProperty(self, name, status):
+            self.badProperties[name] = status
+        def getBadProperties(self):
+            return self.badProperties
+
+    def __init__(self):
+        self.results = {}
+    
+    def getResults(self):
+        return self.results
+
+    # Parse the response element down to the properties
+    def parseResponse(self, response):
+        # Verify that the node is the correct element <DAV:response>
+        if response.tag != davxml.response:
+            return
+        
+        # Node is the right type, so iterate over all child response nodes and process each one
+        result = PropFindParser.PropFindResult()
+        for child in response.getchildren():
+
+            # Is it the href
+            if child.tag == davxml.href:
+                result.setResource(child.text)
+            
+            # Is it propstat
+            elif child.tag == davxml.propstat:
+                self.parsePropStat(child, result)
+        
+        # Add the resource only if we got one
+        if result.getResource():
+            self.results[result.getResource()] = result
+
+    def parsePropStat(self, propstat, result):
+        # Scan the propstat node the status - we only process OK status
+    
+        # Now look for a <DAV:status> element in its children
+        status = propstat.find(str(davxml.status))
+        
+        # Now parse the response and dispatch accordingly
+        if status is not None:
+
+            status_result = parseStatusLine(status.text)
+            badstatus = status_result if status_result / 100 != 2 else None
+
+            # Now look for a <DAV:prop> element in its children
+            for item in propstat.findall(str(davxml.prop)):
+                self.parseProp(item, result, badstatus)                
+
+    def parseProp(self, property, result, badstatus):
+        # Scan the prop node - each child is processed
+        for item in property.getchildren():
+            self.parsePropElement(item, result, badstatus)
+    
+    # Parsing of property elements
+    def parsePropElement(self, prop, result, badstatus):
+        # Here we need to detect the type of element and dispatch accordingly
+        if badstatus:
+            result.addBadProperty(QName(prop.tag), badstatus)
+
+        elif prop.tag in PropFindParser.textProperties:
+            self.parsePropElementText(prop, result)
+
+        elif prop.tag in PropFindParser.hrefProperties:
+            self.parsePropElementHref(prop, result, False)
+
+        elif prop.tag in PropFindParser.hrefListProperties:
+            self.parsePropElementHref(prop, result, True)
+
+        else:
+            self.parsePropElementUnknown(prop, result)
+
+    def parsePropElementText(self, prop, result):
+        # Grab the element data
+        result.addTextProperty(QName(prop.tag), prop.text if prop.text else "")
+        result.addNodeProperty(QName(prop.tag), prop)
+
+    def parsePropElementHref(self, prop, result, is_list):
+        # Grab the element data
+        hrefs = tuple([URL(url=href.text, decode=True) for href in prop.findall(str(davxml.href))])
+        if not is_list:
+            if len(hrefs) == 1:
+                hrefs = hrefs[0]
+            else:
+                hrefs = ""
+        result.addHrefProperty(QName(prop.tag), hrefs)
+        result.addNodeProperty(QName(prop.tag), prop)
+
+    def parsePropElementUnknown(self, prop, result):
+        # Just add the node
+        result.addNodeProperty(QName(prop.tag), prop)

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/propnames.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/propnames.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/propnames.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,43 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.webdav.propfindbase import PropFindBase
+from xml.etree.ElementTree import Element
+from protocol.webdav.definitions import davxml
+from protocol.utils.xmlhelpers import BetterElementTree
+
+class PropNames(PropFindBase):
+
+    def __init__(self, session, url, depth):
+        super(PropNames, self).__init__(session, url, depth)
+        self.initRequestData()
+
+    def generateXML(self, os):
+        # Structure of document is:
+        #
+        # <DAV:propfind>
+        #   <DAV:propname/>
+        # </DAV:propfind>
+        
+        # <DAV:propfind> element
+        propfind = Element(davxml.propfind)
+        
+        # <DAV:propname> element
+        propfind.append(Element(davxml.propname))
+        
+        # Now we have the complete document, so write it out (no indentation)
+        xmldoc = BetterElementTree(propfind)
+        xmldoc.writeUTF8(os)

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/proppatch.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/proppatch.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/proppatch.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,75 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from xml.etree.ElementTree import Element
+from protocol.webdav.definitions import davxml
+from protocol.utils.xmlhelpers import BetterElementTree
+from protocol.webdav.requestresponse import RequestResponse
+from xml.etree.ElementTree import SubElement
+from protocol.webdav.definitions import methods
+from protocol.http.data.string import RequestDataString
+from StringIO import StringIO
+
+class PropPatch(RequestResponse):
+
+    def __init__(self, session, url, setprops=None, delprops=None):
+        super(PropPatch, self).__init__(session, methods.PROPPATCH, url)
+        self.setprops = setprops
+        self.delprops = delprops
+        
+        self.initRequestData()
+
+    def initRequestData(self):
+        # Write XML info to a string
+        os = StringIO()
+        self.generateXML(os);
+        self.request_data = RequestDataString(os.getvalue(), "text/xml; charset=utf-8");
+    
+    def setOutput(self, response_data):
+        self.response_data = response_data
+
+    def generateXML(self, os):
+        # Structure of document is:
+        #
+        # <DAV:propertyupdate>
+        #   <DAV:set>
+        #     <<names/values of each property as elements>>
+        #   </DAV:set>
+        #   <DAV:remove>
+        #     <<names of each property as elements>>
+        #   </DAV:remove>
+        # </DAV:propertyupdate>
+        
+        # <DAV:propertyupdate> element
+        propertyupdate = Element(davxml.propertyupdate)
+        
+        # <DAV:set> element
+        if self.setprops:
+            set = SubElement(propertyupdate, davxml.set)
+            propel = SubElement(set, davxml.prop)
+            for prop in self.setprops:
+                propel.append(prop)
+
+        # <DAV:remove> element
+        if self.delprops:
+            remove = SubElement(propertyupdate, davxml.remove)
+            propel = SubElement(remove, davxml.prop)
+            for prop in self.delprops:
+                propel.append(prop)
+
+        # Now we have the complete document, so write it out (no indentation)
+        xmldoc = BetterElementTree(propertyupdate)
+        xmldoc.writeUTF8(os)

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/put.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/put.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/put.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,46 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.webdav.requestresponse import RequestResponse
+from protocol.webdav.definitions import methods
+from protocol.webdav.definitions import headers
+
+class Put(RequestResponse):
+
+    def __init__(self, session, url, lock=None):
+        super(Put, self).__init__(session, methods.PUT, url, lock=lock)
+
+    def setData(self, request_data, response_data, etag=None, new_item=False):
+        assert(not (etag and new_item))
+        self.request_data = request_data
+        self.response_data = response_data
+    
+        # Must have matching ETag
+        if etag:
+            self.etag = etag
+            self.etag_match = True
+
+        # ETag should be '*' and we add If-None-Match header to ensure we do not overwrite something already there
+        if new_item:
+            self.etag = "*"
+            self.etag_match = False
+
+    def getNewETag(self):
+        # Get the ETag header from response headers
+        if self.hasResponseHeader(headers.ETag):
+            return self.getResponseHeader(headers.ETag)
+        else:
+            return None

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/report.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/report.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/report.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,26 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.webdav.requestresponse import RequestResponse
+from protocol.webdav.definitions import methods
+
+class Report(RequestResponse):
+
+    def __init__(self, session, url):
+        super(Report, self).__init__(session, methods.REPORT, url)
+
+    def _setOutput(self, response_data):
+        self.response_data = response_data;

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/requestresponse.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/requestresponse.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/requestresponse.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,37 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.http.requestresponse import RequestResponse as HTTPRequestResponse
+from protocol.http.requestresponse import RequestResponse
+from protocol.webdav.definitions import headers
+
+class RequestResponse(HTTPRequestResponse):
+
+    def __init__(self, session, method, ruri, etag=None, etag_match=False, lock=None):
+        super(RequestResponse, self).__init__(session, method, ruri, etag, etag_match)
+        self.lock = lock
+
+    def setLock(self, lock):
+        self.lock = lock
+
+    def addHeaders(self, hdrs):
+        # Do inherited
+        super(RequestResponse, self).addHeaders(hdrs)
+
+        # Do Lock matching
+        if self.lock:
+            # This is an untagged token - i.e. it applies to the resource being addressed
+            hdrs.append((headers.If, "(<%s>)" % (self.lock,)))

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/session.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/session.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/session.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,141 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.http.session import Session as HTTPSession
+from protocol.http.requestresponse import RequestResponse
+from protocol.http.data.string import ResponseDataString
+from protocol.http.definitions import methods
+from protocol.http.definitions import statuscodes
+from protocol.webdav.definitions import headers
+
+class Session(HTTPSession):
+    
+    def __init__(self, server, port=None, ssl=False, log=None):
+        super(Session, self).__init__(server, port, ssl, log)
+        self.initialised = False
+        self.version = ()
+
+    def initialise(self, host, base_uri):
+        # Set host change
+        self.setServer(host)
+        
+        # Loop repeating until we can do it or get a fatal error
+        first_time = True
+        while True:
+
+            # Create OPTIONS request for the base_uri
+            request = RequestResponse(self, methods.OPTIONS, base_uri)
+            request.setSession(self)
+            sout = ResponseDataString()
+            request.setData(None, sout)
+    
+            # Add request and process it
+            self.sendRequest(request)
+            
+            # Check response
+            if request.getStatusCode() == statuscodes.Unauthorized:
+
+                # If we had authorization before, then chances are auth details are wrong - so delete and try again with new auth
+                if self.hasAuthorization():
+
+                    self.authorization = None
+                    
+                    # Display error so user knows why the prompt occurs again
+                    self.displayHTTPError(request)
+    
+                # Get authorization object (prompt the user) and redo the request
+                self.authorization, cancelled = self.getAuthorizor(first_time, request.getResponseHeaders(headers.WWWAuthenticate))
+                
+                # Check for auth cancellation
+                if cancelled:
+
+                    self.authorization = None
+                    return False
+    
+                first_time = False
+                
+                # Repeat the request loop with new authorization
+                continue
+            
+            # Look for success and exit loop for further processing
+            if request.getStatusCode() in (statuscodes.OK, statuscodes.NoContent):
+
+                # Grab the server string
+                if request.hasResponseHeader(headers.Server):
+                    self.setServerDescriptor = self.setServerDescriptor(request.getResponseHeader(headers.Server))
+                
+                # Now check the response headers for a DAV version (may be more than one)
+                self.version = ()
+                for dav_version in request.getResponseHeaders(headers.DAV):
+                
+                    # Tokenize on commas
+                    for token in dav_version.split(","):
+
+                        token = token.strip()
+                        self.addVersion(token)
+
+                self.setServerType(self.version)
+    
+                # Put other strings into capability
+                capa = ""
+                for name, value in request.getResponseHeaders().iteritems():
+
+                    if (not name.lower().startswith(headers.Server) and
+                        not name.lower().startswith(headers.Date) and
+                        name.lower().startswith("Content-")):
+
+                        capa += "%s: %s\n" % (name, value,)
+
+                self.setServerCapability(capa)
+    
+                # Just assume any version is fine for now
+                break
+            
+            # If we get here we had some kind of fatal error
+            self.handleHTTPError(request)
+            return False
+        
+        self.initialised = True
+    
+        return True
+
+    def addVersion(self, token):
+        self.version += (token,)
+
+    def hasDAVVersion(self, version):
+        return version in self.version
+
+    def hasDAV(self):
+        return self.hasDAVVersion(headers.DAV1)
+
+    def hasDAVLocking(self):
+        return self.hasDAVVersion(headers.DAV2) or self.hasDAVVersion(headers.DAVbis)
+
+    def hasDAVACL(self):
+        return self.hasDAVVersion(headers.DAVACL)
+
+    def getAuthorizor(self, first_time, www_authenticate):
+        raise NotImplementedError
+    
+    def setServerType(self, type):
+        raise NotImplementedError
+    
+    def setServerDescriptor(self, txt):
+        raise NotImplementedError
+    
+    def setServerCapability(self, txt):
+        raise NotImplementedError
+    

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/tests/__init__.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/tests/__init__.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/tests/__init__.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,15 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_ace.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_ace.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_ace.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,134 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.webdav.ace import ACE
+from xml.etree.ElementTree import XML
+from protocol.webdav.definitions import davxml
+from xml.etree.ElementTree import Element
+from protocol.utils.xmlhelpers import BetterElementTree
+from StringIO import StringIO
+
+import unittest
+
+class TestRequest(unittest.TestCase):
+    
+    def verifyXML(self, x, y=None):
+        
+        x = x.replace("\n", "\r\n")
+        if y:
+            y = y.replace("\n", "\r\n")
+            
+        # Parse the XML data
+        a = ACE()
+        a.parseACE(XML(x))
+        
+        # Generate the XML data
+        aclnode = Element(davxml.acl)
+        a.generateACE(aclnode)
+        os = StringIO()
+        xmldoc = BetterElementTree(aclnode.getchildren()[0])
+        xmldoc.writeUTF8(os)
+        
+        # Verify data
+        self.assertEqual(os.getvalue(), y if y else x)
+
+    def test_XML1(self):
+        
+        self.verifyXML("""<?xml version='1.0' encoding='utf-8'?>
+<ns0:ace xmlns:ns0="DAV:">
+  <ns0:principal>
+    <ns0:href>/principals/users/a</ns0:href>
+  </ns0:principal>
+  <ns0:grant>
+    <ns0:privilege>
+      <ns0:read />
+    </ns0:privilege>
+  </ns0:grant>
+</ns0:ace>
+""")
+
+    def test_XML2(self):
+        
+        self.verifyXML("""<?xml version='1.0' encoding='utf-8'?>
+<ns0:ace xmlns:ns0="DAV:">
+  <ns0:principal>
+    <ns0:unauthenticated />
+  </ns0:principal>
+  <ns0:deny>
+    <ns0:privilege>
+      <ns0:read />
+    </ns0:privilege>
+    <ns0:privilege>
+      <ns0:write />
+    </ns0:privilege>
+  </ns0:deny>
+</ns0:ace>
+""")
+
+    def test_XML3(self):
+        
+        self.verifyXML("""<?xml version='1.0' encoding='utf-8'?>
+<ns0:ace xmlns:ns0="DAV:">
+  <ns0:principal>
+    <ns0:href>/principals/users/a</ns0:href>
+  </ns0:principal>
+  <ns0:grant>
+    <ns0:privilege>
+      <ns0:read />
+    </ns0:privilege>
+  </ns0:grant>
+  <ns0:protected />
+  <ns0:inherited />
+</ns0:ace>
+""")
+
+    def test_XML4(self):
+        
+        self.verifyXML("""<?xml version='1.0' encoding='utf-8'?>
+<ns0:ace xmlns:ns0="DAV:">
+  <ns0:invert>
+    <ns0:principal>
+      <ns0:self />
+    </ns0:principal>
+  </ns0:invert>
+  <ns0:grant>
+    <ns0:privilege>
+      <ns0:read />
+    </ns0:privilege>
+  </ns0:grant>
+  <ns0:protected />
+  <ns0:inherited />
+</ns0:ace>
+""")
+
+    def test_XML5(self):
+        
+        self.verifyXML("""<?xml version='1.0' encoding='utf-8'?>
+<ns0:ace xmlns:ns0="DAV:">
+  <ns0:principal>
+    <ns0:property>
+      <ns0:owner />
+    </ns0:property>
+  </ns0:principal>
+  <ns0:grant>
+    <ns0:privilege>
+      <ns0:read />
+    </ns0:privilege>
+  </ns0:grant>
+</ns0:ace>
+""")
+
+

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_acl.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_acl.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_acl.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,43 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.http.session import Session
+from protocol.webdav.acl import ACL
+
+import unittest
+
+class TestRequest(unittest.TestCase):
+    
+    def test_Method(self):
+        
+        server = Session("www.example.com")
+        request = ACL(server, "/", ())
+        self.assertEqual(request.getMethod(), "ACL")
+    
+class TestRequestHeaders(unittest.TestCase):
+    pass
+
+class TestRequestBody(unittest.TestCase):
+    pass
+
+class TestResponse(unittest.TestCase):
+    pass
+
+class TestResponseHeaders(unittest.TestCase):
+    pass
+
+class TestResponseBody(unittest.TestCase):
+    pass

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_copy.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_copy.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_copy.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,72 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.http.session import Session
+from protocol.webdav.copy import Copy
+
+import unittest
+
+class TestRequest(unittest.TestCase):
+    
+    def test_Method(self):
+        
+        server = Session("www.example.com")
+        request = Copy(server, "/a", "http://www.example.com/b")
+        self.assertEqual(request.getMethod(), "COPY")
+
+class TestRequestHeaders(unittest.TestCase):
+    def test_NoSpecialHeaders(self):
+        
+        server = Session("www.example.com")
+        request = Copy(server, "/a", "http://www.example.com/b")
+        hdrs = request.generateRequestHeader()
+        self.assertFalse("If-None-Match:" in hdrs)
+        self.assertFalse("If-Match:" in hdrs)
+        self.assertTrue("Overwrite: F" in hdrs)
+        self.assertTrue("Destination: http://www.example.com/b" in hdrs)
+    
+    def test_IfMatchHeader(self):
+        
+        server = Session("www.example.com")
+        request = Copy(server, "/a", "http://www.example.com/b")
+        request.setData(etag="\"12345\"")
+        hdrs = request.generateRequestHeader()
+        self.assertFalse("If-None-Match:" in hdrs)
+        self.assertTrue("If-Match: \"12345\"" in hdrs)
+        self.assertTrue("Overwrite: F" in hdrs)
+        self.assertTrue("Destination: http://www.example.com/b" in hdrs)
+    
+    def test_OverwriteHeader(self):
+        
+        server = Session("www.example.com")
+        request = Copy(server, "/a", "http://www.example.com/b", overwrite=True)
+        hdrs = request.generateRequestHeader()
+        self.assertFalse("If-None-Match:" in hdrs)
+        self.assertFalse("If-Match:" in hdrs)
+        self.assertTrue("Overwrite: T" in hdrs)
+        self.assertTrue("Destination: http://www.example.com/b" in hdrs)
+
+class TestRequestBody(unittest.TestCase):
+    pass
+
+class TestResponse(unittest.TestCase):
+    pass
+
+class TestResponseHeaders(unittest.TestCase):
+    pass
+
+class TestResponseBody(unittest.TestCase):
+    pass

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_delete.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_delete.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_delete.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,58 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.http.session import Session
+from protocol.webdav.delete import Delete
+
+import unittest
+
+class TestRequest(unittest.TestCase):
+    
+    def test_Method(self):
+        
+        server = Session("www.example.com")
+        request = Delete(server, "/")
+        self.assertEqual(request.getMethod(), "DELETE")
+
+class TestRequestHeaders(unittest.TestCase):
+    def test_NoSpecialHeaders(self):
+        
+        server = Session("www.example.com")
+        request = Delete(server, "/")
+        hdrs = request.generateRequestHeader()
+        self.assertFalse("If-None-Match:" in hdrs)
+        self.assertFalse("If-Match:" in hdrs)
+    
+    def test_IfMatchHeader(self):
+        
+        server = Session("www.example.com")
+        request = Delete(server, "/")
+        request.setData(etag="\"12345\"")
+        hdrs = request.generateRequestHeader()
+        self.assertFalse("If-None-Match:" in hdrs)
+        self.assertTrue("If-Match: \"12345\"" in hdrs)
+
+class TestRequestBody(unittest.TestCase):
+    pass
+
+class TestResponse(unittest.TestCase):
+    pass
+
+class TestResponseHeaders(unittest.TestCase):
+    pass
+
+class TestResponseBody(unittest.TestCase):
+    pass

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_get.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_get.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_get.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,58 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.http.session import Session
+from protocol.webdav.get import Get
+
+import unittest
+
+class TestRequest(unittest.TestCase):
+    
+    def test_Method(self):
+        
+        server = Session("www.example.com")
+        request = Get(server, "/")
+        self.assertEqual(request.getMethod(), "GET")
+
+class TestRequestHeaders(unittest.TestCase):
+    def test_NoSpecialHeaders(self):
+        
+        server = Session("www.example.com")
+        request = Get(server, "/")
+        hdrs = request.generateRequestHeader()
+        self.assertFalse("If-None-Match:" in hdrs)
+        self.assertFalse("If-Match:" in hdrs)
+    
+    def test_IfMatchHeader(self):
+        
+        server = Session("www.example.com")
+        request = Get(server, "/")
+        request.setData(None, etag="\"12345\"")
+        hdrs = request.generateRequestHeader()
+        self.assertFalse("If-None-Match:" in hdrs)
+        self.assertTrue("If-Match: \"12345\"" in hdrs)
+
+class TestRequestBody(unittest.TestCase):
+    pass
+
+class TestResponse(unittest.TestCase):
+    pass
+
+class TestResponseHeaders(unittest.TestCase):
+    pass
+
+class TestResponseBody(unittest.TestCase):
+    pass

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_head.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_head.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_head.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,58 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.http.session import Session
+from protocol.webdav.head import Head
+
+import unittest
+
+class TestRequest(unittest.TestCase):
+    
+    def test_Method(self):
+        
+        server = Session("www.example.com")
+        request = Head(server, "/")
+        self.assertEqual(request.getMethod(), "HEAD")
+
+class TestRequestHeaders(unittest.TestCase):
+    def test_NoSpecialHeaders(self):
+        
+        server = Session("www.example.com")
+        request = Head(server, "/")
+        hdrs = request.generateRequestHeader()
+        self.assertFalse("If-None-Match:" in hdrs)
+        self.assertFalse("If-Match:" in hdrs)
+    
+    def test_IfMatchHeader(self):
+        
+        server = Session("www.example.com")
+        request = Head(server, "/")
+        request.setData(None, etag="\"12345\"")
+        hdrs = request.generateRequestHeader()
+        self.assertFalse("If-None-Match:" in hdrs)
+        self.assertTrue("If-Match: \"12345\"" in hdrs)
+
+class TestRequestBody(unittest.TestCase):
+    pass
+
+class TestResponse(unittest.TestCase):
+    pass
+
+class TestResponseHeaders(unittest.TestCase):
+    pass
+
+class TestResponseBody(unittest.TestCase):
+    pass

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_lock.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_lock.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_lock.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,119 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.http.session import Session
+from protocol.webdav.lock import Lock
+from protocol.webdav.definitions import headers
+from StringIO import StringIO
+
+import unittest
+
+class TestRequest(unittest.TestCase):
+    
+    def test_Method(self):
+        
+        server = Session("www.example.com")
+        request = Lock(server, "/", headers.Depth0, Lock.eExclusive, "user at example.com", -1)
+        self.assertEqual(request.getMethod(), "LOCK")
+
+class TestRequestHeaders(unittest.TestCase):
+
+    def test_NoSpecialHeaders(self):
+        
+        server = Session("www.example.com")
+        request = Lock(server, "/", headers.Depth0, Lock.eExclusive, "user at example.com", -1, exists=Lock.eResourceMayExist)
+        hdrs = request.generateRequestHeader()
+        self.assertFalse("If-None-Match:" in hdrs)
+        self.assertFalse("If-Match:" in hdrs)
+    
+    def test_IfMatchHeader(self):
+        
+        server = Session("www.example.com")
+        request = Lock(server, "/", headers.Depth0, Lock.eExclusive, "user at example.com", -1, exists=Lock.eResourceMustExist)
+        hdrs = request.generateRequestHeader()
+        self.assertFalse("If-None-Match:" in hdrs)
+        self.assertTrue("If-Match: *" in hdrs)
+    
+    def test_IfNoneMatchHeader(self):
+        
+        server = Session("www.example.com")
+        request = Lock(server, "/", headers.Depth0, Lock.eExclusive, "user at example.com", -1, exists=Lock.eResourceMustNotExist)
+        hdrs = request.generateRequestHeader()
+        self.assertTrue("If-None-Match: *" in hdrs)
+        self.assertFalse("If-Match:" in hdrs)
+
+    def test_Depth0Headers(self):
+        
+        server = Session("www.example.com")
+        request = Lock(server, "/", headers.Depth0, Lock.eExclusive, "user at example.com", -1, exists=Lock.eResourceMustNotExist)
+        hdrs = request.generateRequestHeader()
+        self.assertTrue("Depth: 0" in hdrs)
+        self.assertFalse("Depth: 1" in hdrs)
+        self.assertFalse("Depth: infinity" in hdrs)
+
+    def test_Depth1Headers(self):
+        
+        server = Session("www.example.com")
+        request = Lock(server, "/", headers.Depth1, Lock.eExclusive, "user at example.com", -1, exists=Lock.eResourceMustNotExist)
+        hdrs = request.generateRequestHeader()
+        self.assertFalse("Depth: 0" in hdrs)
+        self.assertTrue("Depth: 1" in hdrs)
+        self.assertFalse("Depth: infinity" in hdrs)
+
+    def test_DepthInfinityHeaders(self):
+        
+        server = Session("www.example.com")
+        request = Lock(server, "/", headers.DepthInfinity, Lock.eExclusive, "user at example.com", -1, exists=Lock.eResourceMustNotExist)
+        hdrs = request.generateRequestHeader()
+        self.assertFalse("Depth: 0" in hdrs)
+        self.assertFalse("Depth: 1" in hdrs)
+        self.assertTrue("Depth: infinity" in hdrs)
+
+
+class TestRequestBody(unittest.TestCase):
+    
+    def test_GenerateXML(self):
+        
+        server = Session("www.example.com")
+        request = Lock(server, "/", headers.Depth0, Lock.eExclusive, "user at example.com", -1)
+        os = StringIO()
+        request.generateXML(os)
+        self.assertEqual(os.getvalue(), """<?xml version='1.0' encoding='utf-8'?>
+<ns0:lockinfo xmlns:ns0="DAV:">
+  <ns0:lockscope>
+    <ns0:exclusive />
+  </ns0:lockscope>
+  <ns0:locktype>
+    <ns0:write />
+  </ns0:locktype>
+  <ns0:owner>user at example.com</ns0:owner>
+</ns0:lockinfo>
+""".replace("\n", "\r\n")
+)
+
+class TestResponse(unittest.TestCase):
+    pass
+
+class TestResponseHeaders(unittest.TestCase):
+
+    def test_OneHeader(self):
+        
+        server = Session("www.example.com")
+        request = Lock(server, "/", headers.Depth0, Lock.eExclusive, "user at example.com", -1)
+        request.getResponseHeaders().update({
+            "Lock-Token": ("<user at example.com>",),
+        })
+        self.assertEqual(request.getLockToken(), "user at example.com")

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_move.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_move.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_move.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,72 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.http.session import Session
+from protocol.webdav.move import Move
+
+import unittest
+
+class TestRequest(unittest.TestCase):
+    
+    def test_Method(self):
+        
+        server = Session("www.example.com")
+        request = Move(server, "/a", "http://www.example.com/b")
+        self.assertEqual(request.getMethod(), "MOVE")
+
+class TestRequestHeaders(unittest.TestCase):
+    def test_NoSpecialHeaders(self):
+        
+        server = Session("www.example.com")
+        request = Move(server, "/a", "http://www.example.com/b")
+        hdrs = request.generateRequestHeader()
+        self.assertFalse("If-None-Match:" in hdrs)
+        self.assertFalse("If-Match:" in hdrs)
+        self.assertTrue("Overwrite: F" in hdrs)
+        self.assertTrue("Destination: http://www.example.com/b" in hdrs)
+    
+    def test_IfMatchHeader(self):
+        
+        server = Session("www.example.com")
+        request = Move(server, "/a", "http://www.example.com/b")
+        request.setData(etag="\"12345\"")
+        hdrs = request.generateRequestHeader()
+        self.assertFalse("If-None-Match:" in hdrs)
+        self.assertTrue("If-Match: \"12345\"" in hdrs)
+        self.assertTrue("Overwrite: F" in hdrs)
+        self.assertTrue("Destination: http://www.example.com/b" in hdrs)
+    
+    def test_OverwriteHeader(self):
+        
+        server = Session("www.example.com")
+        request = Move(server, "/a", "http://www.example.com/b", overwrite=True)
+        hdrs = request.generateRequestHeader()
+        self.assertFalse("If-None-Match:" in hdrs)
+        self.assertFalse("If-Match:" in hdrs)
+        self.assertTrue("Overwrite: T" in hdrs)
+        self.assertTrue("Destination: http://www.example.com/b" in hdrs)
+
+class TestRequestBody(unittest.TestCase):
+    pass
+
+class TestResponse(unittest.TestCase):
+    pass
+
+class TestResponseHeaders(unittest.TestCase):
+    pass
+
+class TestResponseBody(unittest.TestCase):
+    pass

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_options.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_options.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_options.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,68 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.http.session import Session
+from protocol.webdav.options import Options
+
+import unittest
+
+class TestRequest(unittest.TestCase):
+    
+    def test_Method(self):
+        
+        server = Session("www.example.com")
+        request = Options(server, "/")
+        self.assertEqual(request.getMethod(), "OPTIONS")
+    
+class TestRequestHeaders(unittest.TestCase):
+    pass
+
+class TestRequestBody(unittest.TestCase):
+    pass
+
+class TestResponse(unittest.TestCase):
+    pass
+
+class TestResponseHeaders(unittest.TestCase):
+
+    def test_OneHeader(self):
+        
+        server = Session("www.example.com")
+        request = Options(server, "/")
+        request.getResponseHeaders().update({
+            "Allow": ("GET, PUT, OPTIONS, HEAD",),
+        })
+        self.assertEqual(set(request.getAllowed()), set(("GET", "PUT", "OPTIONS", "HEAD")))
+        self.assertTrue(request.isAllowed("GET"))
+        self.assertTrue(request.isAllowed("PUT"))
+        self.assertTrue(request.isAllowed("OPTIONS"))
+        self.assertTrue(request.isAllowed("HEAD"))
+    
+    def test_MultipleHeader(self):
+        
+        server = Session("www.example.com")
+        request = Options(server, "/")
+        request.getResponseHeaders().update({
+            "Allow": ("GET, PUT", "OPTIONS, HEAD",),
+        })
+        self.assertEqual(set(request.getAllowed()), set(("GET", "PUT", "OPTIONS", "HEAD")))
+        self.assertTrue(request.isAllowed("GET"))
+        self.assertTrue(request.isAllowed("PUT"))
+        self.assertTrue(request.isAllowed("OPTIONS"))
+        self.assertTrue(request.isAllowed("HEAD"))
+
+class TestResponseBody(unittest.TestCase):
+    pass

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_principalmatch.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_principalmatch.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_principalmatch.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,84 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.webdav.session import Session
+from protocol.webdav.principalmatch import PrincipalMatch
+from StringIO import StringIO
+from protocol.webdav.definitions import davxml
+import unittest
+
+class TestRequest(unittest.TestCase):
+    
+    def test_Method(self):
+        
+        server = Session("www.example.com")
+        request = PrincipalMatch(server, "/", ())
+        self.assertEqual(request.getMethod(), "REPORT")
+    
+class TestRequestHeaders(unittest.TestCase):
+    
+    def test_Depth0Headers(self):
+        
+        server = Session("www.example.com")
+        request = PrincipalMatch(server, "/", ())
+        hdrs = request.generateRequestHeader()
+        self.assertTrue("Depth: 0" in hdrs)
+        self.assertFalse("Depth: 1" in hdrs)
+        self.assertFalse("Depth: infinity" in hdrs)
+
+class TestRequestBody(unittest.TestCase):
+ 
+    def test_GenerateXMLOneProperty(self):
+        
+        server = Session("www.example.com")
+        request = PrincipalMatch(server, "/", (davxml.getetag,))
+        os = StringIO()
+        request.generateXML(os)
+        self.assertEqual(os.getvalue(), """<?xml version='1.0' encoding='utf-8'?>
+<ns0:principal-match xmlns:ns0="DAV:">
+  <ns0:self />
+  <ns0:prop>
+    <ns0:getetag />
+  </ns0:prop>
+</ns0:principal-match>
+""".replace("\n", "\r\n")
+)
+
+    def test_GenerateXMLMultipleProperties(self):
+        
+        server = Session("www.example.com")
+        request = PrincipalMatch(server, "/", (davxml.getetag, davxml.displayname,))
+        os = StringIO()
+        request.generateXML(os)
+        self.assertEqual(os.getvalue(), """<?xml version='1.0' encoding='utf-8'?>
+<ns0:principal-match xmlns:ns0="DAV:">
+  <ns0:self />
+  <ns0:prop>
+    <ns0:getetag />
+    <ns0:displayname />
+  </ns0:prop>
+</ns0:principal-match>
+""".replace("\n", "\r\n")
+)
+
+class TestResponse(unittest.TestCase):
+    pass
+
+class TestResponseHeaders(unittest.TestCase):
+    pass
+
+class TestResponseBody(unittest.TestCase):
+    pass

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_propfind.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_propfind.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_propfind.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,87 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.webdav.session import Session
+from protocol.webdav.propfind import PropFind
+from StringIO import StringIO
+from protocol.webdav.definitions import davxml
+from xml.etree.ElementTree import QName
+from protocol.webdav.definitions import headers
+import unittest
+
+class TestRequest(unittest.TestCase):
+    
+    def test_Method(self):
+        
+        server = Session("www.example.com")
+        request = PropFind(server, "/", headers.Depth0, (davxml.getetag, QName("http://example.com/ns/", "taggy")))
+        self.assertEqual(request.getMethod(), "PROPFIND")
+
+class TestRequestHeaders(unittest.TestCase):
+    
+    def test_Depth0Headers(self):
+        
+        server = Session("www.example.com")
+        request = PropFind(server, "/", headers.Depth0, (davxml.getetag, QName("http://example.com/ns/", "taggy")))
+        hdrs = request.generateRequestHeader()
+        self.assertTrue("Depth: 0" in hdrs)
+        self.assertFalse("Depth: 1" in hdrs)
+        self.assertFalse("Depth: infinity" in hdrs)
+
+    def test_Depth1Headers(self):
+        
+        server = Session("www.example.com")
+        request = PropFind(server, "/", headers.Depth1, (davxml.getetag, QName("http://example.com/ns/", "taggy")))
+        hdrs = request.generateRequestHeader()
+        self.assertFalse("Depth: 0" in hdrs)
+        self.assertTrue("Depth: 1" in hdrs)
+        self.assertFalse("Depth: infinity" in hdrs)
+
+    def test_DepthInfinityHeaders(self):
+        
+        server = Session("www.example.com")
+        request = PropFind(server, "/", headers.DepthInfinity, (davxml.getetag, QName("http://example.com/ns/", "taggy")))
+        hdrs = request.generateRequestHeader()
+        self.assertFalse("Depth: 0" in hdrs)
+        self.assertFalse("Depth: 1" in hdrs)
+        self.assertTrue("Depth: infinity" in hdrs)
+
+class TestRequestBody(unittest.TestCase):
+ 
+    def test_GenerateXML(self):
+        
+        server = Session("www.example.com")
+        request = PropFind(server, "/", headers.Depth0, (davxml.getetag, QName("http://example.com/ns/", "taggy")))
+        os = StringIO()
+        request.generateXML(os)
+        self.assertEqual(os.getvalue(), """<?xml version='1.0' encoding='utf-8'?>
+<ns0:propfind xmlns:ns0="DAV:">
+  <ns0:prop>
+    <ns0:getetag />
+    <ns1:taggy xmlns:ns1="http://example.com/ns/" />
+  </ns0:prop>
+</ns0:propfind>
+""".replace("\n", "\r\n")
+)
+
+class TestResponse(unittest.TestCase):
+    pass
+
+class TestResponseHeaders(unittest.TestCase):
+    pass
+
+class TestResponseBody(unittest.TestCase):
+    pass

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_propfindparser.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_propfindparser.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_propfindparser.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,121 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.webdav.propfindparser import PropFindParser
+from xml.etree.ElementTree import XML
+from protocol.webdav.definitions import davxml
+import unittest
+
+class TestRequest(unittest.TestCase):
+    
+    def parseXML(self, x):
+        
+        x = x.replace("\n", "\r\n")
+            
+        # Parse the XML data
+        p = PropFindParser()
+        p.parse(XML(x))
+        return p
+
+    def checkResource(self, parser, resource, properties):
+        result = parser.getResults().get(resource, None)
+        self.assertTrue(result is not None)
+        for prop, value in properties:
+            self.assertEqual(result.getTextProperties().get(prop, None), value)
+
+    def test_SinglePropSingleResource(self):
+        
+        parser = self.parseXML("""<?xml version='1.0' encoding='utf-8'?>
+<ns0:multistatus xmlns:ns0="DAV:">
+  <ns0:response>
+    <ns0:href>/principals/users/a</ns0:href>
+    <ns0:propstat>
+      <ns0:prop>
+        <ns0:getetag>12345</ns0:getetag>
+      </ns0:prop>
+      <ns0:status>HTTP/1.1 200 OK</ns0:status>
+    </ns0:propstat>
+  </ns0:response>
+</ns0:multistatus>
+""")
+        
+        self.checkResource(parser, "/principals/users/a", (
+            (davxml.getetag, "12345",),
+        ))
+
+    def test_MultiplePropsSingleResource(self):
+        
+        parser = self.parseXML("""<?xml version='1.0' encoding='utf-8'?>
+<ns0:multistatus xmlns:ns0="DAV:">
+  <ns0:response>
+    <ns0:href>/principals/users/a</ns0:href>
+    <ns0:propstat>
+      <ns0:prop>
+        <ns0:displayname>Name</ns0:displayname>
+        <ns0:getetag>12345</ns0:getetag>
+      </ns0:prop>
+      <ns0:status>HTTP/1.1 200 OK</ns0:status>
+    </ns0:propstat>
+  </ns0:response>
+</ns0:multistatus>
+""")
+        
+        result = parser.getResults().get("/principals/users/a", None)
+        self.assertTrue(result is not None)
+        self.assertEqual(result.getTextProperties().get(davxml.getetag, None), "12345")
+        self.assertEqual(result.getTextProperties().get(davxml.displayname, None), "Name")
+
+        self.checkResource(parser, "/principals/users/a", (
+            (davxml.getetag,     "12345",),
+            (davxml.displayname, "Name",),
+        ))
+
+    def test_MultiplePropsSingleMultipleResources(self):
+        
+        parser = self.parseXML("""<?xml version='1.0' encoding='utf-8'?>
+<ns0:multistatus xmlns:ns0="DAV:">
+  <ns0:response>
+    <ns0:href>/principals/users/a</ns0:href>
+    <ns0:propstat>
+      <ns0:prop>
+        <ns0:displayname>Name1</ns0:displayname>
+        <ns0:getetag>1</ns0:getetag>
+      </ns0:prop>
+      <ns0:status>HTTP/1.1 200 OK</ns0:status>
+    </ns0:propstat>
+  </ns0:response>
+  <ns0:response>
+    <ns0:href>/principals/users/b</ns0:href>
+    <ns0:propstat>
+      <ns0:prop>
+        <ns0:displayname>Name2</ns0:displayname>
+        <ns0:getetag>2</ns0:getetag>
+      </ns0:prop>
+      <ns0:status>HTTP/1.1 200 OK</ns0:status>
+    </ns0:propstat>
+  </ns0:response>
+</ns0:multistatus>
+""")
+
+        self.checkResource(parser, "/principals/users/a", (
+            (davxml.getetag,     "1",),
+            (davxml.displayname, "Name1",),
+        ))
+
+        self.checkResource(parser, "/principals/users/b", (
+            (davxml.getetag,     "2",),
+            (davxml.displayname, "Name2",),
+        ))

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_propnames.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_propnames.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_propnames.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,82 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.webdav.session import Session
+from protocol.webdav.propnames import PropNames
+from StringIO import StringIO
+from protocol.webdav.definitions import headers
+import unittest
+
+class TestPropNames(unittest.TestCase):
+    
+    def test_Method(self):
+        
+        server = Session("www.example.com")
+        request = PropNames(server, "/", headers.Depth0)
+        self.assertEqual(request.getMethod(), "PROPFIND")
+    
+class TestRequestHeaders(unittest.TestCase):
+    
+    def test_Depth0Headers(self):
+        
+        server = Session("www.example.com")
+        request = PropNames(server, "/", headers.Depth0)
+        hdrs = request.generateRequestHeader()
+        self.assertTrue("Depth: 0" in hdrs)
+        self.assertFalse("Depth: 1" in hdrs)
+        self.assertFalse("Depth: infinity" in hdrs)
+
+    def test_Depth1Headers(self):
+        
+        server = Session("www.example.com")
+        request = PropNames(server, "/", headers.Depth1)
+        hdrs = request.generateRequestHeader()
+        self.assertFalse("Depth: 0" in hdrs)
+        self.assertTrue("Depth: 1" in hdrs)
+        self.assertFalse("Depth: infinity" in hdrs)
+
+    def test_DepthInfinityHeaders(self):
+        
+        server = Session("www.example.com")
+        request = PropNames(server, "/", headers.DepthInfinity)
+        hdrs = request.generateRequestHeader()
+        self.assertFalse("Depth: 0" in hdrs)
+        self.assertFalse("Depth: 1" in hdrs)
+        self.assertTrue("Depth: infinity" in hdrs)
+
+class TestRequestBody(unittest.TestCase):
+ 
+    def test_GenerateXML(self):
+        
+        server = Session("www.example.com")
+        request = PropNames(server, "/", headers.Depth0)
+        os = StringIO()
+        request.generateXML(os)
+        self.assertEqual(os.getvalue(), """<?xml version='1.0' encoding='utf-8'?>
+<ns0:propfind xmlns:ns0="DAV:">
+  <ns0:propname />
+</ns0:propfind>
+""".replace("\n", "\r\n")
+)
+
+class TestResponse(unittest.TestCase):
+    pass
+
+class TestResponseHeaders(unittest.TestCase):
+    pass
+
+class TestResponseBody(unittest.TestCase):
+    pass

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_put.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_put.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_put.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,75 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.http.session import Session
+from protocol.webdav.put import Put
+
+import unittest
+
+class TestRequest(unittest.TestCase):
+    
+    def test_Method(self):
+        
+        server = Session("www.example.com")
+        request = Put(server, "/")
+        self.assertEqual(request.getMethod(), "PUT")
+    
+class TestRequestHeaders(unittest.TestCase):
+
+    def test_NoSpecialHeaders(self):
+        
+        server = Session("www.example.com")
+        request = Put(server, "/")
+        request.setData(None, None)
+        hdrs = request.generateRequestHeader()
+        self.assertFalse("If-None-Match:" in hdrs)
+        self.assertFalse("If-Match:" in hdrs)
+    
+    def test_IfMatchHeader(self):
+        
+        server = Session("www.example.com")
+        request = Put(server, "/")
+        request.setData(None, None, etag="\"12345\"")
+        hdrs = request.generateRequestHeader()
+        self.assertFalse("If-None-Match:" in hdrs)
+        self.assertTrue("If-Match: \"12345\"" in hdrs)
+    
+    def test_IfNoneMatchHeader(self):
+        
+        server = Session("www.example.com")
+        request = Put(server, "/")
+        request.setData(None, None, new_item=True)
+        hdrs = request.generateRequestHeader()
+        self.assertTrue("If-None-Match: *" in hdrs)
+        self.assertFalse("If-Match:" in hdrs)
+    
+    def test_Bad(self):
+        
+        server = Session("www.example.com")
+        request = Put(server, "/")
+        self.assertRaises(AssertionError, request.setData, None, None, **{"etag": "\"12345\"", "new_item": True})
+
+class TestRequestBody(unittest.TestCase):
+    pass
+
+class TestResponse(unittest.TestCase):
+    pass
+
+class TestResponseHeaders(unittest.TestCase):
+    pass
+
+class TestResponseBody(unittest.TestCase):
+    pass

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_report.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_report.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_report.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,43 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.http.session import Session
+from protocol.webdav.report import Report
+
+import unittest
+
+class TestRequest(unittest.TestCase):
+    
+    def test_Method(self):
+        
+        server = Session("www.example.com")
+        request = Report(server, "/")
+        self.assertEqual(request.getMethod(), "REPORT")
+    
+class TestRequestHeaders(unittest.TestCase):
+    pass
+
+class TestRequestBody(unittest.TestCase):
+    pass
+
+class TestResponse(unittest.TestCase):
+    pass
+
+class TestResponseHeaders(unittest.TestCase):
+    pass
+
+class TestResponseBody(unittest.TestCase):
+    pass

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_template.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_template.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_template.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,35 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+import unittest
+
+class TestRequest(unittest.TestCase):
+    pass
+
+class TestRequestHeaders(unittest.TestCase):
+    pass
+
+class TestRequestBody(unittest.TestCase):
+    pass
+
+class TestResponse(unittest.TestCase):
+    pass
+
+class TestResponseHeaders(unittest.TestCase):
+    pass
+
+class TestResponseBody(unittest.TestCase):
+    pass

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_unlock.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_unlock.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/tests/test_unlock.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,49 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.http.session import Session
+from protocol.webdav.unlock import Unlock
+
+import unittest
+
+class TestRequest(unittest.TestCase):
+    
+    def test_Method(self):
+        
+        server = Session("www.example.com")
+        request = Unlock(server, "/", "locked-up-in-chains")
+        self.assertEqual(request.getMethod(), "UNLOCK")
+
+class TestRequestHeaders(unittest.TestCase):
+
+    def test_LockTokenHeaders(self):
+        
+        server = Session("www.example.com")
+        request = Unlock(server, "/", "locked-up-in-chains")
+        hdrs = request.generateRequestHeader()
+        self.assertTrue("Lock-Token: <locked-up-in-chains>" in hdrs)
+
+class TestRequestBody(unittest.TestCase):
+    pass
+
+class TestResponse(unittest.TestCase):
+    pass
+
+class TestResponseHeaders(unittest.TestCase):
+    pass
+
+class TestResponseBody(unittest.TestCase):
+    pass

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/unlock.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/unlock.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/unlock.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,34 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.webdav.requestresponse import RequestResponse
+from protocol.webdav.definitions import methods
+from protocol.webdav.definitions import headers
+
+class Unlock(RequestResponse):
+
+    def __init__(self, session, url, lock_token):
+        
+        super(Unlock, self).__init__(session, methods.UNLOCK, url)
+
+        self.lock_token = lock_token
+
+    def addHeaders(self, hdrs):
+        # Do default
+        super(Unlock, self).addHeaders(hdrs)
+    
+        # Add lock-token header
+        hdrs.append((headers.LockToken, "<%s>" % (self.lock_token,)))

Added: CalDAVClientLibrary/trunk/src/protocol/webdav/xmlresponseparser.py
===================================================================
--- CalDAVClientLibrary/trunk/src/protocol/webdav/xmlresponseparser.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/protocol/webdav/xmlresponseparser.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,31 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from xml.etree.ElementTree import XML
+
+class XMLResponseParser(object):
+
+    def parseData(self, data):
+        # XML parse the data
+        self.parse(XML(data))
+
+    def parseFile(self, fpath):
+        fp = open(fpath, "r")
+        self.parse(XML(fp.read()))
+        fp.close()
+
+    def parse(self, root_node):
+        raise NotImplementedError

Added: CalDAVClientLibrary/trunk/src/ui/WebDAVBrowser.nib/classes.nib
===================================================================
--- CalDAVClientLibrary/trunk/src/ui/WebDAVBrowser.nib/classes.nib	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/ui/WebDAVBrowser.nib/classes.nib	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>IBClasses</key>
+	<array>
+		<dict>
+			<key>CLASS</key>
+			<string>FirstResponder</string>
+			<key>LANGUAGE</key>
+			<string>ObjC</string>
+			<key>SUPERCLASS</key>
+			<string>NSObject</string>
+		</dict>
+		<dict>
+			<key>CLASS</key>
+			<string>NSTableView</string>
+			<key>LANGUAGE</key>
+			<string>ObjC</string>
+			<key>SUPERCLASS</key>
+			<string>NSControl</string>
+		</dict>
+		<dict>
+			<key>ACTIONS</key>
+			<dict>
+				<key>browserAction</key>
+				<string>id</string>
+				<key>changeBrowserView</key>
+				<string>id</string>
+				<key>changeDataView</key>
+				<string>id</string>
+				<key>resetServer</key>
+				<string>id</string>
+				<key>startupCancelAction</key>
+				<string>id</string>
+				<key>startupOKAction</key>
+				<string>id</string>
+				<key>tableAction</key>
+				<string>id</string>
+			</dict>
+			<key>CLASS</key>
+			<string>WebDAVBrowserDelegate</string>
+			<key>LANGUAGE</key>
+			<string>ObjC</string>
+			<key>OUTLETS</key>
+			<dict>
+				<key>browser</key>
+				<string>id</string>
+				<key>columnView</key>
+				<string>id</string>
+				<key>dataView</key>
+				<string>id</string>
+				<key>list</key>
+				<string>id</string>
+				<key>listView</key>
+				<string>id</string>
+				<key>mainSplitterView</key>
+				<string>id</string>
+				<key>passwordText</key>
+				<string>id</string>
+				<key>pathLabel</key>
+				<string>id</string>
+				<key>progress</key>
+				<string>id</string>
+				<key>propertiesView</key>
+				<string>id</string>
+				<key>serverText</key>
+				<string>id</string>
+				<key>startupSheet</key>
+				<string>id</string>
+				<key>table</key>
+				<string>id</string>
+				<key>text</key>
+				<string>id</string>
+				<key>toolbarBrowserViewButton</key>
+				<string>id</string>
+				<key>toolbarDataViewButton</key>
+				<string>id</string>
+				<key>userText</key>
+				<string>id</string>
+				<key>webView</key>
+				<string>id</string>
+				<key>window</key>
+				<string>id</string>
+			</dict>
+		</dict>
+	</array>
+	<key>IBVersion</key>
+	<string>1</string>
+</dict>
+</plist>

Added: CalDAVClientLibrary/trunk/src/ui/WebDAVBrowser.nib/info.nib
===================================================================
--- CalDAVClientLibrary/trunk/src/ui/WebDAVBrowser.nib/info.nib	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/ui/WebDAVBrowser.nib/info.nib	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>IBFramework Version</key>
+	<string>629</string>
+	<key>IBOldestOS</key>
+	<integer>5</integer>
+	<key>IBOpenObjects</key>
+	<array/>
+	<key>IBSystem Version</key>
+	<string>9B18</string>
+	<key>targetFramework</key>
+	<string>IBCocoaFramework</string>
+</dict>
+</plist>

Added: CalDAVClientLibrary/trunk/src/ui/WebDAVBrowser.nib/keyedobjects.nib
===================================================================
(Binary files differ)


Property changes on: CalDAVClientLibrary/trunk/src/ui/WebDAVBrowser.nib/keyedobjects.nib
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: CalDAVClientLibrary/trunk/src/ui/WebDAVBrowser.py
===================================================================
--- CalDAVClientLibrary/trunk/src/ui/WebDAVBrowser.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/ui/WebDAVBrowser.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,574 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Code based on PyObjC examples included with Apple Developer tools.
+"""
+
+import Foundation, AppKit, WebKit #@UnusedImport
+
+from AppKit import * #@UnusedWildImport
+from AppKit import NSApplication #@UnresolvedImport
+from AppKit import NSImage #@UnresolvedImport
+from AppKit import NSMenuItem #@UnresolvedImport
+from AppKit import NSObject #@UnresolvedImport
+from AppKit import NSPlainFileType #@UnresolvedImport
+from AppKit import NSSize #@UnresolvedImport
+from AppKit import NSToolbar #@UnresolvedImport
+from AppKit import NSToolbarCustomizeToolbarItemIdentifier #@UnresolvedImport
+from AppKit import NSToolbarFlexibleSpaceItemIdentifier #@UnresolvedImport
+from AppKit import NSToolbarItem #@UnresolvedImport
+from AppKit import NSToolbarPrintItemIdentifier #@UnresolvedImport
+from AppKit import NSToolbarSeparatorItemIdentifier #@UnresolvedImport
+from AppKit import NSToolbarSpaceItemIdentifier #@UnresolvedImport
+from AppKit import NSURL #@UnresolvedImport
+from AppKit import NSUserDefaults #@UnresolvedImport
+from AppKit import NSWorkspace #@UnresolvedImport
+
+from Foundation import * #@UnusedWildImport
+
+from PyObjCTools import NibClassBuilder, AppHelper
+
+from objc import IBOutlet #@UnusedImport
+from objc import YES, NO
+from objc import selector #@UnusedImport
+
+from protocol.utils import xmlhelpers
+from ui.session import Session
+from xml.etree.ElementTree import _ElementInterface
+import types
+
+NibClassBuilder.extractClasses("WebDAVBrowser")
+
+kServerToolbarItemIdentifier = "WDB: Server Toolbar Identifier"
+kRefreshToolbarItemIdentifier = "WDB: Refresh Toolbar Identifier"
+kBrowserViewToolbarItemIdentifier = "WDB: Browser View Toolbar Identifier"
+kDataViewToolbarItemIdentifier = "WDB: Data View Toolbar Identifier"
+
+def addToolbarItem(aController, anIdentifier, aLabel, aPaletteLabel,
+                   aToolTip, aTarget, anAction, anItemContent, aMenu):
+    """
+    Add a toolbar button of some kind.
+    """
+    toolbarItem = NSToolbarItem.alloc().initWithItemIdentifier_(anIdentifier)
+
+    toolbarItem.setLabel_(aLabel)
+    toolbarItem.setPaletteLabel_(aPaletteLabel)
+    toolbarItem.setToolTip_(aToolTip)
+    toolbarItem.setTarget_(aTarget)
+    if anAction:
+        toolbarItem.setAction_(anAction)
+
+    if type(anItemContent) == NSImage:
+        toolbarItem.setImage_(anItemContent)
+    else:
+        toolbarItem.setView_(anItemContent)
+        bounds = anItemContent.bounds()
+        minSize = (bounds[1][0], bounds[1][1])
+        maxSize = (bounds[1][0], bounds[1][1])
+        toolbarItem.setMinSize_( minSize )
+        toolbarItem.setMaxSize_( maxSize )
+
+    if aMenu:
+        menuItem = NSMenuItem.alloc().init()
+        menuItem.setSubmenu_(aMenu)
+        menuItem.setTitle_( aMenu.title() )
+        toolbarItem.setMenuFormRepresentation_(menuItem)
+
+    aController._toolbarItems[anIdentifier] = toolbarItem
+
+WRAPPED={}
+class Wrapper(NSObject):
+    """
+    NSOutlineView doesn't retain values, which means we cannot use normal
+    python values as values in an outline view.
+    """
+    def init_(self, value):
+        self.value = value
+        return self
+
+    def __str__(self):
+        return '<Wrapper for %s>' % self.value
+
+    def description(self):
+        return str(self)
+
+def wrap_object(obj):
+    if WRAPPED.has_key(obj):
+        return WRAPPED[obj]
+    else:
+        WRAPPED[obj] = Wrapper.alloc().init_(obj)
+        return WRAPPED[obj]
+
+def unwrap_object(obj):
+    if obj is None:
+        return obj
+    return obj.value
+
+class WebDAVBrowserDelegate(NibClassBuilder.AutoBaseClass):
+    """
+    Class defined in NIB file. This acts as the delegate and responder
+    for the various NSViews and toolbar items. It basically our controller.
+    """
+
+    BROWSERVIEW_COLUMNS = 0
+    BROWSERVIEW_LIST    = 1
+
+    DATAVIEW_PROPERTIES = 0
+    DATAVIEW_DATA       = 1
+    #DATAVIEW_DELEGATES  = 2
+    #DATAVIEW_ACLS       = 3
+
+    selectedDetails = None
+
+    def awakeFromNib(self):
+        # Initialise our session and selected state parameters
+        self.session = None
+        self.columns = [[]]
+        self.selectedResource = None
+        self.selectedDetails = None
+        self.selectedData = None
+
+        # Cache some useful icons
+        self.fileImage = NSWorkspace.sharedWorkspace().iconForFileType_(NSPlainFileType)
+        self.fileImage.setSize_(NSSize(16, 16))
+        self.directoryImage = NSWorkspace.sharedWorkspace().iconForFile_("/usr/")
+        self.directoryImage.setSize_(NSSize(16, 16))
+
+        # Initialise the toolbar
+        self._toolbarItems = {}
+        self._toolbarDefaultItemIdentifiers = []
+        self._toolbarAllowedItemIdentifiers = []
+        self.createToolbar()
+
+        # Set up browser view
+        container = self.mainSplitterView.subviews()[0]
+        container.addSubview_(self.columnView)
+        container.addSubview_(self.listView)
+        self.currentBrowserView = None
+        self.setBrowserView(self.BROWSERVIEW_COLUMNS)
+        self.browser.setMaxVisibleColumns_(7)
+        self.browser.setMinColumnWidth_(150)
+
+        # Set up data view
+        container = self.mainSplitterView.subviews()[1]
+        container.addSubview_(self.propertiesView)
+        container.addSubview_(self.dataView)
+        self.currentDataView = None
+        self.setDataView(self.DATAVIEW_PROPERTIES)
+        self.text.setString_("") 
+        
+        self.pathLabel.setStringValue_("No server specified")
+    
+        # Get preferences
+        lastServer = NSUserDefaults.standardUserDefaults().stringForKey_("LastServer")
+        if lastServer and len(lastServer):
+            self.serverText.setStringValue_(lastServer)
+        lastUser = NSUserDefaults.standardUserDefaults().stringForKey_("LastUser")
+        if lastUser and len(lastUser):
+            self.userText.setStringValue_(lastUser)
+
+    def createToolbar(self):
+        """
+        Create the toolbar for our app.
+        """
+        toolbar = NSToolbar.alloc().initWithIdentifier_("Toolbar")
+        toolbar.setDelegate_(self)
+        toolbar.setAllowsUserCustomization_(YES)
+        toolbar.setAutosavesConfiguration_(YES)
+
+        self.createToolbarItems()
+
+        self.window.setToolbar_(toolbar)
+
+    def createToolbarItems(self):
+        """
+        Create the toolbar item and define the default and allowed set.
+        """
+        addToolbarItem(self, kServerToolbarItemIdentifier,
+                       "Server", "Server", "Reset Server", self,
+                       "resetServer:", NSImage.imageNamed_("NSNetwork"), None,)
+
+        addToolbarItem(self, kRefreshToolbarItemIdentifier,
+                       "Refresh", "Refresh", "Refresh Display", self,
+                       "refreshData:", NSImage.imageNamed_("NSRefresh"), None,)
+
+        addToolbarItem(self, kBrowserViewToolbarItemIdentifier,
+                       "Browser", "Browser", "Browser View", self,
+                       "changeBrowserView:", self.toolbarBrowserViewButton, None,)
+
+        addToolbarItem(self, kDataViewToolbarItemIdentifier,
+                       "View", "View", "Data View", self,
+                       "changeDataView:", self.toolbarDataViewButton, None,)
+
+        self._toolbarDefaultItemIdentifiers = [
+            kServerToolbarItemIdentifier,
+            kBrowserViewToolbarItemIdentifier,
+            kDataViewToolbarItemIdentifier,
+            NSToolbarFlexibleSpaceItemIdentifier,
+            kRefreshToolbarItemIdentifier,
+        ]
+
+        self._toolbarAllowedItemIdentifiers = [
+            kServerToolbarItemIdentifier,
+            kBrowserViewToolbarItemIdentifier,
+            kDataViewToolbarItemIdentifier,
+            kRefreshToolbarItemIdentifier,
+            NSToolbarSeparatorItemIdentifier,
+            NSToolbarSpaceItemIdentifier,
+            NSToolbarFlexibleSpaceItemIdentifier,
+            NSToolbarPrintItemIdentifier,
+            NSToolbarCustomizeToolbarItemIdentifier,
+        ]
+
+    def toolbarDefaultItemIdentifiers_(self, anIdentifier):
+        """
+        Return the default set of toolbar items.
+        """
+        return self._toolbarDefaultItemIdentifiers
+
+    def toolbarAllowedItemIdentifiers_(self, anIdentifier):
+        """
+        Return the allowed set of toolbar items.
+        """
+        return self._toolbarAllowedItemIdentifiers
+
+    def toolbar_itemForItemIdentifier_willBeInsertedIntoToolbar_(self,
+                                                                 toolbar,
+                                                                 itemIdentifier, flag):
+        """
+        Delegate method fired when the toolbar is about to insert an
+        item into the toolbar.  Item is identified by itemIdentifier.
+
+        Effectively makes a copy of the cached reference instance of
+        the toolbar item identified by itemIdentifier.
+        """
+        newItem = NSToolbarItem.alloc().initWithItemIdentifier_(itemIdentifier)
+        item = self._toolbarItems[itemIdentifier]
+
+        newItem.setLabel_( item.label() )
+        newItem.setPaletteLabel_( item.paletteLabel() )
+        if item.view():
+            newItem.setView_( item.view() )
+        else:
+            newItem.setImage_( item.image() )
+
+        newItem.setToolTip_( item.toolTip() )
+        newItem.setTarget_( item.target() )
+        newItem.setAction_( item.action() )
+        newItem.setMenuFormRepresentation_( item.menuFormRepresentation() )
+
+        if newItem.view():
+            newItem.setMinSize_( item.minSize() )
+            newItem.setMaxSize_( item.maxSize() )
+
+        return newItem
+
+    def setBrowserView(self, view):
+        """
+        Change the browser view pane to the specified list type.
+        """
+        newView = {
+            self.BROWSERVIEW_COLUMNS: self.columnView,
+            self.BROWSERVIEW_LIST   : self.listView,
+        }.get(view, None)
+        if self.currentBrowserView != newView:
+            if self.currentBrowserView:
+                self.currentBrowserView.setHidden_(YES)
+            self.currentBrowserView = newView
+            if self.currentBrowserView:
+                self.currentBrowserView.setHidden_(NO)
+            self.browserview = view
+            self.refreshView()
+    
+    def changeBrowserView_(self, sender):
+        """
+        User clicked a browser toolbar button.
+        """
+        self.setBrowserView(sender.selectedSegment())
+
+    def setDataView(self, view):
+        """
+        Change the data view pane to the specified type.
+        """
+        newView = {
+            self.DATAVIEW_PROPERTIES: self.propertiesView,
+            self.DATAVIEW_DATA      : self.dataView,
+        }.get(view, None)
+        if self.currentDataView != newView:
+            if self.currentDataView:
+                self.currentDataView.setHidden_(YES)
+            self.currentDataView = newView
+            if self.currentDataView:
+                self.currentDataView.setHidden_(NO)
+            self.dataview = view
+            self.refreshView()
+    
+    def changeDataView_(self, sender):
+        """
+        User clicked a view toolbar button.
+        """
+        self.setDataView(sender.selectedSegment())
+        
+    def resetServer_(self, sender):
+        """
+        Display the sheet asking for server details.
+        """
+        NSApplication.sharedApplication().beginSheet_modalForWindow_modalDelegate_didEndSelector_contextInfo_(
+            self.startupSheet,
+            self.window,
+            None, None, 0
+        )
+    
+    def refreshData_(self, sender):
+        """
+        Force a refresh of the data for the current selected resource.
+        """
+        if self.selectedResource:
+            self.selectedResource.clear()
+            
+            self.progress.startAnimation_(self)
+            resources = self.selectedResource.listChildren()
+            self.columns[-1] = resources
+            self.progress.stopAnimation_(self)
+    
+            self.refreshView()
+
+    def refreshView(self):
+        """
+        Refresh the actual data view for the selected resource.
+        """
+        if self.selectedResource:
+            self.progress.startAnimation_(self)
+            if self.dataview == self.DATAVIEW_PROPERTIES:
+                self.selectedDetails = self.selectedResource.getAllDetails()
+                self.table.reloadData()
+                self.table.deselectAll_(self)
+                self.text.setString_("")
+            elif self.dataview == self.DATAVIEW_DATA:
+                self.selectedData = self.selectedResource.getDataAsHTML()
+                url = NSURL.alloc().initWithString_(self.serverText.stringValue())
+                self.webView.mainFrame().loadHTMLString_baseURL_(self.selectedData, url)
+            self.progress.stopAnimation_(self)
+
+    def startupOKAction_(self, btn):
+        """
+        User clicked OK in the server setup sheet.
+        """
+        
+        # Create the actual session.
+        server = self.serverText.stringValue()
+        user = self.userText.stringValue()
+        pswd = self.passwordText.stringValue()
+        self.session = Session(server, user, pswd, logging=False)
+        self.window.setTitle_(self.serverText.stringValue())
+        self.pathLabel.setStringValue_("/")
+        NSUserDefaults.standardUserDefaults().setObject_forKey_(server, "LastServer")
+        NSUserDefaults.standardUserDefaults().setObject_forKey_(user, "LastUser")
+        
+        # List the root resource.
+        self.progress.startAnimation_(self)
+        resources = self.session.getRoot().listChildren()
+        self.progress.stopAnimation_(self)
+        self.columns = [resources]
+
+        # Done with the sheet.
+        self.startupSheet.close()
+        NSApplication.sharedApplication().endSheet_(self.startupSheet)
+        
+        # Force reload of browser pane views.
+        self.browser.loadColumnZero()
+        self.list.reloadItem_(None)
+
+    def startupCancelAction_(self, btn):
+        """
+        User clicked the cancel button in the server sheet.
+        """
+        self.startupSheet.close()
+        NSApplication.sharedApplication().endSheet_(self.startupSheet)
+    
+    def browserAction_(self, browser):
+        """
+        Something changed in the column browser.
+        """
+        
+        # Update current path.
+        self.pathLabel.setStringValue_(browser.path())
+
+        # Get new selected resource and refresh the data view.
+        self.selectedResource = None
+        self.selectedDetails = None
+        col = len(self.columns)
+        row = -1
+        while row == -1:
+            col -= 1
+            if col < 0:
+                break
+            row = self.browser.selectedRowInColumn_(col)
+        if row >= 0:
+            self.selectedResource = self.columns[col][row]
+            
+        self.refreshView()
+
+    def browser_willDisplayCell_atRow_column_(self, browser, cell, row, col):
+        """
+        Delegate method to set the actual stuff displayed in a column view row.
+        """
+        isLeaf = not self.columns[col][row].isCollection()
+        cell.setLeaf_(isLeaf)
+        cell.setStringValue_(self.columns[col][row].getName())
+        cell.setImage_(self.fileImage if isLeaf else self.directoryImage)
+
+    def browser_numberOfRowsInColumn_(self, browser, col):
+        """
+        Delegate method that returns the number of rows in a column view column.
+        """
+        if col == 0:
+            return len(self.columns[0])
+        del self.columns[col:]
+        resource = self.columns[col - 1][browser.selectedRowInColumn_(col - 1)]
+        self.progress.startAnimation_(self)
+        resources = resource.listChildren()
+        self.progress.stopAnimation_(self)
+        self.columns.append(resources)
+        return len(resources)
+
+    def outlineViewSelectionDidChange_(self, notification):
+        """
+        Delegate method called when the selection in the outline view changes.
+        """
+        
+        # Get the new selected resource and refresh the data view.
+        row = self.list.selectedRow()
+        if row == -1:
+            self.selectedResource = None
+            self.pathLabel.setStringValue_("Nothing Selected")
+        else:
+            self.selectedResource = unwrap_object(self.list.itemAtRow_(row))
+            self.pathLabel.setStringValue_(self.selectedResource.getPath())
+            
+        self.refreshView()
+
+    def outlineView_numberOfChildrenOfItem_(self, outlineView, item):
+        """
+        Delegate method to return the number of children of an item in the outline view.
+        """
+        if self.session is None:
+            return 0
+        if item is None:
+            resource = self.session.getRoot()
+        else:
+            resource = unwrap_object(item)
+        self.progress.startAnimation_(self)
+        resources = resource.listChildren()
+        self.progress.stopAnimation_(self)
+        return len(resources)
+        
+    def outlineView_isItemExpandable_(self, outlineView, item):
+        """
+        Delegate method to return the whether an item in the outline view is expandable.
+        """
+        if item is None:
+            return YES
+        else:
+            resource = unwrap_object(item)
+            return YES if resource.isCollection() else NO
+
+    def outlineView_child_ofItem_(self, outlineView, index, item):
+        """
+        Delegate method to return the item associated with a row in the outline view.
+        """
+        if item is None:
+            resource = self.session.getRoot()
+        else:
+            resource = unwrap_object(item)
+        self.progress.startAnimation_(self)
+        resources = resource.listChildren()
+        self.progress.stopAnimation_(self)
+        return wrap_object(resources[index])
+
+    def outlineView_objectValueForTableColumn_byItem_(self, outlineView, tableColumn, item):
+        """
+        Delegate method to return the data displayed in the outline view.
+        """
+        if item is None:
+            resource = self.session.getRoot()
+        else:
+            resource = unwrap_object(item)
+        return {
+            "Name"    : resource.getName(),
+            "Size"    : resource.getSize(),
+            "Modified": resource.getLastMod(),
+        }[tableColumn.identifier()]
+
+    def tableAction_(self, table):
+        """
+        Called when the selection in the properties list changes.
+        """
+        
+        # Get the selected property and display its value.
+        row = self.table.selectedRow()
+        if row >= 0:
+            value = self.selectedDetails[row][1]
+            if type(value) in (types.ListType, types.TupleType,):
+                if len(value) == 1:
+                    text = self.propValueToText(value[0])
+                else:
+                    sorted = [self.propValueToText(v) for v in value]
+                    sorted.sort()
+                    text = "\r".join(sorted)
+            else:
+                text = self.propValueToText(value)
+        else:
+            text = ""
+        self.text.setString_(text) 
+
+    def numberOfRowsInTableView_(self, tableView):
+        """
+        Delegate method to return the number of rows in the list.
+        @param tableView:
+        @type tableView:
+        """
+        if self.selectedDetails is None:
+            return 0
+        return len(self.selectedDetails)
+
+    def tableView_objectValueForTableColumn_row_(self, tableView, col, row):
+        """
+        Delegate method to return the text for a list cell.
+        """
+        return str(self.selectedDetails[row][0])
+
+    def tableView_shouldEditTableColumn_row_(self, tableView, col, row):
+        """
+        Delegate method to indicate whether a cell can be edited.
+        """
+        return 0
+
+    def propValueToText(self, value):
+        """
+        Do a sensible print of a property value taking type into account.
+        """
+        if type(value) in (types.StringType, types.UnicodeType, types.IntType):
+            text = str(value)
+        elif isinstance(value, _ElementInterface):
+            text = xmlhelpers.elementToString(value).replace("\n", "")
+        else:
+            text = str(value)
+        return text
+
+if __name__ == "__main__":
+    AppHelper.runEventLoop()

Added: CalDAVClientLibrary/trunk/src/ui/__init__.py
===================================================================
--- CalDAVClientLibrary/trunk/src/ui/__init__.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/ui/__init__.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,15 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##

Added: CalDAVClientLibrary/trunk/src/ui/resource.py
===================================================================
--- CalDAVClientLibrary/trunk/src/ui/resource.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/ui/resource.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,134 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from protocol.url import URL
+from protocol.webdav.definitions import davxml
+import os
+
+class Resource(object):
+    """
+    Maintains data for a WebDAV resource, including list of properties,
+    child resources and actual data.
+    """
+    
+    def __init__(self, session, path, lastmod="", size="", type=""):
+        
+        self.session = session
+        self.path = path.rstrip("/")
+        self.iscollection = path.endswith("/")
+        self.lastmod = lastmod
+        self.size = size
+        self.type = type
+        self.children = None
+        self.details = None
+        self.data = None
+
+    def getPath(self):
+        return self.path
+
+    def getName(self):
+        return os.path.basename(self.path)
+
+    def getLastMod(self):
+        return self.lastmod
+
+    def getSize(self):
+        return self.size
+
+    def getType(self):
+        return self.type
+
+    def isCollection(self):
+        return self.iscollection
+
+    def findPath(self, path=None, elements=None):
+        if path:
+            elements = path.lstrip("/").rstrip("/").split("/")
+        if self.listChildren():
+            for child in self.children:
+                if child.getName() == elements[0]:
+                    elements = elements[1:]
+                    if elements:
+                        return child.findPath(elements=elements)
+                    else:
+                        return child
+        return None
+
+    def findChild(self, name):
+        if self.children:
+            for child in self.children:
+                if child.getName() == name:
+                    return child
+        return None
+
+    def listChildren(self):
+        if self.children is None:
+            resource = URL(url=self.path+"/")
+            props = (
+                davxml.resourcetype,
+                davxml.getlastmodified,
+                davxml.getcontentlength,
+                davxml.getcontenttype,
+            )
+            results = self.session.account.session.getPropertiesOnHierarchy(resource, props)
+            items = results.keys()
+            items.sort()
+            self.children = [Resource(
+                self.session,
+                rurl,
+                lastmod=results[rurl].get(davxml.getlastmodified, ""),
+                size=results[rurl].get(davxml.getcontentlength, ""),
+                type=results[rurl].get(davxml.getcontenttype, ""),
+            ) for rurl in items if rurl != self.path + "/"]
+        return self.children
+
+    def getDetails(self):
+        resource = URL(url=self.path+"/")
+        props = (davxml.resourcetype,)
+        props += (davxml.getcontentlength, davxml.getlastmodified,)
+        props, _ignore_bad = self.session.account.session.getProperties(resource, props)
+        size = props.get(davxml.getcontentlength, "-")
+        if not size:
+            size = "0"
+        modtime = props.get(davxml.getlastmodified, "-")
+        return ["Size: %s" % (size,), "Modtime: %s" % (modtime,)]
+
+    def getAllDetails(self):
+        if self.details is None:
+            resource = URL(url=self.path+"/")
+            props = self.session.account.session.getPropertyNames(resource)
+            results, _ignore_bad = self.session.account.session.getProperties(resource, props)
+            sorted = results.keys()
+            sorted.sort()
+            self.details = [(key, results[key],) for key in sorted]
+        return self.details
+
+    def getData(self):
+        if self.data is None:
+            resource = URL(url=self.path+"/")
+            self.data, _ignore_etag = self.session.account.session.readData(resource)
+        return self.data
+
+    def getDataAsHTML(self):
+        data = self.getData()
+        if not self.type.startswith("text/html"):
+            data = "<HTML><BODY>%s</BODY></HTML>" % (data.replace("\r\n", "\n").replace("\r", "\n").replace("\n", "<br>\n"),)
+        return data
+
+    def clear(self):
+        self.children = None
+        self.details = None
+        self.data = None

Added: CalDAVClientLibrary/trunk/src/ui/session.py
===================================================================
--- CalDAVClientLibrary/trunk/src/ui/session.py	                        (rev 0)
+++ CalDAVClientLibrary/trunk/src/ui/session.py	2008-03-25 18:56:21 UTC (rev 2248)
@@ -0,0 +1,38 @@
+##
+# Copyright (c) 2007-2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from client.account import CalDAVAccount
+from ui.resource import Resource
+
+class Session(object):
+    """
+    Maintains the basic information for a session and the root resource.
+    """
+    
+    def __init__(self, server, user, pswd, logging):
+        
+        self.server = server
+        self.user = user
+        self.pswd = pswd
+            
+        # Create the account
+        ssl = server.startswith("https://")
+        server = server[8:] if ssl else server[7:]
+        paths = "/principals/users/%s/" % (self.user,)
+        self.account = CalDAVAccount(server, ssl=ssl, user=self.user, pswd=self.pswd, root=paths, principal=paths, logging=logging)
+        
+    def getRoot(self):
+        return Resource(self, "/")

-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20080325/0eca7122/attachment-0001.html 


More information about the calendarserver-changes mailing list