<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>David Fischer dot Name &#187; Development</title>
	<atom:link href="http://www.davidfischer.name/category/development/feed/" rel="self" type="application/rss+xml" />
	<link>http://www.davidfischer.name</link>
	<description>Some Things to Some People</description>
	<lastBuildDate>Thu, 02 Sep 2010 05:58:33 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.0.1</generator>
		<item>
		<title>Django and asynchronous jobs</title>
		<link>http://www.davidfischer.name/2010/09/django-and-asynchronous-jobs/</link>
		<comments>http://www.davidfischer.name/2010/09/django-and-asynchronous-jobs/#comments</comments>
		<pubDate>Thu, 02 Sep 2010 00:08:27 +0000</pubDate>
		<dc:creator>David</dc:creator>
				<category><![CDATA[Development]]></category>
		<category><![CDATA[celery]]></category>
		<category><![CDATA[django]]></category>
		<category><![CDATA[rabbitmq]]></category>

		<guid isPermaLink="false">http://www.davidfischer.name/?p=575</guid>
		<description><![CDATA[I&#8217;m going to focus on]]></description>
			<content:encoded><![CDATA[<p>I&#8217;m going to focus on <a href="http://www.rabbitmq.com/">RabbitMQ</a> &#038; <a href="http://celeryproject.org/">Celery</a>, what it buys you and why and when to use it. <a href="http://ericholscher.com/blog/2010/jun/23/large-problems-django-mostly-solved-delayed-execut/">Eric&#8217;s blog</a> always seems to be a couple months ahead of mine, so instead of re-hashing what he wrote, I&#8217;ll let you read his blog and then come back and finish my complimentary post. Go there now. I&#8217;ll wait.</p>
<p>Here&#8217;s the mini-rehash for those who didn&#8217;t actually go read it. Basically, RabbitMQ is a general purpose messaging queue system based on the <a href="http://en.wikipedia.org/wiki/Advanced_Message_Queuing_Protocol">AMQP</a> protocol. When you need something to get executed, you simply queue up a message with RabbitMQ. It won&#8217;t get lost if Rabbit restarts, you can be pretty confident that your tasks will eventually get executed or you&#8217;ll be able to find out why they didn&#8217;t. Celery (and django-celery) is a Python library for queuing up tasks to be executed and for actually executing these tasks asynchronously. It gets its tasks from RabbitMQ although it does work with other queues as well.</p>
<h5>Why RabbitMQ &#038; Celery?</h5>
<p>Not everyone needs to execute asynchronous tasks. If you&#8217;re reading this and asking yourself &#8220;why would I ever need that,&#8221; you probably don&#8217;t need it. My project involves data analysis that takes minutes for each item in the database. In addition, I periodically re-analyze items in the database. At first, I built a cronjob (using Django <a href="http://docs.djangoproject.com/en/1.2/howto/custom-management-commands/">management commands</a>) which would wake up and check for new items queued in the database every minute. Then, I built a second cronjob for re-analysis. As the system grew, this became hokey and had issues anytime the database crashed during this analysis. Anytime your Django view connects to some external service that may or may not be up (source control, web services) or may take a while (shell commands, massive database queries, etc.), it&#8217;s probably best done asynchronously. It&#8217;s also great for anything that needs to be executed periodically at a particular interval (caching, expensive calculations). Celery also provides built-in ways to retry failed tasks a specified number of times at a specified interval which is particularly useful for ensuring that things actually get done.</p>
<h5>Setup &#038; integration with Django</h5>
<p>My Django views didn&#8217;t change much after I switched to Celery. Instead of adding an item to the database when I want to queue up an item for analysis, I simply queue up the execution of a <span style="font-family: monospace">@task</span> with django-celery. The particular tasks that your application can handle are simply placed in a <span style="font-family: monospace">tasks.py</span> file in your Django applications. To process the tasks, you simply run <span style="font-family: monospace">python manage.py celeryd</span>. It can also be setup to run at boot time using init.d (<a href="http://github.com/ask/celery/blob/master/contrib/debian/init.d/celeryd">Ubuntu/Debian</a>, <a href="http://groups.google.com/group/celery-users/browse_thread/thread/149af195003f3e59">Redhat/Fedora</a>). Celeryd clients can connect to Rabbit from the same server as RabbitMQ or a different server or servers in order to distribute the load.</p>
<pre><code class="python">##old views.py
def queue_task(request, data):
    """
    A cronjob will poll for new queued items periodically and process them
    """
    item = NewQueueItem(data)
    item.save()

    render_to_response('success.html')

## new views.py
from tasks import process_item

def queue_task(request, data):
    process_item.delay(data)

    render_to_response('success.html')

## tasks.py
from celery.decorators import task

@task(max_retries=3, default_retry_delay=5*60)
def process_item(data, **kwargs):
    ...
</code></pre>
<h5>The nitty gritty</h5>
<p>One useful bit that I had to dig to find in the Celery documentation was on locking a particular task so that two different workers don&#8217;t work on the same thing at the same time. This could happen in my application if two users queued up analysis on the same item for example. The <a href="http://celeryq.org/docs/cookbook/tasks.html">trick</a> is to use Django&#8217;s cache framework and lock a particular item in the cache.</p>
<h5>A word on Djangocon</h5>
<p>Unlike last year, I convinced work to send me to <a href="http://djangocon.us/">Djangocon</a> in Portland next week. I&#8217;ll probably do some live blogging on some of the interesting topics. If you&#8217;ll also be there, say hi!</p>
]]></content:encoded>
			<wfw:commentRss>http://www.davidfischer.name/2010/09/django-and-asynchronous-jobs/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>Piston Looks Good, But I&#8217;m Not Using It</title>
		<link>http://www.davidfischer.name/2010/07/piston-looks-good-but-im-not-using-it/</link>
		<comments>http://www.davidfischer.name/2010/07/piston-looks-good-but-im-not-using-it/#comments</comments>
		<pubDate>Thu, 15 Jul 2010 21:01:31 +0000</pubDate>
		<dc:creator>David</dc:creator>
				<category><![CDATA[Development]]></category>
		<category><![CDATA[django]]></category>
		<category><![CDATA[piston]]></category>
		<category><![CDATA[rest]]></category>
		<category><![CDATA[webservices]]></category>

		<guid isPermaLink="false">http://www.davidfischer.name/?p=546</guid>
		<description><![CDATA[Firstly, I&#8217;ve been missing in]]></description>
			<content:encoded><![CDATA[<p>Firstly, I&#8217;ve been missing in action for a few months and I apologize to you, my loyal reader, for that. Without making excuses (here comes the excuses), work has been picking up, my girlfriend moved from about 15 miles away to only about 8 blocks away and <a href="http://starcraft2.com">Starcraft II</a> is in beta. Regardless, I&#8217;m back in the Python action. WoooHooo!</p>
<h5>REST interfaces &#038; Django</h5>
<p>This post is somewhat of a follow-up on my post on <a href="http://www.davidfischer.name/2009/07/restful-django-powered-web-services/">RESTful Django web services</a> because I didn&#8217;t really talk in my previous post about <a href="http://bitbucket.org/jespern/django-piston">Piston</a>. Piston (sometimes django-piston) is a library for creating RESTful services in Django and it supports some of the features that I spoke about in my previous post such as good caching support with Django&#8217;s cache framework, different output formats (eg. XML &#038; JSON) via what Piston calls emitters, and the ability but not the requirement to use Django models as REST resources. I don&#8217;t know how I missed Piston before, but people <a href="http://ericholscher.com/blog/2009/nov/11/large-problems-django-mostly-solved-rest-api/">blog</a> (*) about it and it has made the rounds on the <a href="http://groups.google.com/group/django-users/search?group=django-users&#038;q=piston">Django User&#8217;s</a> list. However, even after looking closely at it, I decided not to go with it. In this post I&#8217;m going to talk about what I did and did not like and why I rolled my own REST micro-framework. That almost sounds like I&#8217;m giving myself too much credit given that my micro-framework is only ~30 lines.</p>
<p>(*) BTW, Despite the fact that Eric updates his blog somewhat infrequently (sounds familiar) it is well worth a read.</p>
<h5>Piston: the good</h5>
<p>Piston ships with quite a bit of good documentation and allegedly is used to power some of BitBucket&#8217;s services &#8212; lending to its credibility. Specifically, I liked the fact that it plugged directly into Django models. You simply write a short <strong>Handler</strong> for your model explaining what fields to expose and you&#8217;re mostly done.</p>
<pre><code class="python">import re
from piston.handler import BaseHandler
from myapp.models import Blogpost

class BlogPostHandler(BaseHandler):
    allowed_methods = ('GET')
    fields = ('title', 'content', ('author', ('username', 'first_name')))
    exclude = ('id', re.compile(r'^private_'))
    model = Blogpost

    def read(self, request, post_slug):
        post = Blogpost.objects.get(slug=post_slug)
        return post
</code></pre>
<p>It effectively wraps up your handler and does all the JSON/XML/YAML serialization for you while still giving you the ability to customize it. On top of this, it plugs in nicely with Django&#8217;s <a href="http://docs.djangoproject.com/en/1.2/ref/forms/validation">form validation</a> and allows you to do some other nice features like throttling requests based on which user does it.</p>
<h5>Piston: the bad &#038; the ugly</h5>
<p>I started to look at Piston, but because I wasn&#8217;t using throttling, using OAuth, outputting anything other than JSON and I wasn&#8217;t tying to models I didn&#8217;t think that Piston bought me anything. In reality, it wasn&#8217;t doing anything my me other than properly returning <a href="http://docs.djangoproject.com/en/1.2/ref/request-response/#django.http.HttpResponseNotAllowed">HttpResponseNotAllowed</a>. My other issue is that this project involved different outputs based on HTTP headers. For example, a GET on a certain URL would return JSON formatted data (a read in the CRUD world) if an HTTP header was present and an HTML page presenting that data if it wasn&#8217;t. Piston uses different emitters based on a request parameter <em>format</em> (eg. <span style="font-family:monospace">/path/resource/?format=JSON</span>). Piston gets you up and running quickly, but it didn&#8217;t fit my use case.</p>
<p>Also, this is a little nitpicky, but when I see something like: </p>
<pre><code class="python">return rc.FORBIDDEN # returns HTTP 401</code></pre>
<p>I cringe a little bit considering that status code 403 is the correct status code for Forbidden. There&#8217;s a <a href="http://bitbucket.org/jespern/django-piston/issue/125/http-code-forbidden-and-unauthorized">ticket</a> for this already. Why did Piston define constants for returning various status codes anyway when that functionality is already <a href="http://docs.djangoproject.com/en/1.2/ref/request-response/#httpresponse-subclasses">built into</a> Django. Is <span style="font-family:monospace">rc.DELETED</span> so much easier than <span style="font-family:monospace">HttpResponse(status_code=204)</span>? Perhaps it&#8217;s a little clearer and Django really should have HttpResponse subclasses for even the less common responses, but I think this definitely involves repeating yourself (and Django&#8217;s mantra is don&#8217;t repeat yourself).</p>
<h5>The solution</h5>
<p>I always wondered why Django didn&#8217;t allow for routing URLs based on the HTTP method:  It seems like such a common use case. The developers <a href="http://groups.google.com/group/django-developers/browse_thread/thread/15056b2979180228/4f66ea0f5465fd7b">discussed</a> it back in 2006, but in the end it was decided that building only the simple case was best as it yielded a relatively clean <span style="font-family:monospace">urls.py</span>. Building off of that thread, the example in the <a href="http://www.djangobook.com/en/2.0/chapter08/">Django book</a> (search for &#8220;method_splitter&#8221;) and another <a href="http://watchitlater.com/blog/2009/06/django-and-multiple-methods-per-url-pattern/">blog post</a>, I rolled a little framework to meet my needs instead of using something like Piston.</p>
<pre><code class="python">## utils/dispatcher.py
from django.http import HttpResponseNotAllowed

# see rfc 2616 - http://www.ietf.org/rfc/rfc2616.txt s9.2 - s9.9
HTTP_METHODS = ('GET', 'POST', 'PUT', 'HEAD', 'TRACE', 'DELETE', 'OPTIONS', 'CONNECT')

def service_dispatcher(request, *args, **kwargs):
    """
    Routes requests to the correct view method based on the HTTP method
    """

    # loop over all possible HTTP methods and find the appropriate service
    allowed_methods = []
    appropriate_service = None
    for method in HTTP_METHODS:
        service_view = kwargs.pop(method, None)

        if service_view is not None:
            # store legal HTTP methods in case we need to return a 405
            allowed_methods.append(method)

            # found the correct service method
            if request.method == method:
                appropriate_service = service_view

    # if the correct service was found, call it
    # otherwise return a 405 - method not allowed - error
    if appropriate_service is not None:
        return appropriate_service(request, *args, **kwargs)
    else:
        return HttpResponseNotAllowed(allowed_methods)

## urls.py
from django.conf.urls.defaults import *
from myapp.utils.dispatcher import service_dispatcher
from myapp.blog import services

urlpatterns = patterns('',
    url(r'^/myapp/blog/$', service_dispatcher, {'GET': services.blog_get, 'POST': services.blog_post}),
)
</code></pre>
<p>I found this to be a much simpler and easily extensible. The argument against this is that <span style="font-family:monospace">urls.py</span> becomes bigger, but in a lot of ways I found this to be clearer. From reading the <span style="font-family:monospace">urlpatterns</span>, I can quickly tell exactly what gets called in each case. In addition, routing differently based on HTTP headers, cookies, the source or anything else becomes as simple as adding a parameter and a little code to <span style="font-family:monospace">service_dispatcher</span>.</p>
<p>In the end, it&#8217;s wasn&#8217;t that I didn&#8217;t like Piston, it&#8217;s just that I didn&#8217;t need it.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.davidfischer.name/2010/07/piston-looks-good-but-im-not-using-it/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Updates April 2010 Edition</title>
		<link>http://www.davidfischer.name/2010/04/updates-april-2010-edition/</link>
		<comments>http://www.davidfischer.name/2010/04/updates-april-2010-edition/#comments</comments>
		<pubDate>Sat, 17 Apr 2010 16:57:28 +0000</pubDate>
		<dc:creator>David</dc:creator>
				<category><![CDATA[Development]]></category>
		<category><![CDATA[authentication]]></category>
		<category><![CDATA[authorization]]></category>
		<category><![CDATA[django-pyodbc]]></category>
		<category><![CDATA[documentation]]></category>
		<category><![CDATA[mod_wsgi]]></category>
		<category><![CDATA[rpc4django]]></category>
		<category><![CDATA[sphinx]]></category>

		<guid isPermaLink="false">http://www.davidfischer.name/?p=503</guid>
		<description><![CDATA[Django tickets There&#8217;s been only]]></description>
			<content:encoded><![CDATA[<h5>Django tickets</h5>
<p>There&#8217;s been only a little movement on the ticket (<a href="http://code.djangoproject.com/ticket/13101">#13101</a>) I patched for 1.2. However, there&#8217;s been some new developments on the ticket (<a href="http://code.djangoproject.com/ticket/10809">#10809</a>) I patched regarding authentication with mod_wsgi. There&#8217;s been a suggestion to add group based authorization to Django&#8217;s mod_wsgi auth handler. There&#8217;s still some debate as to whether to use Django groups or Django permissions. </p>
<h5>django-pyodbc is dead?</h5>
<p>In a <a href="http://www.davidfischer.name/2010/03/updates-march-2010-edition/">previous post</a>, I talked about getting involved in <a href="http://code.google.com/p/django-pyodbc/">django-pyodbc</a> development. We are using django-pyodbc at work but the project is languishing a little bit. The project has never had a formal release, the documentation (other than source documentation) is a little light, and despite patches being submitted to get the code in shape for Django&#8217;s upcoming 1.2 release, nothing has been checked in by the developers. In fact, there&#8217;s been nothing on the project from the developers since January. I emailed the developers a few days ago offering to help and I haven&#8217;t heard anything back yet. I&#8217;d much rather keep the project together, but if I continue to get nothing I will probably branch the code line and begin development and maintenance. I&#8217;m not looking forward to having to find a Windows box on which to setup multiple versions of SQL Server but I&#8217;m hoping to be able to virtualize it.</p>
<p><strong>Edit (June 23, 2010):</strong> The developers have gotten involved again and I killed my fork of the project.</p>
<h5>RPC4Django updates</h5>
<p>I&#8217;m planning to put some effort into RPC4Django this weekend and make a release in the next week or two. The main features I&#8217;m looking at is the existing blueprint in Launchpad to <a href="https://blueprints.launchpad.net/rpc4django/+spec/handle-authentication">handle authentication</a> out of the box. Other than that, I got a little feedback on the HTTP access control functionality back in January that I need to test. I also plan to rip out the existing documentation and go to a <a href="http://sphinx.pocoo.org/">Sphinx</a> based system. We&#8217;ve been using Sphinx at work and I&#8217;ve been very impressed with its capabilities.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.davidfischer.name/2010/04/updates-april-2010-edition/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Why You Should Be Using Pip and Virtualenv</title>
		<link>http://www.davidfischer.name/2010/04/why-you-should-be-using-pip-and-virtualenv/</link>
		<comments>http://www.davidfischer.name/2010/04/why-you-should-be-using-pip-and-virtualenv/#comments</comments>
		<pubDate>Tue, 13 Apr 2010 07:00:35 +0000</pubDate>
		<dc:creator>David</dc:creator>
				<category><![CDATA[Development]]></category>
		<category><![CDATA[installation]]></category>
		<category><![CDATA[pip]]></category>
		<category><![CDATA[python]]></category>
		<category><![CDATA[virtualenv]]></category>

		<guid isPermaLink="false">http://www.davidfischer.name/?p=467</guid>
		<description><![CDATA[In a previous post, I]]></description>
			<content:encoded><![CDATA[<p>In a <a href="http://www.davidfischer.name/2010/01/extending-distutils-for-repeatable-builds/">previous post</a>, I promised to write about <a href="http://pip.openplans.org/">Pip</a> and <a href="http://virtualenv.openplans.org/">Virtualenv</a> and I&#8217;m now finally making good. Others have <a href="http://www.b-list.org/weblog/2008/dec/15/pip/">done</a> this <a href="http://www.saltycrane.com/blog/2009/05/notes-using-pip-and-virtualenv-django/">before</a>, but I think I have a little to add. If you develop a Python module and you don&#8217;t test it with virtualenv, don&#8217;t make your next release until you do.</p>
<h5>Configuring the environment</h5>
<p>Virtualenv creates a Python environment that is segregated from your system wide Python installation.  In this way, you can test your module without any external packages mucking up the result, add different versions of dependency packages and generally verify the exact set of requirements for your package.</p>
<p>To create the virtual environment:</p>
<pre>% virtualenv --no-site-packages testarea</pre>
<p>This creates a directory <span style="font-family: monospace">testarea/</span> that contains directories for installing modules and a Python executable. Using the virtual environment:</p>
<pre>% cd testarea
% source bin/activate</pre>
<p>Sourcing activate will set environment variables so that only modules installed under <span style="font-family: monospace">testarea/</span> are used. After setting up the environment, any desired packages can be installed (from <a href="http://pypi.python.org">pypi</a>):</p>
<pre>(testarea) % pip install rpc4django</pre>
<p>Packages can also be uninstalled, specific versions can be installed or packages can be installed from the file system, URLs or directly from source control:</p>
<pre>(testarea) % pip uninstall rpc4django
(testarea) % pip install rpc4django==0.1.6</pre>
<p>Pip is worth using over easy_install for its uninstall capabilities alone, but I should mention that pip is actively maintained while setuptools is mostly dead.</p>
<p>When you&#8217;re done with the virtual environment, simply deactivate it:</p>
<pre>(testarea) % deactivate</pre>
<h5>Do it for the tests</h5>
<p><a href="http://www.davidfischer.name/wp-content/uploads/2010/04/virtualenv_testing.png" rel="lightbox[467]"><img src="/wp-content/uploads/2010/04/virtualenv_testing-300x229.png" alt="Testing with virtualenv" title="Testing with virtualenv and pip" width="300" height="229" style="border: 1px solid black; margin: 5px; width: 300px; float: right;" /></a><br />
While the segregated environment that virtualenv provides is extremely well suited to getting the correct environment up and running, it is just as well suited to testing your application under a variety of different package configurations. With pip and virtualenv, testing your application under three different versions of Django is a snap and it doesn&#8217;t affect your system environment in the slightest. </p>
<h5>Dependencies made easy</h5>
<p>My favorite feature of pip is the ability to create a requirements file based on a set of packages installed in your virtual environment (or your global site-packages). Creating a requirements file can be done automatically using the <span style="font-family: monospace">freeze</span> command for pip:</p>
<pre>(testarea) % pip freeze > requirements.txt
(testarea) % more requirements.txt
Django==1.1.1
rpc4django==0.1.7
wsgiref==0.1.2</pre>
<p>Wsgiref will always appear in pip&#8217;s output. It is a <a href="http://docs.python.org/library/wsgiref.html">standard library</a> package that includes <a href="http://guide.python-distribute.org/installation.html#listing-installed-packages">package metadata</a>. The requirements file is used as follows:</p>
<pre>% pip install -r requirements.txt</pre>
<p>The requirements file can be version controlled both to aid in installation and to capture the exact versions of your dependencies directly where they are used rather than after the fact in documentation that can easily become out of date. The requirements file can be used to rebuild a virtual environment or to deploy a virtual environment into the machine&#8217;s site-packages. Pip and virtualenv are exceptionally easy to use and there&#8217;s really no excuse for a Python packager not to use them. </p>
<p><strong>Note:</strong> I&#8217;m working on a fairly large sized application for work. When it is finished, I will release a post-mortem that will also function as an update to my post about <a href="http://www.davidfischer.name/2009/07/packaging-and-sharing-django-applications/">packaging and distributing</a>.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.davidfischer.name/2010/04/why-you-should-be-using-pip-and-virtualenv/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>RPC4Django is Now Hosted in Launchpad</title>
		<link>http://www.davidfischer.name/2010/03/rpc4django-is-now-hosted-in-launchpad/</link>
		<comments>http://www.davidfischer.name/2010/03/rpc4django-is-now-hosted-in-launchpad/#comments</comments>
		<pubDate>Mon, 29 Mar 2010 06:05:30 +0000</pubDate>
		<dc:creator>David</dc:creator>
				<category><![CDATA[Development]]></category>
		<category><![CDATA[bazaar]]></category>
		<category><![CDATA[launchpad]]></category>
		<category><![CDATA[rpc4django]]></category>

		<guid isPermaLink="false">http://www.davidfischer.name/?p=446</guid>
		<description><![CDATA[After some discussion in my]]></description>
			<content:encoded><![CDATA[<p>After some discussion in my <a href="http://www.davidfischer.name/2010/03/updates-march-2010-edition/">last post</a>, I decided to host RPC4Django in <a href="https://code.launchpad.net/rpc4django/main">Launchpad</a>. Every release dating back to 0.1.0 is uploaded and hosted properly there. I also created a 0.1.8 milestone which I hope to work on in the next couple weeks. I tried to request a Launchpad import from subversion but it didn&#8217;t go smoothly. Launchpad isn&#8217;t really setup to handle imports from password protected subversion repositories to which the password doesn&#8217;t give full access. Regardless, all future releases will be from the publicly hosted Bazaar repo in Launchpad.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.davidfischer.name/2010/03/rpc4django-is-now-hosted-in-launchpad/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
	</channel>
</rss>
