From: <ian...@us...> - 2003-07-19 01:28:17
|
Update of /cvsroot/webware-sandbox/Sandbox/ianbicking/pyweb In directory sc8-pr-cvs1:/tmp/cvs-serv3875/pyweb Added Files: IHTTP.py Log Message: Interface for The Universal Python Web Programming Environment. One Framework to Rule Them All! We're All-One Or None! Assimilate Assimilate Assimilate! --- NEW FILE: IHTTP.py --- """ A PyWeb HTTP Interface Spec =========================== Ian Bicking <ia...@co...> 18 July 2003 v0.1 The request and response should be packaged together, so they can be adapted to interfaces that use a single object with both concepts mingled -- in this case the package (Transaction) will be adapted. Request and response should be separate so that interfaces where they are separate are easily implemented. No information should be lost at this stage. Some interfaces may not keep information on what order variables were passed, for instance, as a dictionary interface is easier to work with. This is not intended to be an easy, but rather a complete interface. It should still be possible to adapt other interfaces back into this object. This is particularly important for forwarding and modifying the request for nested calls. If the request is modified and then forwarded to another resource that requires a different interface, it should be possible to adapt the modified request into the needed interface. In cases where information is lost (e.g., ordering), it is felt better to consider the result undefined than to disallow this forwarding. Typically these inconsistencies will not matter in most applications. Forwarding ---------- It may be better if these requests cannot be forwarded, but that a new request/response can be easily created can be used, and that the response can be attached to the current response (kind of proxy-like). But generally, it's better if inclusion can happen on a more abstract level than forwarding and inclusion of subrequests -- e.g., by a function call. Issues ------ Methods are used instead of attributes for all accessors. This is Python 2.1 compatible, as there are situations where attributes would need to be protected with ``property``. This also makes it explicit how values are supposed to be modified. Design Goals ------------ This design is meant to be flexible enough to support a wide variety of frameworks. To achieve this it tries to be simple, not general. The simple interface to URLParser and Resource make little presumption about how those objects implement that interface. Application Server ------------------ This is the larger context in which programs are executed (which I refer to as an 'application server', even though that might not be accurate for all contexts). This remains unspecified in these interfaces, though I feel it should be specified in some manner. There should be some level of introspection. The most obvious instance is to find out whether the request is being run in a thread, process, or asychronously, and potentially to have some control over this (e.g., to spawn a thread or fork a process in an async environment). URL Introspection ----------------- URL introspection is important and unreliable in most environments. It is difficult to be general in the face of mod_rewrite rules, reverse proxies, and other interfaces that are largely opaque to this level. The methods associated with URL introspection remain poorly defined in this interface spec. """ class ITransaction: def __init__(self, request, response): """ Start the transaction, given a request and response. """ def request(self): """ Return the request. """ def response(self): """ Return the response. """ class IHTTPRequest: def __init__(self, *args, **kw): """ The __init__ is left out for now, since presumably this object will be created from something lower-level, and be constructed from those lower-level structures.""" def transaction(self): """ The encapsulating Transaction object. """ def setTransaction(self, transaction): """ Set the transaction. Should only be called once in the request's life, but typically the objects are constructed with the request first, then response, then transaction. """ def pathInfo(self): """ All of the path that comes after the found resource. So if you find the resource at /dir/resource, and the URL was /dir/resource/other/stuff, this would be '/other/stuff'. This should always start with a / or be empty. """ def setPathInfo(self): """ Set the path info. Necessary since the request will be created before the resource is found, and only once the resource is found do we know what the extra path is. Resources that do not want any extra path must raise an exception to disallow it, this restriction is not enforced during URL resolution. """ def getFields(self): """ A list of two-tuples that were used for URL parameters, for ?arg1=a&arg2&arg1=c you'd get [('arg1', 'a'), ('arg2', ''), ('arg1', 'c')] Note this only returns URL parameters, as would be passed in a GET request. This does not return POST parameters. During a POST it is still valid to include GET parameters, and this way they are distinguished. Empty values, e.g., arg2, are always preserved. 'arg2' and 'arg2=' are not distinguished. The resulting list should not be modified. @@: How important is the ordering? Do we care? It adds complexity, because most interfaces don't preserve the ordering, and the interfaces to modify these variables specifically do not support indication of the ordering (nor would such an interface be easy to work with). """ def setGetFields(self, value): """ Set the tuple list, as would be returned by getFields(). This is intended for forwarding, not for construction of the request, though nothing bars that. @@: Should we specify, if a parameter is set, and it is not explicitly GET or POST, then it should be assumed a GET parameter? Or like Zope should we have a separate set of parameters that are only constructed after the request? If so, should these parameters be modifiable at all? """ def getFieldDict(self): """ A dictionary of values. This provides similar functionality to getFields(), but potentially losing information about ordering. This dictionary is derivative of the list returned by getFields(). The dictionary should not be modified. If multiple keys exist, then the value of the dictionary is a list of these keys. The order of this list should be the same order as the keys appeared in the URL. """ def setFieldDict(self): """ Set the result of getFieldDict(). @@: Should this be setFieldValue(self, name, value)? """ def postFields(self): """ A list of two-tuples, as in getFields. Files uploads will be represented by @@ (I'm not sure what... basically like a CGI FieldStorage instance) """ def setPostFields(self, value): """ Set the result of postFields() """ def postFieldDict(self): """ A dictionary, ala getFieldDict() """ def setPostFieldDict(self): """ Set the result of postFieldDict() """ def input(self): """ A file-like object with the body of the request (e.g., the data from a POST, unprocessed, or the file uploaded via a PUT). @@: Will you have to rewind the file before using it, if it is POSTed data? """ def method(self): """ The method used (GET, POST, PUT, etc). All caps. """ def cookies(self): """ Returns a dictionary of cookie names to cookie values. """ def cgiEnviron(self): """ Returns a dictionary of variables, matching the environment as passed to a CGI script. (@@: We may wish to further specify this dictionary) """ def time(self): """ The time at which the request was made, represented by a float or int as might be returned by time.time() """ def requestURI(self, includeHost=False): """ The complete URI of the request. This is typically set during instantiation, and does not depend on the resource being processed. """ def resourceURI(self, includeHost=False): """ The URI of the resource that is being accessed. If includeHost is true, then the full http://..., otherwise just the absolute URI. This does *not* include anything from pathInfo(). """ def applicationRootURI(self, includeHost=False): """ The URI of the application root. This is generally the root of the application server, or an identifiable application embedded in that server. If this does not apply to this request (e.g., Apache identified the resource through its extension) then None is returned. @@: This is a fuzzy definition. """ class IHTTPResponse: def setHeader(self, name, value): """ Sets a header in the response. Used like: setHeader('content-type', 'text/html') """ def addHeader(self, name, value): """ Adds a header to the response. If a header by this name already exists, then both headers will be sent. """ def delHeader(self, name): """ Remove a header from the response. Removes all headers by that name. """ def headers(self): """ The current set of headers that will be set, a dictionary. If a header will be sent multiple times, then the value of that header will be a list, e.g., {'x-repeated': ['x', 'y']} will create: x-repeated: x x-repeated: y All keys will be lower-case. The returned dictionary should not be modified. """ def addCookie(self, name, cookie): """ Add a cookie object to the response. An object of type Cookie.BaseCookie, generally, or something similar. If a cookie by that name is already being sent, then it will be lost/overwritten. (@@: would it make sense to send both?) @@: I thought about not giving them names at all, except that it's hard to refer to the cookies later to delete them. Maybe we should simply use some attribute of the cookie object itself to indicate which to delete. Note that higher-level interfaces may allow string arguments instead of special cookie objects. But we do not, here. """ def delCookie(self, name): """ Remove a cookie, by name. """ def cookies(self): """ A dictionary of cookies, by cookie name. The dictionary should not be modified. """ def setStatus(self, code, message=None): """ Set the status of the response. The code can be an integer or string, but is assumed to be the numeric response. message is typically 'OK', 'Not Found', etc., but will be provided if not given. """ def status(self): """ Return the integer code of the response status, default 200. """ def write(self, s): """ Write the string s to the output. This will be accumulated, unless we have set autoFlush to true. """ def flush(self): """ Flush the accumulated response data. Any headers will be sent if necessary, and any further attempt to change the headers, status, or cookies will raise a ValueError exception. """ def setAutoFlush(self): """ When called, all data is flushed and all future data will be written immediately to the response stream. @@: This could also be a flag to flush(), but a separate method seems better. """ def commitHeaders(self): """ Write the headers and get ready to write the rest of the response stream. @@: Redundant with flush()? """ def isCommitted(self): """ Has this response been committed? If true then no more headers can be set. """ def redirectOutput(self, func, outputStream, headerTrap=None, args=None, kw=None): """ func will be called with *args, **kw. During this time the output will be written to outputStream (e.g., a StringIO object). If headerTrap is given, then calls to setHeader, delHeader, headers, addCookie, delCookie, cookies, setStatus, and status will be forwarded to this object. If not, then those calls will be treated like normal, and will effect the ultimate response. Normal operation will resume after func returns. Calls to redirectOutput can be nested. """ class IURLParser: def __init__(self, *args, **kw): """ This object will be created by configuration parameters, or some uber-parser that can delegate to sub-parsers will be used globally in an application. """ def resourceForTransaction(self, transaction): """ Returns a IResource object for this transaction (ITransaction interface), typically parsing the request. It may modify the request or response. This is almost always implemented as: return self.resourceForPath( transaction, transaction.request().requestURI() This method should be threadsafe. """ def resourceForPath(self, transaction, path): """ Parse the (remaining) path, with transaction as the context. Return a resource object. This way different URLParsers can be attached at different places in the directory hierarchy, and can parse from a given location (instead of the root of the request URI). Typically other contextual information (e.g., document root) will be passed to the constructor. This method should be threadsafe. """ class IResource: def __init__(self, *args, **kw): """ The resource is typically constructed by URLParser instance, though of course the URLParser may delegate this task to another object. How it is constructed or potentially reused is mostly up to the URLParser. """ def isThreadsafe(self): """ Returns true/false. The URLParser should take this into account when considering concurrent reuse of this resource. """ def isReusable(self): """ Returns true/false. If true, then the URLParser should be able to pool instances instead of reconstructing them for new requests (though it need not). """ def respondToTransaction(self, transaction): """ Respond to the transaction, typically using the request to generate calls to the response. This is your application. """ |