Changeset 876

Show
Ignore:
Timestamp:
08/04/07 01:19:30 (1 year ago)
Author:
sgillies
Message:

Switch to Postgres storage, provide atompub links in KML

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • hammock/trunk/hammock.py

    r848 r876  
    11#!env python 
    22 
    3 import daemon 
    43import model 
    54import os 
     
    87if __name__ == '__main__': 
    98     
    10     code = daemon.createDaemon() 
    11  
    129    from wsgiref.simple_server import WSGIServer, WSGIRequestHandler 
    1310    httpd = WSGIServer(('', 8081), WSGIRequestHandler) 
    1411    httpd.set_app(urls.urls) 
    15     try: 
    16         model.COLLECTION.load() 
    17     except IOError: 
    18         pass 
    1912    print "Serving HTTP on %s port %s ..." % httpd.socket.getsockname() 
    2013    try: 
    21         f = open('hammock.pid', 'wb') 
    22         f.write(str(os.getpid())) 
    23         f.close() 
    2414        httpd.serve_forever() 
    2515    except KeyboardInterrupt: 
    26         model.COLLECTION.save() 
    2716        raise 
    2817 
  • hammock/trunk/model.py

    r861 r876  
    1 """ 
    2 SimpleFeature is a working example of a class that satisfies the Python GIS 
    3 feature protocol. 
    4 """ 
    51 
    6 import geojson 
    7 import time 
     2from StringIO import StringIO 
    83 
    9 import infoset 
     4from lxml import etree 
     5from sqlalchemy import * 
     6from sqltypes import Geometry 
    107 
    11 class PropertyCollection(dict): 
    12      
    13     """ 
    14     A Collection of feature properties. 
    15     """ 
    16      
    17     def __getattr__(self, name): 
    18         return self[name] 
    19  
    20     def __setattr__(self, name, value): 
    21         self[name] = value 
     8from shapely.geometry import Point 
    229 
    2310 
    24 class SimpleFeature(object): 
     11ENGINE = create_engine("postgres://postgres:postgres@localhost/hammock0") 
    2512 
    26     """ 
    27     A simple, Atom-ish, single geometry (WGS84) GIS feature.  
    28     """ 
     13metadata = MetaData(ENGINE) 
    2914 
    30     def __init__(self, id=None, geometry=None, title=None, summary=None,  
    31                  published=None, updated=None): 
    32         """Initialize.""" 
    33         self.id = id 
    34         self.geometry = geometry 
    35         self.properties = PropertyCollection() 
    36         self.properties['title'] = title 
    37         self.properties['summary'] = summary 
    38         self.properties['published'] = published 
    39         self.properties['updated'] = updated 
     15PLACES = Table( 
     16    'places', 
     17    metadata, 
     18    Column('place_id', Integer, primary_key=True), 
     19    Column('published', DateTime, default=func.current_timestamp()), 
     20    Column('updated', DateTime, 
     21        default=func.current_timestamp(), 
     22        onupdate=func.current_timestamp() 
     23        ), 
     24    Column('title', String(60)), 
     25    Column('summary', String()), 
     26    Column('kml', String()), 
     27    Column('the_geom', Geometry(4326, 'POINT')) 
     28    ) 
    4029 
    41     def __getitem__(self, key): 
    42         """To satisfy the Python GIS feature protocol, we need to handle at 
    43         least id, geometry, and properties keys. 
    44         """ 
    45         try: 
    46             return getattr(self, key) 
    47         except AttributeError, e: 
    48             raise KeyError, e 
     30class SQLCollection(object): 
    4931 
    50  
    51 def createSimpleFeature(o): 
    52     """Create an instance of SimpleWebFeature from a dict, o. If o does not 
    53     match a Python feature object, simply return o. This function serves as a  
    54     simplejson decoder hook. See coding.load().""" 
    55     try: 
    56         id = o.get('id') 
    57         g = o.get('geometry') 
    58         p = o.get('properties') 
    59         return SimpleFeature(str(id),  
    60             {'type': str(g.get('type', None)), 
    61              'coordinates': g.get('coordinates', [])}, 
    62             title=p.get('title', None), 
    63             summary=p.get('summary', None), 
    64             published=str(p.get('published', None)), 
    65             updated=str(p.get('updated', None))) 
    66     except (KeyError, TypeError, AttributeError): 
     32    def __init__(self): 
    6733        pass 
    68     return o 
    69  
    70  
    71 # Collection 
    72  
    73 class SimpleCollection(object): 
    74  
    75     def __init__(self, size=30): 
    76         self.size = 30 
    77         self.dsname = 'data.json' 
    78         self.data = [ 
    79             ('0', SimpleFeature('0',  
    80                 geometry={'type': 'Point', 'coordinates': [-105.8, 40.5]}, 
    81                 title=u'Feature 0', summary=u'The canonical feature', 
    82                 published='2007-04-09T12:48:36-06:00', 
    83                 updated='2007-04-09T12:48:36-06:00'))     
    84         ] 
    85  
    86     def save(self): 
    87         f = open(self.dsname, 'wb') 
    88         geojson.dump(self.data, f) 
    89         f.close() 
    90  
    91     def load(self): 
    92         f = open(self.dsname, 'rb') 
    93         self.data = geojson.load(f, object_hook=createSimpleFeature) 
    94         f.close() 
    9534 
    9635    def get(self, key): 
    97         d = dict(self.data) 
    98         if key in d.keys(): 
    99             return d[key] 
    100         else: 
     36        result = PLACES.select(PLACES.c.place_id==key).execute() 
     37        return result.fetchone() 
     38 
     39    def __iter__(self): 
     40        result = PLACES.select().execute() 
     41        return iter(result.fetchall()) 
     42 
     43    def add(self, atom=None, kml=None): 
     44        if atom: 
     45            params = parse_atom(atom) 
     46        elif kml: 
     47            params = parse_kml(kml) 
     48            params["kml"] = kml 
     49        connection = ENGINE.connect() 
     50        transaction = connection.begin() 
     51        try: 
     52            connection.execute( 
     53                PLACES.insert(), 
     54                params, 
     55                published=func.current_timestamp(), 
     56                updated=func.current_timestamp(), 
     57                ) 
     58            row = connection.execute( 
     59                PLACES.select( 
     60                    order_by=[desc(PLACES.c.place_id)],  
     61                    limit=1 
     62                    ) 
     63                ).fetchone() 
     64            newid = row['place_id'] 
     65            transaction.commit() 
     66            connection.close() 
     67            return newid 
     68        except: 
     69            transaction.rollback() 
     70            connection.close() 
    10171            return None 
    10272 
    103     def __iter__(self): 
    104         return iter(self.data) 
     73    def set(self, i, atom=None, kml=None): 
     74        if atom: 
     75            params = parse_atom(atom) 
     76        elif kml: 
     77            params = parse_kml(kml) 
     78            params["kml"] = kml 
     79        connection = ENGINE.connect() 
     80        transaction = connection.begin() 
     81        try: 
     82            connection.execute( 
     83                PLACES.update(PLACES.c.place_id==i), 
     84                params, 
     85                ) 
     86            transaction.commit() 
     87            connection.close() 
     88        except: 
     89            transaction.rollback() 
     90            connection.close() 
     91            raise 
    10592 
    106     def add(self, xml=None, json=None): 
    107         try: 
    108             if xml: 
    109                 ob = createSimpleFeature(infoset.parse_atom(xml)) 
    110             elif json: 
    111                 ob = geojson.loads(json, object_hook=createSimpleFeature) 
    112             ob.id = str(int(self.data[0][0]) + 1) 
    113             ob.properties.published = \ 
    114                 time.strftime('%Y-%m-%dT%H:%M:%S+00:00', time.gmtime()) 
    115             ob.properties.updated = ob.properties.published 
    116         except ValueError: 
    117             return None 
    118         if len(self.data) == self.size: 
    119             del self.data[-1] 
    120         self.data.insert(0, (ob.id, ob)) 
    121         return ob.id 
     93    @property 
     94    def updated(self): 
     95        result = PLACES.select( 
     96            PLACES.c.updated != None, 
     97            order_by=[desc(PLACES.c.updated)], 
     98            limit=1 
     99            ).execute().fetchone() 
     100        return result['updated'] 
     101         
    122102 
    123     def set(self, i, xml=None): 
    124         for j in range(len(self.data)): 
    125             n, prev = self.data[j] 
    126             if i == n: 
    127                 try: 
    128                     if xml: 
    129                         ob = createSimpleFeature(infoset.parse_atom(xml)) 
    130                     ob.id = str(i) 
    131                     ob.properties.published = prev.properties.published 
    132                     ob.properties.updated = \ 
    133                         time.strftime('%Y-%m-%dT%H:%M:%S+00:00', time.gmtime()) 
    134                 except ValueError: 
    135                     return None 
    136                 self.data[j] = (str(i), ob) 
    137                 break 
    138          
    139 COLLECTION = SimpleCollection() 
     103COLLECTION = SQLCollection() 
    140104 
     105atom_nsd = { 
     106    'atom': "http://www.w3.org/2005/Atom", 
     107    'georss': "http://www.georss.org/georss" 
     108    } 
     109 
     110def parse_atom(data): 
     111    tree = etree.parse(StringIO(data)) 
     112    root = tree.getroot() 
     113    try: 
     114        title = root.xpath('atom:title', atom_nsd)[0].text 
     115    except: 
     116        title = None 
     117    try: 
     118        summary = root.xpath('atom:summary', atom_nsd)[0].text 
     119    except: 
     120        summary = None 
     121    try: 
     122        lat, long = root.xpath('georss:point', atom_nsd)[0].text.split() 
     123        the_geom = Point(float(long), float(lat)) 
     124    except: 
     125        the_geom = None 
     126 
     127    return {"title": title, "summary": summary, "the_geom": the_geom} 
     128 
     129kml_nsd = { 
     130    "kml": "http://earth.google.com/kml/2.1" 
     131    } 
     132 
     133def parse_kml(data): 
     134    tree = etree.parse(StringIO(data)) 
     135    root = tree.getroot() 
     136    try: 
     137        title = root.xpath('kml:Document/kml:Placemark/kml:name', kml_nsd)[0].text 
     138    except: 
     139        title = None 
     140    try: 
     141        summary = root.xpath("kml:Document/kml:Placemark/kml:description", kml_nsd)[0].text 
     142    except: 
     143        summary = None 
     144    try: 
     145        long, lat, z = root.xpath("kml:Document/kml:Placemark/kml:Point/kml:coordinates", kml_nsd)[0].text.split(",") 
     146        the_geom = Point(float(long), float(lat)) 
     147    except: 
     148        the_geom = None 
     149 
     150    return {"title": title, "summary": summary, "the_geom": the_geom} 
     151     
  • hammock/trunk/templates/collection.html

    r854 r876  
    1717    <div><a href="/index.html">Back to service index</a></div> 
    1818 
     19    <h3>Properties</h3> 
     20    <div class="widget"> 
     21      <label>Updated:</label> 
     22      <br/> 
     23      <span py:content="updated.strftime('%Y-%m-%dT%H:%M:%S-06:00')">VALUE</span> 
     24    </div> 
     25 
    1926    <h3>Items</h3> 
    20     <div py:for="id, item in collection"> 
     27    <div py:for="item in collection"> 
    2128      <a  
    22         py:with="atts={'href': 'features/%s.html' % id}" 
     29        py:with="atts={'href': 'features/%s.html' % item['place_id']}" 
    2330        py:attrs="atts"  
    24         py:content="item.properties['title']" 
     31        py:content="item['title']" 
    2532        > 
    2633        x 
     
    2936 
    3037    <h3>Alternate Representations</h3> 
    31     <div><a href="features.atom">Atom</a></div> 
     38    <div><a href="features/">Atom</a></div> 
    3239    <div><a href="features.kml">KML</a></div> 
    33     <div><a href="features">JSON</a></div> 
    3440 
    3541    <p id="fine-print">Under-powered by Hammock</p> 
  • hammock/trunk/templates/item.html

    r849 r876  
    1717    <h1 py:content="title">Item</h1> 
    1818    <div><a href="../features.html">Back to collection</a></div> 
    19     <h2 py:content="item.properties['title']">Title</h2> 
     19    <h2 py:content="item['title']">Title</h2> 
    2020     
    2121    <h3>Geometry</h3> 
     
    2323      <label>Type:</label> 
    2424      <br/> 
    25       <span py:content="item.geometry['type']">VALUE</span> 
     25      <span py:content="item['the_geom'].geom_type">VALUE</span> 
    2626    </div> 
    2727    <div class="widget"> 
    2828      <label>Coordinates:</label> 
    2929      <br/> 
    30       <span py:content="str(item.geometry['coordinates'])">VALUE</span> 
     30      <span py:content="item['the_geom'].wkt">VALUE</span> 
    3131    </div> 
    3232     
    3333    <h3>Properties</h3> 
    34     <div  
    35       py:for="key in ['summary', 'published', 'updated']" 
    36       class="widget" 
    37       > 
    38       <label><span py:content="key.capitalize()">LABEL</span>:</label> 
     34    <div class="widget"> 
     35      <label>Summary:</label> 
    3936      <br/> 
    40       <span py:content="item.properties[key]">VALUE</span> 
     37      <span py:content="item['summary']">VALUE</span> 
     38    </div> 
     39    <div class="widget"> 
     40      <label>Published:</label> 
     41      <br/> 
     42      <span py:content="item['published'].strftime('%Y-%m-%dT%H:%M:%S-06:00')">VALUE</span> 
     43    </div> 
     44    <div class="widget"> 
     45      <label>Updated:</label> 
     46      <br/> 
     47      <span py:content="item['updated'].strftime('%Y-%m-%dT%H:%M:%S-06:00')">VALUE</span> 
    4148    </div> 
    4249 
    4350    <h3>Alternate Representations</h3> 
    44     <div><a py:attrs="{'href': './%s' % item.id}">JSON</a></div> 
     51    <div><a py:attrs="{'href': './%s' % item['place_id']}">Atom</a></div> 
     52    <div><a py:attrs="{'href': './%s.kml' % item['place_id']}">KML</a></div> 
    4553 
    4654    <p id="fine-print">Under-powered by Hammock</p> 
  • hammock/trunk/urls.py

    r861 r876  
    55urls.add('/index.html', GET=view.service_html) 
    66urls.add('/index.atom', GET=view.service_atom) 
    7 urls.add('/features[/]', GET=view.list, POST=view.feature_post) 
     7 
     8urls.add('/features[/]', 
     9    GET=view.collection_atom, POST=view.collection_POST 
     10    ) 
     11urls.add('/features.atom', 
     12    GET=view.collection_atom, POST=view.collection_POST 
     13    ) 
    814urls.add('/features.html', GET=view.collection_html) 
    9 urls.add('/features.atom', GET=view.atom, POST=view.feature_post_atom) 
    10 urls.add('/features.kml', GET=view.kml) 
    11 urls.add('/features/{id}[/]', GET=view.feature_get) 
     15urls.add('/features.kml', GET=view.collection_kml) 
     16#urls.add('/features/main.kml', GET=view.collection_nwlink) 
     17 
     18urls.add('/features/{id}[/]', GET=view.feature_atom, PUT=view.feature_PUT) 
     19urls.add('/features/{id}.atom', GET=view.feature_atom, PUT=view.feature_PUT) 
    1220urls.add('/features/{id}.html', GET=view.feature_html) 
    13 urls.add('/features/{id}.atom', GET=view.feature_atom, PUT=view.feature_edit) 
    14 urls.add('/features/{id}.json', GET=view.feature_get) 
     21urls.add('/features/{id}.kml', GET=view.feature_kml, PUT=view.feature_PUT) 
    1522 
  • hammock/trunk/view.py

    r861 r876  
    55 
    66import envutil 
    7 import geojson 
    8 import infoset 
    97import model 
    108 
     
    2220 
    2321def service_atom(environ, start_response): 
    24     tmpl = loader.load('service.xml') 
     22    tmpl = loader.load('atom_service.xml') 
    2523    stream = tmpl.generate(site_url=envutil.site_url(environ)) 
    2624    start_response("200 OK", [('Content-Type', 'application/atomsvc+xml')]) 
    2725    return [stream.render()] 
    28  
    29 def list(environ, start_response): 
    30     list = [] 
    31     url = envutil.request_url(environ) 
    32     for item in model.COLLECTION: 
    33         list.append({'id': item[1].id, 'uri': "%s/%s" % (url, item[1].id)})   
    34     start_response("200 OK", [('Content-Type', 'text/plain')]) 
    35     return [geojson.dumps({'members': list})] 
    3626 
    3727def collection_html(environ, start_response): 
     
    3929    stream = tmpl.generate( 
    4030        title='Collection: Features', 
     31        updated=model.COLLECTION.updated, 
    4132        collection=model.COLLECTION 
    4233        ) 
     
    4435    return [stream.render()] 
    4536 
    46 def atom(environ, start_response): 
    47     feed = infoset.build_atom(environ, model.COLLECTION) 
     37def collection_atom(environ, start_response): 
     38    tmpl = loader.load('atom_feed.xml') 
     39    self_url = envutil.request_url(environ) 
     40    alt_url = "%s/%s" % (envutil.site_url(environ), 'features.html') 
     41    stream = tmpl.generate( 
     42        title='Collection: Features', 
     43        self_url=self_url, 
     44        alt_url=alt_url, 
     45        updated=model.COLLECTION.updated, 
     46        collection=model.COLLECTION 
     47        ) 
    4848    start_response("200 OK", [('Content-Type', 'application/atom+xml')]) 
    49     return ['<?xml version="1.0" encoding="utf-8"?>', infoset.tostring(feed)] 
     49    return [stream.render()] 
    5050 
    51 def kml(environ, start_response): 
    52     kml = infoset.build_kml(environ, model.COLLECTION) 
     51def collection_kml(environ, start_response): 
     52    tmpl = loader.load('collection.kml') 
     53    self_url = "%s/%s" % (envutil.site_url(environ), 'features/') 
     54    alt_url = "%s/%s" % (envutil.site_url(environ), 'features.html') 
     55    stream = tmpl.generate( 
     56        title='Collection: Features', 
     57        self_url=self_url, 
     58        alt_url=alt_url, 
     59        collection=model.COLLECTION 
     60        ) 
    5361    start_response("200 OK", [('Content-Type', 'application/vnd.google-earth.kml+xml')]) 
    54     return ['<?xml version="1.0" encoding="utf-8"?>', infoset.tostring(kml)] 
     62    return [stream.render()] 
     63 
     64def collection_POST(environ, start_response): 
     65    data = environ['wsgi.input'].read(int(environ['CONTENT_LENGTH'])) 
     66    if environ['CONTENT_TYPE'] == 'application/atom+xml': 
     67        fid = model.COLLECTION.add(atom=data) 
     68    elif environ['CONTENT_TYPE'] == 'application/vnd.google-earth.kml+xml': 
     69        fid = model.COLLECTION.add(kml=data) 
     70    if fid is None: 
     71        start_response("400 Bad Request", [('Content-Type', 'text/plain')]) 
     72        return ['Posted data is malformed: ', data] 
     73    else: 
     74        url = envutil.request_url(environ) 
     75        start_response("200 OK", [('Content-Type', 'text/plain')]) 
     76        return ['Feature created with URI ', "%s/%s.atom" % (url[:-5], fid)] 
    5577 
    5678def feature_html(environ, start_response): 
     
    5981    tmpl = loader.load('item.html') 
    6082    stream = tmpl.generate( 
    61         title='Feature: %s' % f.id
     83        title='Feature: %s' % f['place_id']
    6284        item = f 
    6385        ) 
     
    6890    fid = environ['wsgiorg.routing_args'][1]['id'] 
    6991    f = model.COLLECTION.get(fid) 
    70     tmpl = loader.load('entry.xml') 
     92    tmpl = loader.load('atom_entry.xml') 
    7193    stream = tmpl.generate(item = f) 
    7294    start_response("200 OK", [('Content-Type', 'application/atom+xml')]) 
    7395    return [stream.render()] 
    7496 
    75 def feature_get(environ, start_response): 
     97def feature_kml(environ, start_response): 
    7698    fid = environ['wsgiorg.routing_args'][1]['id'] 
    7799    f = model.COLLECTION.get(fid) 
    78     if not f: 
    79         start_response("404 Not Found", [('Content-Type', 'text/plain')]) 
    80         return [] 
    81     else: 
    82         start_response("200 OK", [('Content-Type', 'text/plain')]) 
    83         return [geojson.dumps(f)] 
     100    tmpl = loader.load('collection.kml') 
     101    self_url = "%s/%s" % (envutil.site_url(environ), 'features/') 
     102    alt_url = "%s/%s" % (envutil.site_url(environ), 'features.html') 
     103    stream = tmpl.generate( 
     104        title='Feature %s' % f['place_id'], 
     105        self_url=self_url, 
     106        alt_url=alt_url, 
     107        collection=[f] 
     108        ) 
     109    start_response("200 OK", [('Content-Type', 'application/vnd.google-earth.kml+xml')]) 
     110    return [stream.render()] 
    84111 
    85 def feature_post(environ, start_response): 
    86     data = environ['wsgi.input'].read(int(environ['CONTENT_LENGTH'])) 
    87     fid = model.COLLECTION.add(json=data) 
    88     if fid is None: 
    89         start_response("400 Bad Request", [('Content-Type', 'text/plain')]) 
    90         return ['Posted data is malformed: ', data] 
    91     else: 
    92         url = envutil.request_url(environ) 
    93         start_response("200 OK", [('Content-Type', 'text/plain')]) 
    94         return ['Feature created with URI ', "%s/%s" % (url, fid)] 
    95  
    96 def feature_post_atom(environ, start_response): 
    97     data = environ['wsgi.input'].read(int(environ['CONTENT_LENGTH'])) 
    98     fid = model.COLLECTION.add(xml=data) 
    99     if fid is None: 
    100         start_response("400 Bad Request", [('Content-Type', 'text/plain')]) 
    101         return ['Posted data is malformed: ', data] 
    102     else: 
    103         url = envutil.request_url(environ) 
    104         start_response("200 OK", [('Content-Type', 'text/plain')]) 
    105         return ['Feature created with URI ', "%s/%s.atom" % (url[:-5], fid)] 
    106  
    107 def feature_edit(environ, start_response): 
     112def feature_PUT(environ, start_response): 
    108113    fid = environ['wsgiorg.routing_args'][1]['id'] 
    109114    data = environ['wsgi.input'].read(int(environ['CONTENT_LENGTH'])) 
     
    112117        return ['Posted data is malformed: ', data] 
    113118    else: 
    114         model.COLLECTION.set(fid, xml=data) 
     119        if environ['CONTENT_TYPE'] == 'application/atom+xml': 
     120            model.COLLECTION.set(fid, atom=data) 
     121        elif environ['CONTENT_TYPE'] == 'application/vnd.google-earth.kml+xml': 
     122            model.COLLECTION.set(fid, kml=data) 
    115123        url = envutil.request_url(environ) 
    116124        start_response("200 OK", [('Content-Type', 'text/plain')])