Skip to content

Commit

Permalink
readme
Browse files Browse the repository at this point in the history
  • Loading branch information
tef committed Apr 8, 2012
1 parent 5238163 commit 0da2610
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 103 deletions.
236 changes: 134 additions & 102 deletions README.rst
Original file line number Original file line Diff line number Diff line change
@@ -1,120 +1,152 @@
glyph-rpc glyph-rpc
--------- ---------
it is like json-rpc with callbacks. glyph-rpc makes minature websites for your objects, so you can grow your api

like you grow a website:
glyph is a client and server library for getting an object from a server,
and calling methods on it using the callbacks provided by the server.
the server offers objects over http, where the url describes the resource
and its state, and GETing an object returns any contents & callbacks for methods

glyph uses urls internally to track the state between requests, but
the server is free to change the mapping without breaking the client.
this allows glyph to provide duck-typing: clients do not care *what*
kind of object is returned, as long as it has the right named methods.

as a result, glyph allows you to grow your api like you grow a website.


- new methods and objects can be added without breaking clients
- glyph services can redirect/link to other services on other hosts - glyph services can redirect/link to other services on other hosts
- new methods and resources can be added without breaking clients
- glyph can take advantage of http tools like caches and load-balancers - glyph can take advantage of http tools like caches and load-balancers


glyph tries to exploit http rather than simply tunnel requests over it. glyph-rpc tries to exploit http rather than simply tunnel requests over it.


overview example
--------

* simple echo server code, running it
* connecting with the client, calling a method
* inspecting the contents of the reply

* urls are object id's - used to create/find resources at server
* callbacks are links and forms,

* how it works: router, mapper, resource
- router
attach resource classes to url prefixes
on request, finds resource and its mapper
uses the mapper to handle the request
- mappers
maps http <-> resource class
url query string is the resource state
make a resource to handle this request,
and dispatch to a method
adds callbacks to
- resources
have an index() method that returns contents
are created per request - represent a handle
to some state at the server.

* configuration server?

serialization
-------------
the serialization format is an extension of bencoding (from bittorrent).
it is not language specific, json-alike with a few more convieniences.

historical reasons mandated the support of bytestrings & unicode data,
and existing formats (xml/json) required clumsy workarounds. it works
but i'm not proud to reinvent another wheel.


json like vocabulary
- unicode -> u<len bytes>:<utf-8 string>
- dict -> d<key><value><key><value>....e - sorted ascending by key
- list -> l<item><item><item><item>....e
- float -> f<len>:<float in hex - c99 hexadecimal literal format>
- int -> i<number as text>e
- true -> T
- false -> F
- none -> N
additonal datatypes
- byte str -> s<len bytes>:<string>
- datetime -> D%Y-%m-%dT%H:%M:%S.%f
xml like vocabulary
- node -> N<name item><attr item><children item>
an object with a name, attributes and children
attributes is nominally a dict. children nominally list
- extension -> X<item><item><item>
like a node, but contains hyperlinks.

todo: timezones, periods?
todo: standard behaviour on duplicate keys

expect some tweaks

history
------- -------
glyph evolved from trying to connect processes together, after some bad experiences
with message queues exploding. http was known and loved throughout the company,
and yet another ad-hoc rpc system was born.


in the beginning, there was JSON and POST, and it mostly worked with the notable exception of UnicodeDecodeError. The server:
it didn't last very long. 8-bit data was a hard requirement, and so bencoding was used instead, with import glyph
a small change to handle utf-8 data as well as bytes.
r = glyph.Router() # a wsgi application

@r.add()
def hello()
return "Hello World"

s = glyph.Server(r)
s.start()

The client:
import glyph

server = glyph.get('http://server/')

print server.hello()

There is no stub for the client, just the library.

Adding a new function is simple:
@r.add()
def goodbye(name):
return "Goodbye " + name

Or change the functions a little:
@r.add()
def hello(name="World"):
return "Hello "+name

The client still works, without changes:
print server.hello()

with very little changes to call new methods:
print server.hello('dave')
print server.goodbye('dave')

functions can return lists, dicts, sets, byte strings, unicode,
dates, booleans, ints & floats:
@r.add()
def woo():
return [1,True, None, False, "a",u"b"]

functions can even return other functions that are mapped,
through redirection:

@r.add()
@glyph.redirect()
def greeting(lang="en"):
if lang == "en":
return hello

the client doesn't care:
greet = client.greeting()

print greet()


glyph can map objects too:
@r.add()
@glyph.redirect()
def find_user(name):
user_id = database.find_user(name)
return User(user_id)

@r.add()
class User(glyph.Resource):
def __init__(self, id):
self.id = id

def message(self, subject, body):
database.send_message(self.id, subject, body)

def bio(self):
return database.get_bio(self.id)

and the client can get a User and find details:
bob = server.find_user('bob')
bob.messsage('lol', 'feels good man')

