Index: trunk/packages/python-afs/.gitignore
===================================================================
--- trunk/packages/python-afs/.gitignore	(revision 2599)
+++ trunk/packages/python-afs/.gitignore	(revision 2599)
@@ -0,0 +1,14 @@
+afs/_util.c
+afs/_util.dep
+afs/_pts.c
+afs/_pts.dep
+afs/_acl.c
+afs/_acl.dep
+afs/_fs.c
+afs/_fs.dep
+
+*.so
+build
+dist
+*.pyc
+PyAFS.egg-info
Index: trunk/packages/python-afs/MANIFEST.in
===================================================================
--- trunk/packages/python-afs/MANIFEST.in	(revision 2599)
+++ trunk/packages/python-afs/MANIFEST.in	(revision 2599)
@@ -0,0 +1,1 @@
+include README.dev
Index: trunk/packages/python-afs/README
===================================================================
--- trunk/packages/python-afs/README	(revision 2599)
+++ trunk/packages/python-afs/README	(revision 2599)
@@ -0,0 +1,39 @@
+================
+Installing PyAFS
+================
+
+To install PyAFS, you will first need to install Cython_. It's always
+a good idea to install Cython through your package manager, if
+possible. Your system's Cython package may be named ``cython`` or
+``cython-py25``. If your package manager doesn't have a package for
+Cython, or if you wish to install Cython by hand anyway, you can do so
+by running::
+
+  $ easy_install Cython
+
+Once you've done that, to install PyAFS globally, run::
+
+  $ python setup.py install
+
+If you want to build PyAFS without installing it globally, you may
+want to run::
+
+  $ python setup.py build_ext --inplace
+
+which will build the C extensions in place next to their source,
+allowing you to import the various modules, so long as your current
+working directory is the root of the PyAFS source tree.
+
+Alternatively, PyAFS has been packaged for Debian and Ubuntu. To build
+the Debian package of the latest release, run::
+
+  $ git checkout debian
+  $ git buildpackage
+  $ sudo debi
+
+You will need the devscripts and git-buildpackage packages installed,
+as well as this package's build dependencies (cdbs, debhelper,
+python-all-dev, python-support, python-pyrex, python-setuptools,
+libopenafs-dev).
+
+.. _Cython: http://cython.org/
Index: trunk/packages/python-afs/README.dev
===================================================================
--- trunk/packages/python-afs/README.dev	(revision 2599)
+++ trunk/packages/python-afs/README.dev	(revision 2599)
@@ -0,0 +1,16 @@
+This project is an attempt to develop a fully Python-based interface
+to the AFS APIs. A lot of the structure is based on the Perl AFS
+module (http://search.cpan.org/~nog/AFS-2.6.1), but this will
+hopefully be a bit more Pythonic in its interface.
+
+The goal is essentially to implement the full set of "high-level" APIs
+provided by the Perl modules in a combination of Python and
+Cython. This will typically involve a fairly simple wrapping of the C
+API in a Cython module (e.g. afs._pts), and then a higher-level
+wrapping of that Cython module in pure Python (e.g. afs.pts).
+
+I am depending on patches from other people, beacuse I will not finish
+this on my own. I think I've established enough of the structure that
+others can see what I want the structure to look like.
+
+Please be sure to sign off on your patches.
Index: trunk/packages/python-afs/afs/_acl.pyx
===================================================================
--- trunk/packages/python-afs/afs/_acl.pyx	(revision 2599)
+++ trunk/packages/python-afs/afs/_acl.pyx	(revision 2599)
@@ -0,0 +1,48 @@
+from afs._util cimport *
+from afs._util import pyafs_error
+
+cdef extern from "afs/prs_fs.h":
+    enum:
+        PRSFS_READ, PRSFS_WRITE, PRSFS_INSERT, PRSFS_LOOKUP,
+        PRSFS_DELETE, PRSFS_LOCK, PRSFS_ADMINISTER,
+        PRSFS_USR0, PRSFS_USR1, PRSFS_USR2, PRSFS_USR2, PRSFS_USR3,
+        PRSFS_USR4, PRSFS_USR5, PRSFS_USR6, PRSFS_USR7
+
+# This is defined in afs/afs.h, but I can't figure how to include the
+# header. Also, venus/fs.c redefines the struct, so why not!
+cdef struct vcxstat2:
+    afs_int32 callerAccess
+    afs_int32 cbExpires
+    afs_int32 anyAccess
+    char mvstat
+
+READ = PRSFS_READ
+WRITE = PRSFS_WRITE
+INSERT = PRSFS_INSERT
+LOOKUP = PRSFS_LOOKUP
+DELETE = PRSFS_DELETE
+LOCK = PRSFS_LOCK
+ADMINISTER = PRSFS_ADMINISTER
+USR0 = PRSFS_USR0
+USR1 = PRSFS_USR1
+USR2 = PRSFS_USR2
+USR3 = PRSFS_USR3
+USR4 = PRSFS_USR4
+USR5 = PRSFS_USR5
+USR6 = PRSFS_USR6
+USR7 = PRSFS_USR7
+
+DEF MAXSIZE = 2048
+
+def getAcl(char* dir, int follow=1):
+    cdef char space[MAXSIZE]
+    pioctl_read(dir, VIOCGETAL, space, MAXSIZE, follow)
+    return space
+
+def getCallerAccess(char *dir, int follow=1):
+    cdef vcxstat2 stat
+    pioctl_read(dir, VIOC_GETVCXSTATUS2, <void*>&stat, sizeof(vcxstat2), follow)
+    return stat.callerAccess
+
+def setAcl(char* dir, char* acl, int follow=1):
+    pioctl_write(dir, VIOCSETAL, acl, follow)
Index: trunk/packages/python-afs/afs/_fs.pyx
===================================================================
--- trunk/packages/python-afs/afs/_fs.pyx	(revision 2599)
+++ trunk/packages/python-afs/afs/_fs.pyx	(revision 2599)
@@ -0,0 +1,9 @@
+from afs._util cimport *
+from afs._util import pyafs_error
+
+def whichcell(char* path):
+    """Determine which AFS cell a particular path is in."""
+    cdef char cell[MAXCELLCHARS]
+
+    pioctl_read(path, VIOC_FILE_CELL_NAME, cell, sizeof(cell), 1)
+    return cell
Index: trunk/packages/python-afs/afs/_pts.pyx
===================================================================
--- trunk/packages/python-afs/afs/_pts.pyx	(revision 2599)
+++ trunk/packages/python-afs/afs/_pts.pyx	(revision 2599)
@@ -0,0 +1,696 @@
+from afs._util cimport *
+from afs._util import pyafs_error
+import re
+
+cdef extern from "afs/ptuser.h":
+    enum:
+        PR_MAXNAMELEN
+        PRGRP
+        PRUSERS
+        PRGROUPS
+        ANONYMOUSID
+        PR_SF_ALLBITS
+        PR_SF_NGROUPS
+        PR_SF_NUSERS
+
+    ctypedef char prname[PR_MAXNAMELEN]
+
+    struct namelist:
+        unsigned int namelist_len
+        prname *namelist_val
+
+    struct prlist:
+        unsigned int prlist_len
+        afs_int32 *prlist_val
+
+    struct idlist:
+        unsigned int idlist_len
+        afs_int32 *idlist_val
+
+    struct prcheckentry:
+        afs_int32 flags
+        afs_int32 id
+        afs_int32 owner
+        afs_int32 creator
+        afs_int32 ngroups
+        afs_int32 nusers
+        afs_int32 count
+        char name[PR_MAXNAMELEN]
+
+    struct prlistentries:
+        afs_int32 flags
+        afs_int32 id
+        afs_int32 owner
+        afs_int32 creator
+        afs_int32 ngroups
+        afs_int32 nusers
+        afs_int32 count
+        char name[PR_MAXNAMELEN]
+
+    struct prentries:
+        unsigned int prentries_len
+        prlistentries *prentries_val
+
+    int ubik_PR_NameToID(ubik_client *, afs_int32, namelist *, idlist *)
+    int ubik_PR_IDToName(ubik_client *, afs_int32, idlist *, namelist *)
+    int ubik_PR_INewEntry(ubik_client *, afs_int32, char *, afs_int32, afs_int32)
+    int ubik_PR_NewEntry(ubik_client *, afs_int32, char *, afs_int32, afs_int32, afs_int32 *)
+    int ubik_PR_Delete(ubik_client *, afs_int32, afs_int32)
+    int ubik_PR_AddToGroup(ubik_client *, afs_int32, afs_int32, afs_int32)
+    int ubik_PR_RemoveFromGroup(ubik_client *, afs_int32, afs_int32, afs_int32)
+    int ubik_PR_ListElements(ubik_client *, afs_int32, afs_int32, prlist *, afs_int32 *)
+    int ubik_PR_ListOwned(ubik_client *, afs_int32, afs_int32, prlist *, afs_int32 *)
+    int ubik_PR_ListEntry(ubik_client *, afs_int32, afs_int32, prcheckentry *)
+    int ubik_PR_ChangeEntry(ubik_client *, afs_int32, afs_int32, char *, afs_int32, afs_int32)
+    int ubik_PR_IsAMemberOf(ubik_client *, afs_int32, afs_int32, afs_int32, afs_int32 *)
+    int ubik_PR_ListMax(ubik_client *, afs_int32, afs_int32 *, afs_int32 *)
+    int ubik_PR_SetMax(ubik_client *, afs_int32, afs_int32, afs_int32)
+    int ubik_PR_ListEntries(ubik_client *, afs_int32, afs_int32, afs_int32, prentries *, afs_int32 *)
+    int ubik_PR_SetFieldsEntry(ubik_client *, afs_int32, afs_int32, afs_int32, afs_int32, afs_int32, afs_int32, afs_int32, afs_int32)
+
+cdef extern from "afs/pterror.h":
+    enum:
+        PRNOENT
+
+cdef extern from "krb5/krb5.h":
+    struct _krb5_context:
+        pass
+    struct krb5_principal_data:
+        pass
+
+    ctypedef _krb5_context * krb5_context
+    ctypedef krb5_principal_data * krb5_principal
+
+    ctypedef long krb5_int32
+    ctypedef krb5_int32 krb5_error_code
+    krb5_error_code krb5_init_context(krb5_context *)
+    krb5_error_code krb5_parse_name(krb5_context, char *, krb5_principal *)
+    krb5_error_code krb5_unparse_name(krb5_context, krb5_principal, char **)
+    krb5_error_code krb5_524_conv_principal(krb5_context, krb5_principal, char *, char *, char *)
+    krb5_error_code krb5_425_conv_principal(krb5_context, char *, char *, char *, krb5_principal *)
+    krb5_error_code krb5_get_host_realm(krb5_context, char *, char ***)
+    void krb5_free_host_realm(krb5_context, char **)
+    void krb5_free_principal(krb5_context, krb5_principal)
+    void krb5_free_context(krb5_context)
+
+cdef class PTEntry:
+    cdef public afs_int32 flags
+    cdef public afs_int32 id
+    cdef public afs_int32 owner
+    cdef public afs_int32 creator
+    cdef public afs_int32 ngroups
+    cdef public afs_int32 nusers
+    cdef public afs_int32 count
+    cdef public object name
+
+    def __repr__(self):
+        if self.name != '':
+            return '<PTEntry: %s>' % self.name
+        else:
+            return '<PTEntry: PTS ID %s>' % self.id
+
+cdef int _ptentry_from_c(PTEntry p_entry, prcheckentry * c_entry) except -1:
+    if p_entry is None:
+        raise TypeError
+        return -1
+
+    p_entry.flags = c_entry.flags
+    p_entry.id = c_entry.id
+    p_entry.owner = c_entry.owner
+    p_entry.creator = c_entry.creator
+    p_entry.ngroups = c_entry.ngroups
+    p_entry.nusers = c_entry.nusers
+    p_entry.count = c_entry.count
+    p_entry.name = c_entry.name
+    return 0
+
+cdef int _ptentry_to_c(prcheckentry * c_entry, PTEntry p_entry) except -1:
+    if p_entry is None:
+        raise TypeError
+        return -1
+
+    c_entry.flags = p_entry.flags
+    c_entry.id = p_entry.id
+    c_entry.owner = p_entry.owner
+    c_entry.creator = p_entry.creator
+    c_entry.ngroups = p_entry.ngroups
+    c_entry.nusers = p_entry.nusers
+    c_entry.count = p_entry.count
+    strncpy(c_entry.name, p_entry.name, sizeof(c_entry.name))
+    return 0
+
+cdef object kname_re = re.compile(r'^([^.].*?)(?<!\\)(?:\.(.*?))?(?<!\\)@([^@]*)$')
+
+cdef object kname_parse(fullname):
+    """Parse a krb4-style principal into a name, instance, and realm."""
+    cdef object re_match = kname_re.match(fullname)
+    if not re_match:
+        return None
+    else:
+        princ = re_match.groups()
+        return tuple([re.sub(r'\\(.)', r'\1', x) if x else x for x in princ])
+
+cdef object kname_unparse(name, inst, realm):
+    """Unparse a name, instance, and realm into a single krb4
+    principal string."""
+    name = re.sub('r([.\\@])', r'\\\1', name)
+    inst = re.sub('r([.\\@])', r'\\\1', inst)
+    realm = re.sub(r'([\\@])', r'\\\1', realm)
+    if inst:
+        return '%s.%s@%s' % (name, inst, realm)
+    else:
+        return '%s@%s' % (name, realm)
+
+cdef class PTS:
+    """
+    A PTS object is essentially a handle to talk to the server in a
+    given cell.
+
+    cell defaults to None. If no argument is passed for cell, PTS
+    connects to the home cell.
+
+    sec is the security level, an integer from 0 to 3:
+      - 0: unauthenticated connection
+      - 1: try authenticated, then fall back to unauthenticated
+      - 2: fail if an authenticated connection can't be established
+      - 3: same as 2, plus encrypt all traffic to the protection
+        server
+
+    The realm attribute is the Kerberos realm against which this cell
+    authenticates.
+    """
+    cdef ubik_client * client
+    cdef readonly object cell
+    cdef readonly object realm
+
+    def __cinit__(self, cell=None, sec=1):
+        cdef afs_int32 code
+        cdef afsconf_dir *cdir
+        cdef afsconf_cell info
+        cdef krb5_context context
+        cdef char ** hrealms = NULL
+        cdef char * c_cell
+        cdef ktc_principal prin
+        cdef ktc_token token
+        cdef rx_securityClass *sc
+        cdef rx_connection *serverconns[MAXSERVERS]
+        cdef int i
+
+        initialize_PT_error_table()
+
+        if cell is None:
+            c_cell = NULL
+        else:
+            c_cell = cell
+
+        self.client = NULL
+
+        code = rx_Init(0)
+        if code != 0:
+            raise Exception(code, "Error initializing Rx")
+
+        cdir = afsconf_Open(AFSDIR_CLIENT_ETC_DIRPATH)
+        if cdir is NULL:
+            raise OSError(errno,
+                          "Error opening configuration directory (%s): %s" % \
+                              (AFSDIR_CLIENT_ETC_DIRPATH, strerror(errno)))
+        code = afsconf_GetCellInfo(cdir, c_cell, "afsprot", &info)
+        pyafs_error(code)
+
+        code = krb5_init_context(&context)
+        pyafs_error(code)
+        code = krb5_get_host_realm(context, info.hostName[0], &hrealms)
+        pyafs_error(code)
+        self.realm = hrealms[0]
+        krb5_free_host_realm(context, hrealms)
+        krb5_free_context(context)
+
+        self.cell = info.name
+
+        if sec > 0:
+            strncpy(prin.cell, info.name, sizeof(prin.cell))
+            prin.instance[0] = 0
+            strncpy(prin.name, "afs", sizeof(prin.name))
+
+            code = ktc_GetToken(&prin, &token, sizeof(token), NULL);
+            if code != 0:
+                if sec >= 2:
+                    # No really - we wanted authentication
+                    pyafs_error(code)
+                sec = 0
+            else:
+                if sec == 3:
+                    level = rxkad_crypt
+                else:
+                    level = rxkad_clear
+                sc = rxkad_NewClientSecurityObject(level, &token.sessionKey,
+                                                   token.kvno, token.ticketLen,
+                                                   token.ticket)
+
+        if sec == 0:
+            sc = rxnull_NewClientSecurityObject()
+        else:
+            sec = 2
+
+        memset(serverconns, 0, sizeof(serverconns))
+        for 0 <= i < info.numServers:
+            serverconns[i] = rx_NewConnection(info.hostAddr[i].sin_addr.s_addr,
+                                              info.hostAddr[i].sin_port,
+                                              PRSRV,
+                                              sc,
+                                              sec)
+
+        code = ubik_ClientInit(serverconns, &self.client)
+        pyafs_error(code)
+
+        code = rxs_Release(sc)
+
+    def __dealloc__(self):
+        ubik_ClientDestroy(self.client)
+        rx_Finalize()
+
+    def _NameOrId(self, ident):
+        """
+        Given an identifier, convert it to a PTS ID by looking up the
+        name if it's a string, or otherwise just converting it to an
+        integer.
+        """
+        if isinstance(ident, basestring):
+            return self._NameToId(ident)
+        else:
+            return int(ident)
+
+    def _NameToId(self, name):
+        """
+        Converts a user or group to an AFS ID.
+        """
+        cdef namelist lnames
+        cdef idlist lids
+        cdef afs_int32 code, id = ANONYMOUSID
+        name = name.lower()
+
+        lids.idlist_len = 0
+        lids.idlist_val = NULL
+        lnames.namelist_len = 1
+        lnames.namelist_val = <prname *>malloc(PR_MAXNAMELEN)
+        strncpy(lnames.namelist_val[0], name, PR_MAXNAMELEN)
+        code = ubik_PR_NameToID(self.client, 0, &lnames, &lids)
+        if lids.idlist_val is not NULL:
+            id = lids.idlist_val[0]
+            free(lids.idlist_val)
+        if id == ANONYMOUSID:
+            code = PRNOENT
+        pyafs_error(code)
+        return id
+
+    def _IdToName(self, id):
+        """
+        Convert an AFS ID to the name of a user or group.
+        """
+        cdef namelist lnames
+        cdef idlist lids
+        cdef afs_int32 code
+        cdef char name[PR_MAXNAMELEN]
+
+        lids.idlist_len = 1
+        lids.idlist_val = <afs_int32 *>malloc(sizeof(afs_int32))
+        lids.idlist_val[0] = id
+        lnames.namelist_len = 0
+        lnames.namelist_val = NULL
+        code = ubik_PR_IDToName(self.client, 0, &lids, &lnames)
+        if lnames.namelist_val is not NULL:
+            strncpy(name, lnames.namelist_val[0], sizeof(name))
+            free(lnames.namelist_val)
+        if lids.idlist_val is not NULL:
+            free(lids.idlist_val)
+        if name == str(id):
+            code = PRNOENT
+        pyafs_error(code)
+        return name
+
+    def _CreateUser(self, name, id=None):
+        """
+        Create a new user in the protection database. If an ID is
+        provided, that one will be used.
+        """
+        cdef afs_int32 code
+        cdef afs_int32 cid
+        name = name[:PR_MAXNAMELEN].lower()
+
+        if id is not None:
+            cid = id
+
+        if id is not None:
+            code = ubik_PR_INewEntry(self.client, 0, name, cid, 0)
+        else:
+            code = ubik_PR_NewEntry(self.client, 0, name, 0, 0, &cid)
+
+        pyafs_error(code)
+        return cid
+
+    def _CreateGroup(self, name, owner, id=None):
+        """
+        Create a new group in the protection database. If an ID is
+        provided, that one will be used.
+        """
+        cdef afs_int32 code, cid
+
+        name = name[:PR_MAXNAMELEN].lower()
+        oid = self._NameOrId(owner)
+
+        if id is not None:
+            cid = id
+            code = ubik_PR_INewEntry(self.client, 0, name, cid, oid)
+        else:
+            code = ubik_PR_NewEntry(self.client, 0, name, PRGRP, oid, &cid)
+
+        pyafs_error(code)
+        return cid
+
+    def _Delete(self, ident):
+        """
+        Delete the protection database entry with the provided
+        identifier.
+        """
+        cdef afs_int32 code
+        cdef afs_int32 id = self._NameOrId(ident)
+
+        code = ubik_PR_Delete(self.client, 0, id)
+        pyafs_error(code)
+
+    def _AddToGroup(self, user, group):
+        """
+        Add the given user to the given group.
+        """
+        cdef afs_int32 code
+        cdef afs_int32 uid = self._NameOrId(user), gid = self._NameOrId(group)
+
+        code = ubik_PR_AddToGroup(self.client, 0, uid, gid)
+        pyafs_error(code)
+
+    def _RemoveFromGroup(self, user, group):
+        """
+        Remove the given user from the given group.
+        """
+        cdef afs_int32 code
+        cdef afs_int32 uid = self._NameOrId(user), gid = self._NameOrId(group)
+
+        code = ubik_PR_RemoveFromGroup(self.client, 0, uid, gid)
+        pyafs_error(code)
+
+    def _ListMembers(self, ident):
+        """
+        Get the membership of an entity.
+
+        If id is a group, this returns the users that are in that
+        group.
+
+        If id is a user, this returns the list of groups that user is
+        on.
+
+        This returns a list of PTS IDs.
+        """
+        cdef afs_int32 code, over
+        cdef prlist alist
+        cdef int i
+        cdef object members = []
+
+        cdef afs_int32 id = self._NameOrId(ident)
+
+        alist.prlist_len = 0
+        alist.prlist_val = NULL
+
+        code = ubik_PR_ListElements(self.client, 0, id, &alist, &over)
+
+        if alist.prlist_val is not NULL:
+            for i in range(alist.prlist_len):
+                members.append(alist.prlist_val[i])
+            free(alist.prlist_val)
+
+        pyafs_error(code)
+
+        return members
+
+    def _ListOwned(self, owner):
+        """
+        Get all groups owned by an entity.
+        """
+        cdef afs_int32 code, over
+        cdef prlist alist
+        cdef int i
+        cdef object owned = []
+
+        cdef afs_int32 oid = self._NameOrId(owner)
+
+        alist.prlist_len = 0
+        alist.prlist_val = NULL
+
+        code = ubik_PR_ListOwned(self.client, 0, oid, &alist, &over)
+
+        if alist.prlist_val is not NULL:
+            for i in range(alist.prlist_len):
+                owned.append(alist.prlist_val[i])
+            free(alist.prlist_val)
+
+        pyafs_error(code)
+
+        return owned
+
+    def _ListEntry(self, ident):
+        """
+        Load a PTEntry instance with information about the provided
+        entity.
+        """
+        cdef afs_int32 code
+        cdef prcheckentry centry
+        cdef object entry = PTEntry()
+
+        cdef afs_int32 id = self._NameOrId(ident)
+
+        code = ubik_PR_ListEntry(self.client, 0, id, &centry)
+        pyafs_error(code)
+
+        _ptentry_from_c(entry, &centry)
+        return entry
+
+    def _ChangeEntry(self, ident, newname=None, newid=None, newoid=None):
+        """
+        Change the name, ID, and/or owner of a PTS entity.
+
+        For any of newname, newid, and newoid which aren't specified
+        or ar None, the value isn't changed.
+        """
+        cdef afs_int32 code
+        cdef afs_int32 c_newid = 0, c_newoid = 0
+        cdef char * c_newname
+
+        cdef afs_int32 id = self._NameOrId(ident)
+
+        if newname is None:
+            newname = self._IdToName(id)
+        c_newname = newname
+        if newid is not None:
+            c_newid = newid
+        if newoid is not None:
+            c_newoid = newoid
+
+        code = ubik_PR_ChangeEntry(self.client, 0, id, c_newname, c_newoid, c_newid)
+        pyafs_error(code)
+
+    def _IsAMemberOf(self, user, group):
+        """
+        Return True if the given user is a member of the given group.
+        """
+        cdef afs_int32 code
+        cdef afs_int32 flag
+
+        cdef afs_int32 uid = self._NameOrId(user), gid = self._NameOrId(group)
+
+        code = ubik_PR_IsAMemberOf(self.client, 0, uid, gid, &flag)
+        pyafs_error(code)
+
+        return bool(flag)
+
+    def _ListMax(self):
+        """
+        Return a tuple of the maximum user ID and the maximum group
+        ID currently assigned.
+        """
+        cdef afs_int32 code, uid, gid
+
+        code = ubik_PR_ListMax(self.client, 0, &uid, &gid)
+        pyafs_error(code)
+
+        return (uid, gid)
+
+    def _SetMaxUserId(self, id):
+        """
+        Set the maximum currently assigned user ID (the next
+        automatically assigned UID will be id + 1)
+        """
+        cdef afs_int32 code
+
+        code = ubik_PR_SetMax(self.client, 0, id, 0)
+        pyafs_error(code)
+
+    def _SetMaxGroupId(self, id):
+        """
+        Set the maximum currently assigned user ID (the next
+        automatically assigned UID will be id + 1)
+        """
+        cdef afs_int32 code
+
+        code = ubik_PR_SetMax(self.client, 0, id, PRGRP)
+        pyafs_error(code)
+
+    def _ListEntries(self, users=None, groups=None):
+        """
+        Return a list of PTEntry instances representing all entries in
+        the PRDB.
+
+        Returns just users by default, but can return just users, just
+        groups, or both.
+        """
+        cdef afs_int32 code
+        cdef afs_int32 flag = 0, startindex = 0, nentries, nextstartindex
+        cdef prentries centries
+        cdef unsigned int i
+
+        cdef object entries = []
+
+        if groups is None or users is True:
+            flag |= PRUSERS
+        if groups:
+            flag |= PRGROUPS
+
+        while startindex != -1:
+            centries.prentries_val = NULL
+            centries.prentries_len = 0
+            nextstartindex = -1
+
+            code = ubik_PR_ListEntries(self.client, 0, flag, startindex, &centries, &nextstartindex)
+            if centries.prentries_val is not NULL:
+                for i in range(centries.prentries_len):
+                    e = PTEntry()
+                    _ptentry_from_c(e, <prcheckentry *>&centries.prentries_val[i])
+                    entries.append(e)
+                free(centries.prentries_val)
+            pyafs_error(code)
+
+            startindex = nextstartindex
+
+        return entries
+
+    def _SetFields(self, ident, access=None, groups=None, users=None):
+        """
+        Update the fields for an entry.
+
+        Valid fields are the privacy flags (access), the group quota
+        (groups), or the "foreign user quota" (users), which doesn't
+        actually seem to do anything, but is included for
+        completeness.
+        """
+        cdef afs_int32 code
+        cdef afs_int32 mask = 0, flags = 0, nusers = 0, ngroups = 0
+
+        cdef afs_int32 id = self._NameOrId(ident)
+
+        if access is not None:
+            flags = access
+            mask |= PR_SF_ALLBITS
+        if groups is not None:
+            ngroups = groups
+            mask |= PR_SF_NGROUPS
+        if users is not None:
+            nusers = users
+            mask |= PR_SF_NGROUPS
+
+        code = ubik_PR_SetFieldsEntry(self.client, 0, id, mask, flags, ngroups, nusers, 0, 0)
+        pyafs_error(code)
+
+    def _AfsToKrb5(self, afs_name):
+        """Convert an AFS principal to a Kerberos v5 one."""
+        cdef krb5_context ctx = NULL
+        cdef krb5_principal princ = NULL
+        cdef krb5_error_code code = 0
+        cdef char * krb5_princ = NULL
+        cdef char *name = NULL, *inst = NULL, *realm = NULL
+        cdef object pname, pinst, prealm
+
+        if '@' in afs_name:
+            pname, prealm = afs_name.rsplit('@', 1)
+            prealm = prealm.upper()
+            krb4_name = '%s@%s' % (pname, prealm)
+        else:
+            krb4_name = '%s@%s' % (afs_name, self.realm)
+
+        pname, pinst, prealm = kname_parse(krb4_name)
+        if pname:
+            name = pname
+        if pinst:
+            inst = pinst
+        if prealm:
+            realm = prealm
+
+        code = krb5_init_context(&ctx)
+        try:
+            pyafs_error(code)
+
+            code = krb5_425_conv_principal(ctx, name, inst, realm, &princ)
+            try:
+                pyafs_error(code)
+
+                code = krb5_unparse_name(ctx, princ, &krb5_princ)
+                try:
+                    pyafs_error(code)
+
+                    return krb5_princ
+                finally:
+                    if krb5_princ is not NULL:
+                        free(krb5_princ)
+            finally:
+                if princ is not NULL:
+                    krb5_free_principal(ctx, princ)
+        finally:
+            if ctx is not NULL:
+                krb5_free_context(ctx)
+
+    def _Krb5ToAfs(self, krb5_name):
+        """Convert a Kerberos v5 principal to an AFS one."""
+        cdef krb5_context ctx = NULL
+        cdef krb5_principal k5_princ = NULL
+        cdef char *k4_name, *k4_inst, *k4_realm
+        cdef object afs_princ
+        cdef object afs_name, afs_realm
+
+        k4_name = <char *>malloc(40)
+        k4_name[0] = '\0'
+        k4_inst = <char *>malloc(40)
+        k4_inst[0] = '\0'
+        k4_realm = <char *>malloc(40)
+        k4_realm[0] = '\0'
+
+        code = krb5_init_context(&ctx)
+        try:
+            pyafs_error(code)
+
+            code = krb5_parse_name(ctx, krb5_name, &k5_princ)
+            try:
+                pyafs_error(code)
+
+                code = krb5_524_conv_principal(ctx, k5_princ, k4_name, k4_inst, k4_realm)
+                pyafs_error(code)
+
+                afs_princ = kname_unparse(k4_name, k4_inst, k4_realm)
+                afs_name, afs_realm = afs_princ.rsplit('@', 1)
+
+                if k4_realm == self.realm:
+                    return afs_name
+                else:
+                    return '%s@%s' % (afs_name, afs_realm.lower())
+            finally:
+                if k5_princ is not NULL:
+                    krb5_free_principal(ctx, k5_princ)
+        finally:
+            if ctx is not NULL:
+                krb5_free_context(ctx)
Index: trunk/packages/python-afs/afs/_util.pxd
===================================================================
--- trunk/packages/python-afs/afs/_util.pxd	(revision 2599)
+++ trunk/packages/python-afs/afs/_util.pxd	(revision 2599)
@@ -0,0 +1,167 @@
+cdef extern from *:
+    ctypedef long size_t
+
+cdef extern from "errno.h":
+    int errno
+
+cdef extern from "string.h":
+    char * strerror(int errnum)
+    char * strncpy(char *s1, char *s2, size_t n)
+    void * memset(void *b, int c, size_t n)
+    void * memcpy(void *s1, void *s2, size_t n)
+    size_t strlen(char *s)
+
+cdef extern from "stdlib.h":
+     void * malloc(size_t size)
+     void free(void *)
+
+cdef extern from "netinet/in.h":
+    struct in_addr:
+        int s_addr
+    struct sockaddr_in:
+        short sin_family
+        unsigned short sin_port
+        in_addr sin_addr
+        char sin_zero[8]
+
+cdef extern from "afs/stds.h":
+    ctypedef unsigned long afs_uint32
+    ctypedef long afs_int32
+
+cdef extern from "afs/dirpath.h":
+    char * AFSDIR_CLIENT_ETC_DIRPATH
+
+cdef extern from "afs/cellconfig.h":
+    enum:
+        MAXCELLCHARS
+        MAXHOSTSPERCELL
+        MAXHOSTCHARS
+
+    # We just pass afsconf_dir structs around to other AFS functions,
+    # so this can be treated as opaque
+    struct afsconf_dir:
+        pass
+
+    # For afsconf_cell, on the other hand, we care about everything
+    struct afsconf_cell:
+        char name[MAXCELLCHARS]
+        short numServers
+        short flags
+        sockaddr_in hostAddr[MAXHOSTSPERCELL]
+        char hostName[MAXHOSTSPERCELL][MAXHOSTCHARS]
+        char *linkedCell
+        int timeout
+
+    afsconf_dir *afsconf_Open(char *adir)
+    int afsconf_GetCellInfo(afsconf_dir *adir,
+                            char *acellName,
+                            char *aservice,
+                            afsconf_cell *acellInfo)
+
+cdef extern from "rx/rxkad.h":
+    ctypedef char rxkad_level
+
+    enum:
+        MAXKTCNAMELEN
+        MAXKTCREALMLEN
+
+    enum:
+        rxkad_clear
+        rxkad_crypt
+
+    struct ktc_encryptionKey:
+        pass
+
+    struct ktc_principal:
+        char name[MAXKTCNAMELEN]
+        char instance[MAXKTCNAMELEN]
+        char cell[MAXKTCREALMLEN]
+
+    struct rx_securityClass:
+        pass
+
+    rx_securityClass *rxkad_NewClientSecurityObject(rxkad_level level,
+                                                    ktc_encryptionKey *sessionKey,
+                                                    afs_int32 kvno,
+                                                    int ticketLen,
+                                                    char *ticket)
+    rx_securityClass *rxnull_NewClientSecurityObject()
+
+    int rxs_Release(rx_securityClass *aobj)
+
+cdef extern from "rx/rx.h":
+    int rx_Init(int port)
+    void rx_Finalize()
+
+    struct rx_connection:
+        pass
+
+    rx_connection *rx_NewConnection(afs_uint32 shost,
+                                    unsigned short sport,
+                                    unsigned short sservice,
+                                    rx_securityClass *securityObject,
+                                    int serviceSecurityIndex)
+
+cdef extern from "afs/auth.h":
+    enum:
+        MAXKTCTICKETLEN
+
+    struct ktc_token:
+        ktc_encryptionKey sessionKey
+        short kvno
+        int ticketLen
+        char ticket[MAXKTCTICKETLEN]
+
+    int ktc_GetToken(ktc_principal *server,
+                     ktc_token *token,
+                     int tokenLen,
+                     ktc_principal *client)
+
+cdef extern from "afs/prclient.h":
+    enum:
+        PRSRV
+
+cdef extern from "ubik.h":
+    enum:
+        MAXSERVERS
+
+    # ubik_client is an opaque struct, so we don't care about its members
+    struct ubik_client:
+        pass
+
+    int ubik_ClientInit(rx_connection **serverconns,
+                        ubik_client **aclient)
+    afs_int32 ubik_ClientDestroy(ubik_client *aclient)
+
+cdef extern from "afs/com_err.h":
+    char * afs_error_message(int)
+
+# All AFS error tables
+cdef extern from "afs/auth.h":
+    void initialize_KTC_error_table()
+cdef extern from "afs/cellconfig.h":
+    void initialize_ACFG_error_table()
+cdef extern from "afs/pterror.h":
+    void initialize_PT_error_table()
+cdef extern from "rx/rxkad.h":
+    void initialize_RXK_error_table()
+cdef extern from "ubik.h":
+    void initialize_U_error_table()
+
+cdef extern from "afs/vice.h":
+    struct ViceIoctl:
+        void *cin "in"
+        void *out
+        unsigned short out_size
+        unsigned short in_size
+
+cdef extern from "afs/venus.h":
+    enum:
+        # PIOCTLS to Venus that we use
+        VIOCGETAL, VIOC_GETVCXSTATUS2, VIOCSETAL, VIOC_FILE_CELL_NAME
+
+# pioctl doesn't actually have a header, so we have to define it here
+cdef extern int pioctl(char *, afs_int32, ViceIoctl *, afs_int32)
+cdef int pioctl_read(char *, afs_int32, void *, unsigned short, afs_int32) except -1
+cdef int pioctl_write(char *, afs_int32, char *, afs_int32) except -1
+
Index: trunk/packages/python-afs/afs/_util.pyx
===================================================================
--- trunk/packages/python-afs/afs/_util.pyx	(revision 2599)
+++ trunk/packages/python-afs/afs/_util.pyx	(revision 2599)
@@ -0,0 +1,67 @@
+"""
+General PyAFS utilities, such as error handling
+"""
+
+import sys
+
+# otherwise certain headers are unhappy
+cdef extern from "netinet/in.h": pass
+cdef extern from "afs/vice.h": pass
+
+cdef int _init = 0
+
+# pioctl convenience wrappers
+
+cdef extern int pioctl_read(char *dir, afs_int32 op, void *buffer, unsigned short size, afs_int32 follow) except -1:
+    cdef ViceIoctl blob
+    cdef afs_int32 code
+    blob.in_size  = 0
+    blob.out_size = size
+    blob.out = buffer
+    code = pioctl(dir, op, &blob, follow)
+    # This might work with the rest of OpenAFS, but I'm not convinced
+    # the rest of it is consistent
+    if code == -1:
+        raise OSError(errno, strerror(errno))
+    pyafs_error(code)
+    return code
+
+cdef extern int pioctl_write(char *dir, afs_int32 op, char *buffer, afs_int32 follow) except -1:
+    cdef ViceIoctl blob
+    cdef afs_int32 code
+    blob.cin = buffer
+    blob.in_size = 1 + strlen(buffer)
+    blob.out_size = 0
+    code = pioctl(dir, op, &blob, follow)
+    # This might work with the rest of OpenAFS, but I'm not convinced
+    # the rest of it is consistent
+    if code == -1:
+        raise OSError(errno, strerror(errno))
+    pyafs_error(code)
+    return code
+
+# Error handling
+
+class AFSException(Exception):
+    def __init__(self, errno):
+        self.errno = errno
+        self.strerror = afs_error_message(errno)
+
+    def __repr__(self):
+        return "AFSException(%s)" % (self.errno)
+
+    def __str__(self):
+        return "[%s] %s" % (self.errno, self.strerror)
+
+def pyafs_error(code):
+    if not _init:
+        initialize_ACFG_error_table()
+        initialize_KTC_error_table()
+        initialize_PT_error_table()
+        initialize_RXK_error_table()
+        initialize_U_error_table()
+
+        _init = 1
+
+    if code != 0:
+        raise AFSException(code)
Index: trunk/packages/python-afs/afs/acl.py
===================================================================
--- trunk/packages/python-afs/afs/acl.py	(revision 2599)
+++ trunk/packages/python-afs/afs/acl.py	(revision 2599)
@@ -0,0 +1,130 @@
+from afs import _acl
+from afs._acl import READ, WRITE, INSERT, LOOKUP, DELETE, LOCK, ADMINISTER, \
+    USR0, USR1, USR2, USR3, USR4, USR5, USR6, USR7
+from afs._acl import getCallerAccess
+
+_canonical = {
+    "read": "rl",
+    "write": "rlidwk",
+    "all": "rlidwka",
+    "mail": "lik",
+    "none": "",
+}
+
+_reverseCanonical = dict((y, x) for (x, y) in _canonical.iteritems())
+
+_charBitAssoc = [
+    ('r', READ),
+    ('l', LOOKUP),
+    ('i', INSERT),
+    ('d', DELETE),
+    ('w', WRITE),
+    ('k', LOCK),
+    ('a', ADMINISTER),
+    ('A', USR0),
+    ('B', USR1),
+    ('C', USR2),
+    ('D', USR3),
+    ('E', USR4),
+    ('F', USR5),
+    ('G', USR6),
+    ('H', USR7),
+]
+
+_char2bit = dict(_charBitAssoc)
+
+
+def rightsToEnglish(s):
+    """Turns a rlwidwka string into a canonical name if possible"""
+    if s in _reverseCanonical:
+        return _reverseCanonical[s]
+    else:
+        return ''
+
+def readRights(s):
+    """Canonicalizes string rights to bitmask"""
+    if s in _canonical: s = _canonical[s]
+    return _parseRights(s)
+
+def showRights(r):
+    """Takes a bitmask and returns a rwlidka string"""
+    s = ""
+    for char,mask in _charBitAssoc:
+        if r & mask == mask: s += char
+    return s
+
+def _parseRights(s):
+    """Parses a rwlid... rights tring to bitmask"""
+    r = 0
+    try:
+        for c in s:
+            r = r | _char2bit[c]
+    except KeyError:
+        raise ValueError
+    return r
+
+def _parseAcl(inp):
+    lines = inp.split("\n")
+    npos = int(lines[0].split(" ")[0])
+    pos = {}
+    neg = {}
+    for l in lines[2:]:
+        if l == "": continue
+        name, acl = l.split()
+        if npos:
+            npos -= 1
+            pos[name] = int(acl)
+        else:
+            # negative acl
+            neg[name] = int(acl)
+    return (pos, neg)
+
+def _unparseAcl(pos, neg):
+    npos = len(pos)
+    nneg = len(neg)
+    acl = "%d\n%d\n" % (npos, nneg)
+    for p in pos.items():
+        acl += "%s\t%d\n" % p
+    for n in neg.items():
+        acl += "%s\t%d\n" % n
+    return acl
+
+class ACL(object):
+    def __init__(self, pos, neg):
+        """
+        ``pos``
+            Dictionary of usernames to positive ACL bitmasks
+        ``neg``
+            Dictionary of usernames to negative ACL bitmasks
+        """
+        self.pos = pos
+        self.neg = neg
+    @staticmethod
+    def retrieve(dir, follow=True):
+        """Retrieve the ACL for an AFS directory"""
+        pos, neg = _parseAcl(_acl.getAcl(dir, follow))
+        return ACL(pos, neg)
+    def apply(self, dir, follow=True):
+        """Apply the ACL to a directory"""
+        self._clean()
+        _acl.setAcl(dir, _unparseAcl(self.pos, self.neg), follow)
+    def _clean(self):
+        """Clean an ACL by removing any entries whose bitmask is 0"""
+        for n,a in self.pos.items():
+            if a == 0:
+                del self.pos[n]
+        for n,a in self.neg.items():
+            if a == 0:
+                del self.neg[n]
+    def set(self, user, bitmask, negative=False):
+        """Set the bitmask for a given user"""
+        if bitmask < 0 or bitmask > max(_char2bit.values()):
+            raise ValueError, "Invalid bitmask"
+        if negative:
+            self.neg[user] = bitmask
+        else:
+            self.pos[user] = bitmask
+    def remove(self, user, negative=False):
+        """Convenience function to removeSet the bitmask for a given user"""
+        self.set(user, 0, negative)
+        
Index: trunk/packages/python-afs/afs/fs.py
===================================================================
--- trunk/packages/python-afs/afs/fs.py	(revision 2599)
+++ trunk/packages/python-afs/afs/fs.py	(revision 2599)
@@ -0,0 +1,13 @@
+import errno
+from afs import _fs
+from afs._fs import whichcell
+
+def inafs(path):
+    """Return True if a path is in AFS."""
+    try:
+        whichcell(path)
+    except OSError, e:
+        if e.errno in (errno.EINVAL, errno.ENOENT):
+            return False
+
+    return True
Index: trunk/packages/python-afs/afs/pts.py
===================================================================
--- trunk/packages/python-afs/afs/pts.py	(revision 2599)
+++ trunk/packages/python-afs/afs/pts.py	(revision 2599)
@@ -0,0 +1,417 @@
+import collections
+from afs import _pts
+
+try:
+    SetMixin = collections.MutableSet
+except AttributeError:
+    SetMixin = object
+
+class PTRelationSet(SetMixin):
+    """Collection class for the groups/members of a PTEntry.
+
+    This class, which acts like a set, is actually a view of the
+    groups or members associated with a PTS Entry. Changes to this
+    class are immediately reflected to the PRDB.
+
+    Attributes:
+        _ent: The PTEntry whose groups/members this instance
+            represents
+        _set: If defined, the set of either groups or members for this
+            instance's PTEntry
+    """
+    def __init__(self, ent):
+        """Initialize a PTRelationSet class.
+
+        Args:
+            ent: The PTEntry this instance should be associated with.
+        """
+        super(PTRelationSet, self).__init__()
+
+        self._ent = ent
+
+    def _loadSet(self):
+        """Load the membership/groups for this instance's PTEntry.
+
+        If they have not previously been loaded, this method updates
+        self._set with the set of PTEntries that are either members of
+        this group, or the groups that this entry is a member of.
+        """
+        if not hasattr(self, '_set'):
+            self._set = set(self._ent._pts.getEntry(m) for m in
+                            self._ent._pts._ListMembers(self._ent.id))
+
+    def _add(self, elt):
+        """Add a new PTEntry to this instance's internal representation.
+
+        This method adds a new entry to this instance's set of
+        members/groups, but unlike PTRelationSet.add, it doesn't add
+        itself to the other instance's set.
+
+        Args:
+            elt: The element to add.
+        """
+        if hasattr(self, '_set'):
+            self._set.add(self._ent._pts.getEntry(elt))
+
+    def _discard(self, elt):
+        """Remove a PTEntry to this instance's internal representation.
+
+        This method removes an entry from this instance's set of
+        members/groups, but unlike PTRelationSet.discard, it doesn't
+        remove itself from the other instance's set.
+
+        Args:
+            elt: The element to discard.
+        """
+        if hasattr(self, '_set'):
+            self._set.discard(self._ent._pts.getEntry(elt))
+
+    def __len__(self):
+        """Count the members/groups in this set.
+
+        Returns:
+            The number of entities in this instance.
+        """
+        self._loadSet()
+        return len(self._set)
+
+    def __iter__(self):
+        """Iterate over members/groups in this set
+
+        Returns:
+            An iterator that loops over the members/groups of this
+                set.
+        """
+        self._loadSet()
+        return iter(self._set)
+
+    def __contains__(self, name):
+        """Test if a PTEntry is connected to this instance.
+
+        If the membership of the group hasn't already been loaded,
+        this method takes advantage of the IsAMemberOf lookup to test
+        for membership.
+
+        This has the convenient advantage of working even when the
+        user doens't have permission to enumerate the group's
+        membership.
+
+        Args:
+            name: The element whose membership is being tested.
+
+        Returns:
+            True, if name is a member of self (or if self is a member
+                of name); otherwise, False
+        """
+        name = self._ent._pts.getEntry(name)
+        if hasattr(self, '_set'):
+            return name in self._set
+        else:
+            if self._ent.id < 0:
+                return self._ent._pts._IsAMemberOf(name.id, self._ent.id)
+            else:
+                return self._ent._pts._IsAMemberOf(self._ent.id, name.id)
+
+    def __repr__(self):
+        self._loadSet()
+        return repr(self._set)
+
+    def add(self, elt):
+        """Add one new entity to a group.
+
+        This method will add a new user to a group, regardless of
+        whether this instance represents a group or a user. The change
+        is also immediately reflected to the PRDB.
+
+        Raises:
+            TypeError: If you try to add a grop group to a group, or a
+                user to a user
+        """
+        elt = self._ent._pts.getEntry(elt)
+        if elt in self:
+            return
+
+        if self._ent.id < 0:
+            if elt.id < 0:
+                raise TypeError(
+                    "Adding group '%s' to group '%s' is not supported." %
+                    (elt, self._ent))
+
+            self._ent._pts._AddToGroup(elt.id, self._ent.id)
+
+            elt.groups._add(self._ent)
+        else:
+            if elt.id > 0:
+                raise TypeError(
+                    "Can't add user '%s' to user '%s'." %
+                    (elt, self._ent))
+
+            self._ent._pts._AddToGroup(self._ent.id, elt.id)
+
+            elt.members._add(self._ent)
+
+        self._add(elt)
+
+    def discard(self, elt):
+        """Remove one entity from a group.
+
+        This method will remove a user from a group, regardless of
+        whether this instance represents a group or a user. The change
+        is also immediately reflected to the PRDB.
+        """
+        elt = self._ent._pts.getEntry(elt)
+        if elt not in self:
+            return
+
+        if self._ent.id < 0:
+            self._ent._pts._RemoveFromGroup(elt.id, self._ent.id)
+            elt.groups._discard(self._ent)
+        else:
+            self._ent._pts._RemoveFromGroup(self._ent.id, elt.id)
+            elt.members._discard(self._ent)
+
+        self._discard(elt)
+
+    def remove(self, elt):
+        """Remove an entity from a group; it must already be a member.
+
+        If the entity is not a member, raise a KeyError.
+        """
+        if elt not in self:
+            raise KeyError(elt)
+
+        self.discard(elt)
+
+
+class PTEntry(object):
+    """An entry in the AFS protection database.
+
+    PTEntry represents a user or group in the AFS protection
+    database. Each PTEntry is associated with a particular connection
+    to the protection database.
+
+    PTEntry instances should not be created directly. Instead, use the
+    "getEntry" method of the PTS object.
+
+    If a PTS connection is authenticated, it should be possible to
+    change most attributes on a PTEntry. These changes are immediately
+    propogated to the protection database.
+
+    Attributes:
+      id: The PTS ID of the entry
+      name: The username or group name of the entry
+      count: For users, the number of groups they are a member of; for
+        groups, the number of users in that group
+      flags: An integer representation of the flags set on a given
+        entry
+      ngroups: The number of additional groups this entry is allowed
+        to create
+      nusers: Only meaningful for foreign-cell groups, where it
+        indicates the ID of the next entry to be created from that
+        cell.
+      owner: A PTEntry object representing the owner of a given entry.
+      creator: A PTEntry object representing the creator of a given
+        entry. This field is read-only.
+
+      groups: For users, this contains a collection class representing
+        the set of groups the user is a member of.
+      users: For groups, this contains a collection class representing
+        the members of this group.
+    """
+    _attrs = ('id', 'name', 'count', 'flags', 'ngroups', 'nusers')
+    _entry_attrs = ('owner', 'creator')
+
+    def __new__(cls, pts, id=None, name=None):
+        if id is None:
+            if name is None:
+                raise TypeError('Must specify either a name or an id.')
+            else:
+                id = pts._NameToId(name)
+
+        if id not in pts._cache:
+            if name is None:
+                name = pts._IdToName(id)
+
+            inst = super(PTEntry, cls).__new__(cls)
+            inst._pts = pts
+            inst._id = id
+            inst._name = name
+            if id < 0:
+                inst.members = PTRelationSet(inst)
+            else:
+                inst.groups = PTRelationSet(inst)
+            pts._cache[id] = inst
+        return pts._cache[id]
+
+    def __repr__(self):
+        if self.name != '':
+            return '<PTEntry: %s>' % self.name
+        else:
+            return '<PTEntry: PTS ID %s>' % self.id
+
+    def _get_id(self):
+        return self._id
+    def _set_id(self, val):
+        del self._pts._cache[self._id]
+        self._pts._ChangeEntry(self.id, newid=val)
+        self._id = val
+        self._pts._cache[val] = self
+    id = property(_get_id, _set_id)
+
+    def _get_name(self):
+        return self._name
+    def _set_name(self, val):
+        self._pts._ChangeEntry(self.id, newname=val)
+        self._name = val
+    name = property(_get_name, _set_name)
+
+    def _get_krbname(self):
+        return self._pts._AfsToKrb5(self.name)
+    def _set_krbname(self, val):
+        self.name = self._pts._Krb5ToAfs(val)
+    krbname = property(_get_krbname, _set_krbname)
+
+    def _get_count(self):
+        self._loadEntry()
+        return self._count
+    count = property(_get_count)
+
+    def _get_flags(self):
+        self._loadEntry()
+        return self._flags
+    def _set_flags(self, val):
+        self._pts._SetFields(self.id, access=val)
+        self._flags = val
+    flags = property(_get_flags, _set_flags)
+
+    def _get_ngroups(self):
+        self._loadEntry()
+        return self._ngroups
+    def _set_ngroups(self, val):
+        self._pts._SetFields(self.id, groups=val)
+        self._ngroups = val
+    ngroups = property(_get_ngroups, _set_ngroups)
+
+    def _get_nusers(self):
+        self._loadEntry()
+        return self._nusers
+    def _set_nusers(self, val):
+        self._pts._SetFields(self.id, users=val)
+        self._nusers = val
+    nusers = property(_get_nusers, _set_nusers)
+
+    def _get_owner(self):
+        self._loadEntry()
+        return self._owner
+    def _set_owner(self, val):
+        self._pts._ChangeEntry(self.id, newoid=self._pts.getEntry(val).id)
+        self._owner = val
+    owner = property(_get_owner, _set_owner)
+
+    def _get_creator(self):
+        self._loadEntry()
+        return self._creator
+    creator = property(_get_creator)
+
+    def _loadEntry(self):
+        if not hasattr(self, '_flags'):
+            info = self._pts._ListEntry(self._id)
+            for field in self._attrs:
+                setattr(self, '_%s' % field, getattr(info, field))
+            for field in self._entry_attrs:
+                setattr(self, '_%s' % field, self._pts.getEntry(getattr(info, field)))
+
+
+PTS_UNAUTH = 0
+PTS_AUTH = 1
+PTS_FORCEAUTH = 2
+PTS_ENCRYPT = 3
+
+
+class PTS(_pts.PTS):
+    """A connection to an AFS protection database.
+
+    This class represents a connection to the AFS protection database
+    for a particular cell.
+
+    Both the umax and gmax attributes can be changed if the connection
+    was authenticated by a principal on system:administrators for the
+    cell.
+
+    For sufficiently privileged and authenticated connections,
+    iterating over a PTS object will yield all entries in the
+    protection database, in no particular order.
+
+    Args:
+      cell: The cell to connect to. If None (the default), PTS
+        connects to the workstations home cell.
+      sec: The security level to connect with:
+        - PTS_UNAUTH: unauthenticated connection
+        - PTS_AUTH: try authenticated, then fall back to
+          unauthenticated
+        - PTS_FORCEAUTH: fail if an authenticated connection can't be
+          established
+        - PTS_ENCRYPT: same as PTS_FORCEAUTH, plus encrypt all traffic
+          to the protection server
+
+    Attributes:
+      realm: The Kerberos realm against which this cell authenticates
+      umax: The maximum user ID currently assigned (the next ID
+        assigned will be umax + 1)
+      gmax: The maximum (actually minimum) group ID currently assigned
+        (the next ID assigned will be gmax - 1, since group IDs are
+        negative)
+    """
+    def __init__(self, *args, **kwargs):
+        self._cache = {}
+
+    def __iter__(self):
+        for pte in self._ListEntries():
+            yield self.getEntry(pte.id)
+
+    def getEntry(self, ident):
+        """Retrieve a particular PTEntry from this cell.
+
+        getEntry accepts either a name or PTS ID as an argument, and
+        returns a PTEntry object with that name or ID.
+        """
+        if isinstance(ident, PTEntry):
+            if ident._pts is not self:
+                raise TypeError("Entry '%s' is from a different cell." %
+                                elt)
+            return ident
+
+        elif isinstance(ident, basestring):
+            return PTEntry(self, name=ident)
+        else:
+            return PTEntry(self, id=ident)
+
+    def getEntryFromKrbname(self, ident):
+        """Retrieve a PTEntry matching a given Kerberos v5 principal.
+
+        getEntryFromKrb accepts a krb5 principal, converts it to the
+        equivalent AFS principal, and returns a PTEntry for that
+        principal."""
+        return self.getEntry(self._Krb5ToAfs(ident))
+
+    def expire(self):
+        """Flush the cache of PTEntry objects.
+
+        This method will disconnect all PTEntry objects from this PTS
+        object and flush the cache.
+        """
+        for elt in self._cache.keys():
+            del self._cache[elt]._pts
+            del self._cache[elt]
+
+    def _get_umax(self):
+        return self._ListMax()[0]
+    def _set_umax(self, val):
+        self._SetMaxUserId(val)
+    umax = property(_get_umax, _set_umax)
+
+    def _get_gmax(self):
+        return self._ListMax()[1]
+    def _set_gmax(self, val):
+        self._SetMaxGroupId(val)
+    gmax = property(_get_gmax, _set_gmax)
Index: trunk/packages/python-afs/afs/tests/test__pts.py
===================================================================
--- trunk/packages/python-afs/afs/tests/test__pts.py	(revision 2599)
+++ trunk/packages/python-afs/afs/tests/test__pts.py	(revision 2599)
@@ -0,0 +1,45 @@
+import os
+from afs._pts import PTS
+import nose
+
+def get_this_cell():
+    # Feel free to add more places ThisCell might show up
+    to_try = ['/private/var/db/openafs/etc/ThisCell',
+              '/etc/openafs/ThisCell',
+              '/usr/vice/etc/ThisCell']
+    for f in to_try:
+        if os.path.isfile(f):
+            return open(f).read().strip()
+
+def test_init_home_cell():
+    p = PTS()
+    assert p.cell == get_this_cell(), "PTS doesn't initialize to ThisCell when none specified."
+
+def test_init_other_cell():
+    cell = 'zone.mit.edu'
+    p = PTS('zone.mit.edu')
+    assert p.cell == cell, "PTS doesn't initialize to provided cell."
+
+def test_user_name_to_id():
+    p = PTS()
+    name = 'broder'
+    id = p._NameToId(name)
+    assert id == 41803, "PTS can't convert user name to ID."
+    assert p._IdToName(id) == name, "PTS can't convert user ID to name."
+
+def test_group_name_to_id():
+    p = PTS()
+    name = 'system:administrators'
+    id = p._NameToId(name)
+    assert id == -204, "PTS can't convert group name to ID."
+    assert p._IdToName(id) == name, "PTS can't convert group ID to name."
+
+def test_name_or_id():
+    p = PTS()
+    name = 'system:administrators'
+    id = -204
+    assert p._NameOrId(name) == id, "PTS._NameOrId can't identify name."
+    assert p._NameOrId(id) == id, "PTS._NameOrId can't identify ID."
+
+if __name__ == '__main__':
+    nose.main()
Index: trunk/packages/python-afs/afs/tests/test_acl.py
===================================================================
--- trunk/packages/python-afs/afs/tests/test_acl.py	(revision 2599)
+++ trunk/packages/python-afs/afs/tests/test_acl.py	(revision 2599)
@@ -0,0 +1,21 @@
+import nose
+import afs.acl as acl
+
+def test_showRights():
+    assert acl.showRights(acl.READ | acl.WRITE) == "rw"
+
+def test_readRights():
+    assert acl.readRights('read') & acl.READ
+    assert acl.readRights('read') & acl.LOOKUP
+    assert not acl.readRights('read') & acl.WRITE
+
+def test_retrieve():
+    assert acl.ACL.retrieve('/afs/athena.mit.edu/contrib/bitbucket2').pos['system:anyuser'] & acl.WRITE
+    assert acl.ACL.retrieve('/afs/athena.mit.edu/user/t/a/tabbott').neg['yuranlu'] & acl.USR0
+
+def test_getCallerAccess():
+    assert acl.getCallerAccess('/afs/athena.mit.edu/contrib/bitbucket2') & acl.WRITE
+
+if __name__ == '__main__':
+    nose.main()
+
Index: trunk/packages/python-afs/debian/changelog
===================================================================
--- trunk/packages/python-afs/debian/changelog	(revision 2599)
+++ trunk/packages/python-afs/debian/changelog	(revision 2599)
@@ -0,0 +1,17 @@
+python-afs (0.1.1-1) unstable; urgency=low
+
+  * New upstream release.
+
+ -- Evan Broder <broder@mit.edu>  Mon, 07 Dec 2009 17:58:34 -0500
+
+python-afs (0.1.0-2) unstable; urgency=low
+
+  * Correct build-deps.
+
+ -- Evan Broder <broder@mit.edu>  Tue, 24 Nov 2009 18:53:35 -0500
+
+python-afs (0.1.0-1) unstable; urgency=low
+
+  * Initial Release.
+
+ -- Evan Broder <broder@mit.edu>  Sun, 22 Nov 2009 21:38:37 -0500
Index: trunk/packages/python-afs/debian/compat
===================================================================
--- trunk/packages/python-afs/debian/compat	(revision 2599)
+++ trunk/packages/python-afs/debian/compat	(revision 2599)
@@ -0,0 +1,1 @@
+5
Index: trunk/packages/python-afs/debian/control
===================================================================
--- trunk/packages/python-afs/debian/control	(revision 2599)
+++ trunk/packages/python-afs/debian/control	(revision 2599)
@@ -0,0 +1,14 @@
+Source: python-afs
+Section: python
+Priority: extra
+Maintainer: Evan Broder <broder@mit.edu>
+Build-Depends: cdbs, debhelper, python-all-dev, python-support, cython, python-setuptools, libopenafs-dev, libkrb5-dev
+Standards-Version: 3.8.3
+
+Package: python-afs
+Architecture: any
+Depends: ${shlibs:Depends}, ${misc:Depends}, ${python:Depends}
+Provides: ${python:Provides}
+Description: AFS bindings for Python
+ PyAFS provides a set of Python bindings for the AFS distributed
+ filesystem.
Index: trunk/packages/python-afs/debian/copyright
===================================================================
--- trunk/packages/python-afs/debian/copyright	(revision 2599)
+++ trunk/packages/python-afs/debian/copyright	(revision 2599)
@@ -0,0 +1,18 @@
+This package was written and debianized by Evan Broder
+<broder@mit.edu> on Sun, Nov. 22, 2009.
+
+Upstream Author: 
+
+    Evan Broder <broder@mit.edu>
+
+Copyright: 
+
+    Copyright (C) 2009 Evan Broder
+
+License:
+
+    This software is licensed under version 2 or later of the GPL. The
+    GPL can be found in the file `/usr/share/common-licenses/GPL-2'.
+    
+The Debian packaging is (C) 2009, Evan Broder <broder@mit.edu> and is
+licensed under the GPL License, see above.
Index: trunk/packages/python-afs/debian/gbp.conf
===================================================================
--- trunk/packages/python-afs/debian/gbp.conf	(revision 2599)
+++ trunk/packages/python-afs/debian/gbp.conf	(revision 2599)
@@ -0,0 +1,3 @@
+[git-buildpackage]
+upstream-branch = master
+debian-branch = debian
Index: trunk/packages/python-afs/debian/pycompat
===================================================================
--- trunk/packages/python-afs/debian/pycompat	(revision 2599)
+++ trunk/packages/python-afs/debian/pycompat	(revision 2599)
@@ -0,0 +1,1 @@
+2
Index: trunk/packages/python-afs/debian/rules
===================================================================
--- trunk/packages/python-afs/debian/rules	(revision 2599)
+++ trunk/packages/python-afs/debian/rules	(revision 2599)
@@ -0,0 +1,9 @@
+#!/usr/bin/make -f
+
+DEB_PYTHON_SYSTEM=pysupport
+
+include /usr/share/cdbs/1/rules/debhelper.mk
+include /usr/share/cdbs/1/class/python-distutils.mk
+
+clean::
+	rm -rf afs/_acl.c afs/_fs.c afs/_pts.c afs/_util.c
Index: trunk/packages/python-afs/setup.py
===================================================================
--- trunk/packages/python-afs/setup.py	(revision 2599)
+++ trunk/packages/python-afs/setup.py	(revision 2599)
@@ -0,0 +1,54 @@
+#!/usr/bin/python
+
+from distutils.core import setup
+from distutils.extension import Extension
+from Cython.Distutils import build_ext
+import sys
+import os
+
+for root in ['/Library/OpenAFS/Tools',
+             '/usr/local',
+             '/usr/afsws',
+             '/usr']:
+    if os.path.exists('%s/include/afs/afs.h' % root):
+        break
+
+include_dirs = [os.path.join(os.path.dirname(__file__), 'afs'),
+                '%s/include' % root]
+library_dirs = ['%s/lib' % root,
+                '%s/lib/afs' % root]
+if os.path.exists('%s/lib/libafsauthent_pic.a' % root):
+    suffix = '_pic'
+else:
+    suffix = ''
+libraries = ['afsauthent%s' % suffix, 'afsrpc%s' % suffix, 'resolv']
+define_macros = [('AFS_PTHREAD_ENV', None)]
+
+def PyAFSExtension(module, *args, **kwargs):
+    kwargs.setdefault('libraries', []).extend(libraries)
+    kwargs.setdefault('include_dirs', []).extend(include_dirs)
+    kwargs.setdefault('library_dirs', []).extend(library_dirs)
+    kwargs.setdefault('define_macros', []).extend(define_macros)
+    return Extension(module,
+                     ["%s.pyx" % module.replace('.', '/')],
+                     *args,
+                     **kwargs)
+
+setup(
+    name="PyAFS",
+    version="0.1.1",
+    description="PyAFS - Python bindings for AFS",
+    author="Evan Broder",
+    author_email="broder@mit.edu",
+    url="http://github.com/ebroder/pyafs/",
+    license="GPL",
+    requires=['Cython'],
+    packages=['afs', 'afs.tests'],
+    ext_modules=[
+        PyAFSExtension("afs._util"),
+        PyAFSExtension("afs._acl"),
+        PyAFSExtension("afs._fs"),
+        PyAFSExtension("afs._pts", libraries=['krb5']),
+        ],
+    cmdclass= {"build_ext": build_ext}
+)