like before, new methods can be added without breaking old clients.
unlike before, we can change object internals:

@r.add()
@glyph.redirect()
def find_user(name):
user_id, shard = database.find_user(name)
return User(user_id, shard)

@r.add()
class User(glyph.Resource):
def __init__(self, id, shard):
self.id = id
self.shard = shard

...

Even though the internals have changed, the names haven't, so the client
works as ever:

bob = server.find_user('bob')
bob.messsage('lol', 'feels good man')

underneath all this - glyph maps all of this to http:
# by default, a server returns an object with a bunch
# of methods that redirect to the mapped obejcts

server = glyph.get('http://server/')


we had a simple server stub that bound methods to urls and client code would call POST(url, {args}). # in this case, it will have an attribute 'find_user'
and we passed a bunch of urls around to sub-processes in order for them to report things. # find user is a special sort of object - a form
although we had not hard coded urls into the system, the api was still rigid. adding a new method # it has a url, method and arguments attached.
required passing in yet another url, or crafting urls per-request with client side logic.


instead of passing around urls and writing stubs to use them each time, we figured we could pass around links and forms,
which would *know* how to call the api, and it would fetch these *from* the api server itself.
the urls contain enough information to make the call on the server end and are opaque to the client.


we've needed to change the api numerous times since then. adding new methods doesn't break old clients. # when we call server.find_user(...), it submits that form
adding new state to the server doesn't break clients. using links and forms to interact with services is pleasant to # find_user redirects to a url for User(bob_id, cluster_id)
use for the client, and flexible for the server.
bob = server.find_user('bob')


of all the terrible code i've written, this worked out pretty well so far. # each object is mapped to a url, which contains the internal state
# of the object - i.e /User/?id=bob_id&cluster=cluster_id


# similarly, methods are mapped to a url too
# bob.message is a form pointing to /User/message?id=bo_id&cluster=cluster_id

bob.messsage('lol', 'feels good man')


status
------


notable omissions: although glyph maps urls to objects on the server side, these urls are
html/json/xml output opaque to the client - the server is free to change them to point to
content type overriding other objects, or to add new internal state without breaking the client.
authentication handling


Client code doesn't need to know how to construct requests, or store all
of the state needed to make requests - the server tells it, rather than
the programmer.




4 changes: 4 additions & 0 deletions doc/todo
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ todo code:


options support ? options support ?
def OPTIONS in mapper, options handling in Handler def OPTIONS in mapper, options handling in Handler
although roy thinks it is is pretty useless.


http compliance: http compliance:
set cache-control, expires, etags headers on get set cache-control, expires, etags headers on get
Expand Down Expand Up @@ -204,6 +205,9 @@ todo code:
or x-www-urlencoding/form encoding? or x-www-urlencoding/form encoding?


to think? to think?

uri templates

mapper: mapper:
transients transients
- shouldn't post to create? form should use get? - shouldn't post to create? form should use get?
Expand Down
5 changes: 5 additions & 0 deletions glyph/resource/handler.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from werkzeug.wrappers import Response from werkzeug.wrappers import Response
from werkzeug.exceptions import HTTPException, NotFound, BadRequest, NotImplemented, MethodNotAllowed from werkzeug.exceptions import HTTPException, NotFound, BadRequest, NotImplemented, MethodNotAllowed
from werkzeug.utils import redirect as Redirect from werkzeug.utils import redirect as Redirect
from werkzeug.datastructures import ResponseCacheControl


from ..data import CONTENT_TYPE, dump, parse, get, form, link, node, embed, ismethod, methodargs from ..data import CONTENT_TYPE, dump, parse, get, form, link, node, embed, ismethod, methodargs


Expand All @@ -15,15 +16,19 @@ class Handler(object):
INLINE=False INLINE=False
EXPIRES=False EXPIRES=False
REDIRECT=None REDIRECT=None
CACHE=ResponseCacheControl()

def __init__(self, handler=None): def __init__(self, handler=None):
if handler: if handler:
self.safe=handler.safe self.safe=handler.safe
self.inline=handler.inline self.inline=handler.inline
self.expires=handler.expires self.expires=handler.expires
self.cache=handler.cache
else: else:
self.safe=self.SAFE self.safe=self.SAFE
self.inline=self.INLINE self.inline=self.INLINE
self.redirect=self.REDIRECT self.redirect=self.REDIRECT
self.cache=self.CACHE


@staticmethod @staticmethod
def parse(resource, data): def parse(resource, data):
Expand Down
2 changes: 1 addition & 1 deletion tests/__init__.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@






class Test(unittest2.TestCase): class EncodingTest(unittest2.TestCase):
def testCase(self): def testCase(self):
cases = [ cases = [
1, 1,
Expand Down

0 comments on commit 0da2610

Please sign in to comment.