<?xml version='1.0' encoding='UTF-8'?><?xml-stylesheet href="http://www.blogger.com/styles/atom.css" type="text/css"?><feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' xmlns:blogger='http://schemas.google.com/blogger/2008' xmlns:georss='http://www.georss.org/georss' xmlns:gd="http://schemas.google.com/g/2005" xmlns:thr='http://purl.org/syndication/thread/1.0'><id>tag:blogger.com,1999:blog-5235863953075762128</id><updated>2024-08-30T19:43:04.360+03:00</updated><category term="sqlalchemy"/><category term="XML"/><category term="db"/><category term="devconf"/><category term="dirty hack"/><category term="hg"/><category term="ip2cc"/><category term="web"/><category term="конференция"/><category term="DNS"/><category term="HTML"/><category term="NoSQL"/><category term="PIL"/><category term="Unicode"/><category term="datetime"/><category term="factory"/><category term="form"/><category term="list comprehension"/><category term="lock"/><category term="logging"/><category term="memcache"/><category term="mix-in"/><category term="mmap"/><category term="recursion"/><category term="shared memory"/><category term="ssh"/><category term="trolling"/><category term="собеседование"/><category term="шаблоны"/><title type='text'>Змееводство</title><subtitle type='html'></subtitle><link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='http://otkds.blogspot.com/feeds/posts/default'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default?redirect=false'/><link rel='alternate' type='text/html' href='http://otkds.blogspot.com/'/><link rel='hub' href='http://pubsubhubbub.appspot.com/'/><link rel='next' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default?start-index=26&amp;max-results=25&amp;redirect=false'/><author><name>Anonymous</name><uri>http://www.blogger.com/profile/11828750781940428293</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><generator version='7.00' uri='http://www.blogger.com'>Blogger</generator><openSearch:totalResults>27</openSearch:totalResults><openSearch:startIndex>1</openSearch:startIndex><openSearch:itemsPerPage>25</openSearch:itemsPerPage><entry><id>tag:blogger.com,1999:blog-5235863953075762128.post-9045213935178245834</id><published>2019-03-12T09:24:00.000+03:00</published><updated>2019-03-12T09:24:34.100+03:00</updated><title type='text'>Не самый очевидный del</title><content type='html'>&lt;div dir=&quot;ltr&quot; style=&quot;text-align: left;&quot; trbidi=&quot;on&quot;&gt;В Python 3 следующий код&lt;br /&gt;
&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;exc = None
try:
    assert False
except Exception as exc:
    pass
print(exc)&lt;/code&gt;&lt;/pre&gt;даёт ошибку &lt;code&gt;NameError: name &#39;exc&#39; is not defined&lt;/code&gt;. Это, конечно, здорово, что исключение, образующее циклические ссылки, очищается при выходе из &lt;code&gt;except&lt;/code&gt;. Не здорово, что при этом удаляется явно присвоенное значение.&lt;br /&gt;
&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://otkds.blogspot.com/feeds/9045213935178245834/comments/default' title='Комментарии к сообщению'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5235863953075762128/9045213935178245834' title='Комментарии: 0'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/9045213935178245834'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/9045213935178245834'/><link rel='alternate' type='text/html' href='http://otkds.blogspot.com/2019/03/del.html' title='Не самый очевидный &lt;code&gt;del&lt;/code&gt;'/><author><name>Anonymous</name><uri>http://www.blogger.com/profile/11828750781940428293</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5235863953075762128.post-8773321792686755256</id><published>2015-06-08T14:16:00.000+03:00</published><updated>2015-06-08T14:18:41.160+03:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="DNS"/><title type='text'>Wildcard DNS</title><content type='html'>&lt;div&gt;Памятка себе любимому, а то всё время что-нибудь забываю и заново ищу:&lt;br /&gt;
&lt;br /&gt;
&lt;ul&gt;&lt;li&gt;*.vcap.me → 127.0.0.1&lt;/li&gt;
&lt;li&gt;*.42foo.com → 127.0.0.1, ::1&lt;/li&gt;
&lt;li&gt;*.lvh.me → 127.0.0.1&lt;/li&gt;
&lt;li&gt;*.localhost.tv → 127.0.0.1, ::1&lt;/li&gt;
&lt;li&gt;*.lacolhost.com → 127.0.0.1&lt;/li&gt;
&lt;li&gt;*.&lt;var&gt;ip&lt;/var&gt;.xip.io → &lt;var&gt;ip&lt;/var&gt;&lt;/li&gt;
&lt;li&gt;*.&lt;var&gt;ip&lt;/var&gt;.nip.io → &lt;var&gt;ip&lt;/var&gt;﻿&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;&lt;br /&gt;
&lt;div&gt;Кто не в курсе, использования доменных имён по этим шаблонам позволяет вести разработку с виртуальными хостами (поддоменами) локально и делиться ссылками с коллегами без необходимости править (просить править) &lt;kbd&gt;/etc/hosts&lt;/kbd&gt; или настраивать dnsmasq.&lt;br /&gt;
&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://otkds.blogspot.com/feeds/8773321792686755256/comments/default' title='Комментарии к сообщению'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5235863953075762128/8773321792686755256' title='Комментарии: 1'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/8773321792686755256'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/8773321792686755256'/><link rel='alternate' type='text/html' href='http://otkds.blogspot.com/2015/06/wildcard-dns.html' title='Wildcard DNS'/><author><name>Anonymous</name><uri>http://www.blogger.com/profile/11828750781940428293</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5235863953075762128.post-4852017038424105635</id><published>2014-12-17T17:48:00.001+03:00</published><updated>2014-12-17T17:53:34.621+03:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="dirty hack"/><category scheme="http://www.blogger.com/atom/ns#" term="mix-in"/><category scheme="http://www.blogger.com/atom/ns#" term="sqlalchemy"/><title type='text'>Грязные технологии: миксины моделей SQLAlchemy</title><content type='html'>&lt;div&gt;SQLAlchemy позволяет выносить часть определения модели в отдельный базовый класс, который будет потом «подмешиваться» к другим. Очень удобно, когда есть какой-то повторяющийся кусок в большом количестве классов моделей. Но есть один неприятный момент: все поля должны «знать», к какой модели они принадлежат, а для этого нужно копировать объект поля. С первым уровнем SQLAlchemy хорошо справляется, так что с простым &lt;code&gt;Column&lt;/code&gt; всё путём, но ведь поля могут содержать ссылки на другие объекты (например, &lt;code&gt;ForeignKey&lt;/code&gt;), к которым предъявляются такие же требования. И тут авторы SQLAlchemy пошли самым простым путём: если нельзя сделать автоматически на все случаи жизни, то не пусть делает пользователь вручную. В результате код миксина должен выглядеть примерно так:&lt;br /&gt;
&lt;pre&gt;&lt;code&gt;class WithParent(object):
    @declared_attr
    def parent_id(cls):
        return Column(ForeignKey(Parent.id))
    @declared_attr
    def parent(cls):
        return relationship(Parent)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;По сути обработанные &lt;code&gt;declared_attr&lt;/code&gt; свойства решают ту же проблему, которую &lt;a href=&quot;http://otkds.blogspot.com/2014/12/return-locals.html&quot;&gt;мы уже решали декоратором &lt;code&gt;return_locals&lt;/code&gt;&lt;/a&gt;, а именно позволить выполнять код в определении класса несколько раз. Поэтому и решение напрашивается то же, только теперь нам нужно все свойства дополнительно завернуть в задекорированный метод. Понятно, что мы не хотим делать отдельный вызов нашей фабрики на каждый дескриптор, поэтому однажды полученный результат надо закешировать:&lt;br /&gt;
&lt;pre&gt;&lt;code&gt;def declared_mixin(*args):

    def wrapper(func):
        attrs = weakref.WeakKeyDictionary()
        def create_descriptor(name):
            def get_attr(cls):
                if cls not in attrs:
                    # Call func only once per class
                    attrs[cls] = return_locals(func)()
                return attrs[cls][name]
            get_attr.__name__ = name
            return declared_attr(get_attr)
        dict_ = {name: create_descriptor(name)
                 for name in func.func_code.co_varnames}
        dict_[&#39;__doc__&#39;] = func.__doc__
        return type(func.__name__, args, dict_)

    if len(args)==1 and not isinstance(args[0], type):
        # Short form (without args) is used
        func = args[0]
        args = ()
        return wrapper(func)
    else:
        return wrapper&lt;/code&gt;&lt;/pre&gt;Теперь наш пример миксина выглядит гораздо приятнее:&lt;br /&gt;
&lt;pre&gt;&lt;code&gt;@declared_mixin
def WithParent():
    parent_id = Column(ForeignKey(Parent.id))
    parent = relationship(Parent)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;В комментариях к прошлому посту Андрей Светлов &lt;a href=&quot;http://otkds.blogspot.com/2014/12/return-locals.html?showComment=1417952776236#c6243418209059317588&quot;&gt;резонно заметил&lt;/a&gt;, что хак слишком грязен для столь небольшого эффекта. В ситуации же с миксином полученный эффект уже больше: если в исходном варианте на одну смысловую строчку кода приходилось две строчки шума, то здесь мы от шума полностью избавились. И дело даже не в том, что строк стало меньше, зашумлённый код гораздо сложнее читать. Вопрос поиска менее грязных путей получения нужного результата остаётся открытым.&lt;br /&gt;
&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://otkds.blogspot.com/feeds/4852017038424105635/comments/default' title='Комментарии к сообщению'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5235863953075762128/4852017038424105635' title='Комментарии: 0'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/4852017038424105635'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/4852017038424105635'/><link rel='alternate' type='text/html' href='http://otkds.blogspot.com/2014/12/sqlalchemy-mixins.html' title='Грязные технологии: миксины моделей SQLAlchemy'/><author><name>Anonymous</name><uri>http://www.blogger.com/profile/11828750781940428293</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5235863953075762128.post-5169639806051097014</id><published>2014-12-03T20:32:00.000+03:00</published><updated>2014-12-17T17:55:43.966+03:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="dirty hack"/><category scheme="http://www.blogger.com/atom/ns#" term="factory"/><category scheme="http://www.blogger.com/atom/ns#" term="sqlalchemy"/><title type='text'>Грязные технологии: фабрика классов на основе функции</title><content type='html'>&lt;div&gt;Так уж получилось, что нам часто требуется определять одинаковые (или почти одинаковые) классы моделей SQLAlchemy для разных &lt;code&gt;MetaData&lt;/code&gt; ну и, соответственно, с разными базовыми классами. Декларативно. Повсеместный copy-paste быстро надоел. Были мысли создавать второй класс путём копирования, но уж больно сложно получается: не так просто определить, где на какой глубине остановиться, а где заменить ссылки на что-то уже отзеркалированное. Гораздо проще сделать фабрику и создавать столько классов, сколько нужно. В питоне ж это просто:&lt;br /&gt;
&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;def create_C(Base):
    class C(Base):
        id = Column(Integer, primary_key=True)
        # …
    return C&lt;/code&gt;&lt;/pre&gt;Всё хорошо, только вот отступ лишний появляется. Кому-о, может, и мелочь, а нам он сильно не понравился. Неужели нельзя без него? Ну типа класс задекорировать чем-о так, чтобы он в фабрику превратился. Проблема в том, что тело класса выполняется сразу и только один раз, и никакими декораторами это правило не отменить. Ну да, можно отменить: всего-то с байткодом чуть поколдовать. Только уж очень гразным хак получается и переносимость между версиями под большим вопросом. &lt;br /&gt;
&lt;/div&gt;&lt;div&gt;Зато вот у функции тело можно выполнять когда захочешь и сколько угодно раз. Почему бы этим не воспользоваться и не превратить декоратором функцию в класс?&lt;br /&gt;
&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;@create_class(Base)
def C():
    id = Column(Integer, primary_key=True)
    # …
    return locals()&lt;/code&gt;&lt;/pre&gt;Опять что-то лишнее, теперь &lt;code&gt;return locals()&lt;/code&gt;. А в реальных задачах у нас появятся аргументы у функции (пространство имён с другими моделями, например — нам же надо как-то внешние ссылки да связи определять), которыми не захочется пространство имён класса засорять, так что строчка ещё усложнится.&lt;br /&gt;
&lt;/div&gt;&lt;div&gt;Вот бы здорово было бы вернуть локально определённые переменные автоматически. И это как раз &lt;a href=&quot;http://code.activestate.com/recipes/410698/&quot;&gt;можно сделать&lt;/a&gt;. Немного оптимизации (зачем нам держать трейсер весь вызов, это же приличные накладные расходы?), немного уважения к тем, кто это будет потом отлаживать дебаггером или профилировать, и получается &lt;a href=&quot;https://github.com/SmartTeleMax/iktomi/blob/master/iktomi/unstable/utils/functools.py&quot;&gt;такой декоратор&lt;/a&gt;:&lt;br /&gt;
&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;import sys, functools, inspect


def return_locals(func):
    &#39;&#39;&#39;Modifies decorated function to return its locals&#39;&#39;&#39;

    @functools.wraps(func)
    def wrap(*args, **kwargs):
        frames = []

        def tracer(frame, event, arg):
            frames.append(frame)
            sys.settrace(old_tracer)
            if old_tracer is not None:
                return old_tracer(frame, event, arg)

        old_tracer = sys.gettrace()
        sys.settrace(tracer)
        try:
            func(*args, **kwargs)
        finally:
            sys.settrace(old_tracer)
        assert len(frames) == 1
        argspec = inspect.getargspec(func)
        argnames = list(argspec.args)
        if argspec.varargs is not None:
            argnames.append(argspec.varargs)
        if argspec.keywords is not None:
            argnames.append(argspec.keywords)
        return {name: value 
                for name, value in frames.pop(0).f_locals.items()
                if name not in argnames}

    return wrap&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;Осталась мелочь, создать декоратор самой фабрики:&lt;br /&gt;
&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;def create_class(*bases):
    def wrapper(func):
        return type(func.__name__, bases, return_locals(func)())
    return wrapper&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://otkds.blogspot.com/feeds/5169639806051097014/comments/default' title='Комментарии к сообщению'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5235863953075762128/5169639806051097014' title='Комментарии: 3'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/5169639806051097014'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/5169639806051097014'/><link rel='alternate' type='text/html' href='http://otkds.blogspot.com/2014/12/return-locals.html' title='Грязные технологии: фабрика классов на основе функции'/><author><name>Anonymous</name><uri>http://www.blogger.com/profile/11828750781940428293</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>3</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5235863953075762128.post-1575591471026745260</id><published>2014-05-12T19:43:00.002+04:00</published><updated>2014-05-12T19:45:41.475+04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="mmap"/><category scheme="http://www.blogger.com/atom/ns#" term="shared memory"/><title type='text'>Shared memory с помощью mmap</title><content type='html'>&lt;div&gt;Оказывается, можно использовать &lt;code&gt;mmap()&lt;/code&gt; для создания общей памяти между процессами. А если процессы порождаются с помощью &lt;code&gt;fork()&lt;/code&gt;, то в &lt;code&gt;mmap()&lt;/code&gt; можно передавать &lt;code&gt;-1&lt;/code&gt; в качестве дескриптора:&lt;br /&gt;
&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;import mmap
shmem = mmap.mmap(-1, &lt;var&gt;size&lt;/var&gt;, mmap.MAP_ANONYMOUS|mmap.MAP_SHARED,
                  mmap.PROT_READ|mmap.PROT_WRITE)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://otkds.blogspot.com/feeds/1575591471026745260/comments/default' title='Комментарии к сообщению'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5235863953075762128/1575591471026745260' title='Комментарии: 2'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/1575591471026745260'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/1575591471026745260'/><link rel='alternate' type='text/html' href='http://otkds.blogspot.com/2014/05/shared-memory-mmap.html' title='Shared memory с помощью mmap'/><author><name>Anonymous</name><uri>http://www.blogger.com/profile/11828750781940428293</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5235863953075762128.post-1782059616311175360</id><published>2014-01-23T18:47:00.000+04:00</published><updated>2014-01-23T19:02:04.097+04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="PIL"/><title type='text'>PIL: Конвертация RGBA в RGB</title><content type='html'>&lt;div&gt;Не первый раз сталкиваюсь с задачай и каждый раз ищу решение. В этот раз задокументирую:&lt;br /&gt;
&lt;br /&gt;
&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;from PIL import Image

def rgba_to_rgb(image, background=&#39;white&#39;):
    assert image.mode==&#39;RGBA&#39;
    bg_image = Image.new(&#39;RGB&#39;, image.size, background)
    bg_image.paste(image, mask=image.split()[3])
    return bg_image&lt;/code&gt;&lt;/pre&gt;&lt;br /&gt;
&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://otkds.blogspot.com/feeds/1782059616311175360/comments/default' title='Комментарии к сообщению'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5235863953075762128/1782059616311175360' title='Комментарии: 0'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/1782059616311175360'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/1782059616311175360'/><link rel='alternate' type='text/html' href='http://otkds.blogspot.com/2014/01/pil-rgba-rgb.html' title='PIL: Конвертация RGBA в RGB'/><author><name>Anonymous</name><uri>http://www.blogger.com/profile/11828750781940428293</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5235863953075762128.post-1592014513354506085</id><published>2012-05-03T19:33:00.000+04:00</published><updated>2012-05-03T19:43:06.582+04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="sqlalchemy"/><category scheme="http://www.blogger.com/atom/ns#" term="web"/><title type='text'>Automatic filtering in SQLAlchemy: motivation</title><content type='html'>&lt;div&gt;Server side code of web project usually has 3 layers:&lt;br /&gt;
&lt;br /&gt;
&lt;ul&gt;&lt;li&gt;data classes mapped to relational database,&lt;/li&gt;
&lt;li&gt;request handlers for each URL pattern,&lt;/li&gt;
&lt;li&gt;templates used to render pages.&lt;/li&gt;
&lt;/ul&gt;&lt;br /&gt;
Simple request handlers contain code like the following:&lt;br /&gt;
&lt;br /&gt;
&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;item = session.query(Entry).get(item_id)&lt;/code&gt;&lt;/pre&gt;or&lt;br /&gt;
&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;items = session.query(Entry)[:limit]&lt;/code&gt;&lt;/pre&gt;&lt;br /&gt;
When &lt;code&gt;Entry&lt;/code&gt; class has &lt;code&gt;public&lt;/code&gt; attribute and objects should be shown when &lt;code&gt;Entry.public &lt;/code&gt; is &lt;code&gt;True&lt;/code&gt; only (the simplest example of publicity condition; in real life it might be composite and even involve related tables) we have to include this condition in queries:&lt;br /&gt;
&lt;br /&gt;
&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;item = session.query(Entry).filter_by(public=True, id=item_id).scalar()&lt;/code&gt;&lt;/pre&gt;or&lt;br /&gt;
&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;items = session.query(Entry).filter_by(public=True)[:limit]&lt;/code&gt;&lt;/pre&gt;&lt;br /&gt;
Note, that we already violate DRY principle (the same condition should be used every time we query Entry), but it’s still not problem. Now let’s add relation to some Child class that has similar condition for publicity. If we pass only item or items to template, we have to be careful using their data:&lt;br /&gt;
&lt;br /&gt;
&lt;pre&gt;&lt;code class=&quot;jinja&quot;&gt;{% for child in item.children %}…{% endfor %}&lt;/code&gt;&lt;/pre&gt;must be replaced with&lt;br /&gt;
&lt;pre&gt;&lt;code class=&quot;jinja&quot;&gt;{% for child in item.children %}
{% if child.public %}…{% endif %}
{% endfor %}&lt;/code&gt;&lt;/pre&gt;&lt;br /&gt;
In real life it becomes even more complex: a simple test for empty list is already not so simple. Do we have other options? Yes, we can pass each relation as separate variable and move filtering to the code. This will prevent mess in templates, but this won’t prevent us from using relations directly by mistake. Do you think this shouldn’t happen? We are lazy, and I doubt anybody will define separate variable for relation that doesn’t have publicity condition (yet). But life changes and eventually we might need this condition. Now one developer adds new field to the database, changes all related request handlers and (if he is a responsible person) even templates. Simultaneously (or even later, since people remember code patterns they often used) other person adds usage of this relation unfiltered in some other place and we have unpublished data leaked to public. International scandal, world war III begins (joke).&lt;br /&gt;
&lt;/div&gt;&lt;br /&gt;
&lt;div&gt;In fact, templates developer shouldn’t care about publicity of data. Unpublished data must not reach templates.&lt;br /&gt;
Constructing some data structures specially for templates leads to verbose request handler code instead for concise single line:&lt;br /&gt;
&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;item = session.query(Entry).filter_by(public=True, id=item_id).scalar()
data = {‘id’: item.id,
        ‘title’: item.title,
        ‘date’: item.date,
        ‘body’: item.body}
data[‘children’] = children = []
for child in item.children:
    if not child.public:
        continue
    child_data = {‘id’: child.id,
                  ‘title’: child.title,
                  ‘data’: child.data,
                  ‘body’: child.body}
    if child.author and child.author.public:
        child_data[‘author’] = author = {‘id’: child.author.id,
                                         ‘name’: child.author.name}
        if child.author.company and child.author.company.public:
            author[‘company’] = {‘id’: child.author.company.id,
                                 ‘title’: child.author.company.title}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;br /&gt;
&lt;div&gt;Here is statistics from one big project where I’m involved in development. The numbers below cover public segment only (internal services like editor interface are not included).&lt;br /&gt;
&lt;br /&gt;
&lt;ul&gt;&lt;li&gt;458 templates&lt;/li&gt;
&lt;li&gt;6 databases with 210 tables&lt;/li&gt;
&lt;li&gt;135 mapped classes, 5 of them are bases for inheritance trees&lt;/li&gt;
&lt;li&gt;Data for 63 mapped classes must not go to public unless some condition is met (15 of them indirectly through inheritance). Those are only conditions that can’t be applied when replicating data from internal segment to public without significant impact on performance (changing state field of parent object would trigger publication or deletion of a huge list of children; using publication time in future requires some scheduler to trigger publication), the rest is filtered out before reaching database for public sites.&lt;/li&gt;
&lt;/ul&gt;&lt;br /&gt;
&lt;/div&gt;&lt;br /&gt;
&lt;div&gt;Having we can’t change relations behavior in request handler (this breaks ORM’s single object for each identity rule) I see the following 2 ways to solve the problem:&lt;br /&gt;
&lt;br /&gt;
&lt;ul&gt;&lt;li&gt;define separate mapped classes for public site,&lt;/li&gt;
&lt;li&gt;instruct session to filter all ORM queries.&lt;/li&gt;
&lt;/ul&gt;&lt;br /&gt;
Both ways have problems and require separate analysis.&lt;br /&gt;
&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://otkds.blogspot.com/feeds/1592014513354506085/comments/default' title='Комментарии к сообщению'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5235863953075762128/1592014513354506085' title='Комментарии: 2'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/1592014513354506085'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/1592014513354506085'/><link rel='alternate' type='text/html' href='http://otkds.blogspot.com/2012/05/automatic-filtering-in-sqlalchemy.html' title='Automatic filtering in SQLAlchemy: motivation'/><author><name>Anonymous</name><uri>http://www.blogger.com/profile/11828750781940428293</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5235863953075762128.post-5210067075777557637</id><published>2012-03-26T19:08:00.002+04:00</published><updated>2012-03-26T19:10:30.743+04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="trolling"/><title type='text'>Перестать писать классы?</title><content type='html'>&lt;div dir=&quot;ltr&quot; style=&quot;text-align: left;&quot; trbidi=&quot;on&quot;&gt;
Jack Diederich на конференции PyCon US 2012 сделал замечательный доклад &lt;a href=&quot;http://pyvideo.org/video/880/stop-writing-classes&quot;&gt;Stop Writing Classes&lt;/a&gt;&amp;nbsp;и добрые люди даже &lt;a href=&quot;http://habrahabr.ru/post/140581/&quot;&gt;перевели его на русский язык&lt;/a&gt;. Тема очень правильная, но к этому докладу (как, впрочем, и любым другим провокационным заявлениям) очень недостаёт эпиграфом известной японской поговорки:&lt;br /&gt;
&lt;blockquote class=&quot;tr_bq&quot;&gt;
&lt;span style=&quot;font-size: small;&quot;&gt;&lt;span style=&quot;color: #454545;&quot;&gt;&lt;span style=&quot;font-family: &#39;Times New Roman&#39;, Times, serif;&quot;&gt;If you believe everything you read, better not read.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/blockquote&gt;
Больше всего режет глаза отказ от собственных исключений. А ведь в этом случае нам при использовании придётся перехватывать стандартные generic исключения, для возникновения которых может быть куча причин. Если во всех случаях исключение означает нештатную ситуацию (не перехватывается), то всё нормально. А если нет? Тогда вполне вероятна ситуация, когда мы думаем, что обрабатываем ошибку времени выполнения, в то время как на самом деле где-то в коде затесалась ошибка в логике, проявляющаяся на определённых данных, и потратить время на разгадывание загадок при отладке.&lt;br /&gt;
&lt;br /&gt;
Теперь посмотрим на пример с классом для API. Избавились от громоздоко класса — это хорошо. Но теперь конфигурационный параметр API_KEY стал глобальной переменной, неявно используемой в функции. Implicit is better than explicit? Если это всё находится в моём небольшом скрипте, то всё замечательно. А если код запроса в сторонней библиотеке, а API_KEY нужно читать из конфигурационного файла?&lt;br /&gt;
&lt;br /&gt;
И так можно продолжать со всеми остальными примерами. Урощение, в том числе и избавление от ненужных классов — это хорошо, но только нужно смотреть, насколько оно применимо в каждом конкретном случае.&lt;br /&gt;
&lt;div id=&quot;-chrome-auto-translate-plugin-dialog&quot; style=&quot;background-attachment: initial !important; background-clip: initial !important; background-color: transparent !important; background-image: initial !important; background-origin: initial !important; background-position: initial initial !important; background-repeat: initial initial !important; display: none; left: 0px; margin-bottom: 0px !important; margin-left: 0px !important; margin-right: 0px !important; margin-top: 0px !important; opacity: 1 !important; overflow-x: visible !important; overflow-y: visible !important; padding-bottom: 0px !important; padding-left: 0px !important; padding-right: 0px !important; padding-top: 0px !important; position: absolute !important; text-align: left !important; top: 0px; z-index: 999999 !important;&quot;&gt;
&lt;div style=&quot;-webkit-border-radius: 10px !important; background-color: #363636 !important; background-image: -webkit-gradient(linear, left top, right bottom, color-stop(0%, #000), color-stop(50%, #363636), color-stop(100%, #000)); border-color: #000000 !important; border-width: 0px !important; color: #fafafa !important; font-size: 16px !important; max-width: 300px !important; opacity: 0.8 !important; overflow: visible !important; padding: 8px !important; text-align: left !important; z-index: 999999 !important;&quot;&gt;
&lt;div class=&quot;translate&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;additional&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;img onclick=&quot;document.location.href=&#39;http://translate.google.com/&#39;;&quot; src=&quot;http://www.google.com/uds/css/small-logo.png&quot; style=&quot;-webkit-border-radius: 20px; background-color: rgba(200, 200, 200, 0.3) !important; cursor: pointer !important; margin: 0 !important; padding: 3px 5px 0 !important; position: absolute !important; right: 1px !important; top: -20px !important; z-index: -1 !important;&quot; /&gt;&lt;/div&gt;
&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://otkds.blogspot.com/feeds/5210067075777557637/comments/default' title='Комментарии к сообщению'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5235863953075762128/5210067075777557637' title='Комментарии: 2'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/5210067075777557637'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/5210067075777557637'/><link rel='alternate' type='text/html' href='http://otkds.blogspot.com/2012/03/jack-diederich-pycon-us-2012-stop.html' title='Перестать писать классы?'/><author><name>Anonymous</name><uri>http://www.blogger.com/profile/11828750781940428293</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5235863953075762128.post-8977284181073040112</id><published>2010-05-21T16:12:00.011+04:00</published><updated>2010-06-18T15:50:29.004+04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="devconf"/><category scheme="http://www.blogger.com/atom/ns#" term="конференция"/><title type='text'>Впечатления от DevConf::Python()</title><content type='html'>&lt;div&gt;Перед конференцией &lt;a href=&quot;http://webnewage.org/&quot;&gt;Александр Кошелев&lt;/a&gt;, организатор питонячьей секции, взял интервью у некоторых докладчиков:

&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;http://devconf.ru/python/news/detail/50&quot;&gt;Иван Сагалаев: &quot;Python — это просто очень хороший язык&quot;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://devconf.ru/python/news/detail/51&quot;&gt;Андрей Попп: &quot;После докладов мы можем подискуссировать&quot;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://devconf.ru/python/news/detail/54&quot;&gt;Андрей Власовских: &quot;Python — очень практичный язык&quot;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://devconf.ru/python/news/detail/59&quot;&gt;Александр Шигин: &quot;Cython — &quot;внебрачный ребенок&quot; C и Python&#39;а&quot;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://devconf.ru/python/news/detail/61&quot;&gt;Александр Соловьев: &quot;Мне захотелось иметь Джангу лучше, чем сама Джанга&quot;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://devconf.ru/python/news/detail/62&quot;&gt;Артём Семенов и Виктор Коцеруба: &quot;Приходите к нам на django hate party!&quot;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;/div&gt;

&lt;h4&gt;Впечатления о докладах&lt;/h4&gt;

&lt;div&gt;Андрей Попп рассказал &lt;a href=&quot;http://www.slideshare.net/andreypopp/report-4114331&quot;&gt;про gevent&lt;/a&gt;. Мотивационная часть абсолютно правильная, а вот решение выглядит как хак. Я бы не рискнул использовать gevent, не покопавшись основательно внутри: для подобных хаков важно понимать, как они устроены, чтобы представлять, где они подведут.&lt;/div&gt;

&lt;div&gt;Андрей Светлов &lt;a href=&quot;http://www.slideshare.net/otkds/ss-4201809&quot;&gt;разложил по полочкам импорт в питоне&lt;/a&gt;. Доклад ориентирован в основном на новичков, но и для продвинутых пользователей было немало интересного.&lt;/div&gt;

&lt;div&gt;Андрей Власовских &lt;a href=&quot;http://www.slideshare.net/vlasovskikh/devconf-ru-funcparserlib&quot;&gt;рассказал про свою библиотеку funcparserlib&lt;/a&gt;. Андрей великолепный докладчик и библиотека интригует простотой использования не в ущерб возможностям. Надеюсь в скором времени её опробовать на реальных задачах.&lt;/div&gt;

&lt;div&gt;Мастер-класс Ивана Сагалаева &lt;a href=&quot;http://softwaremaniacs.org/blog/2010/05/19/elementflow/&quot;&gt;о потоковой генерации XML&lt;/a&gt; не впечатлил. Слишком уж сильно он был ориентирован на новичков, а у меня, вероятно, были завышенные ожидания. Однако начинающие программисты могли на практике увидеть правильный процесс создания продукта &amp;mdash; довольно редкая возможность.&lt;/div&gt;

&lt;div&gt;Ещё один доклад Андрей Попп посвятил &lt;a href=&quot;http://www.slideshare.net/andreypopp/web-repozebfg&quot;&gt;созданию web-приложений на repoze.bfg&lt;/a&gt;. Доклад интересный, а вот сам repoze.bfg не впечатлил: не нравится мне enterprise стиль.&lt;/div&gt;

&lt;div&gt;Виктор Коцеруба при поддержке Артема Семенова долго критиковал django. В основном по делу, но как-то очень сдержанно. Я ожидал от Виктора более провокационного изложения. По ходу доклада не все присутствующие соглашались со всеми доводами, много дискутировали.&lt;/div&gt;

&lt;div&gt;В догонку &lt;a href=&quot;http://piranha.org.ua/&quot;&gt;Александр Соловьев&lt;/a&gt; мастерски &lt;a href=&quot;http://www.scribd.com/doc/31601818/devconf-svarga&quot;&gt;представил альтернативу django &amp;mdash; Svarga&lt;/a&gt;. Думаю, этот доклад заслуживает отдельного поста.&lt;/div&gt;

&lt;div&gt;Во второй день Тимофей Первезенцев очень подробно и аргументированно изложил, &lt;a href=&quot;http://www.slideshare.net/otkds/python-4205056&quot;&gt;что требуется от шаблонизаторов, и какие есть проблемы у имеющихся библиотек&lt;/a&gt;. Доклад вызвал много обсуждений и сильно затянулся. В результате Александру Соловьеву осталось довольно мало времени на его мастер-класс про API mercurial. Однако Александр не только уложился по времени, но и очень подробно и доходчиво показал, как с ним можно работать.&lt;/div&gt;

&lt;h4&gt;Общие впечатления&lt;/h4&gt;

&lt;div&gt;К организации конференции можно сделать много замечаний, но это всё не существенно. Рядом с залом стоял флип-чарт, которым мы активно пользовались для дополнительных обсуждений во время перерывов. Отличная возможность, но я бы всё-таки предпочёл видеть маркерную доску, с которой можно стирать написанное: это и более удобно для исправлений, и меньше психологический барьер начать использовать (в других секциях флипы, по-моему, так и не начали использовать). В кофебрейк, обед и за вечерним спонсорским пивом сдвигались столы и большая часть питонеров продолжала обсуждение. В общем, много знакомств, море общения, куча новый идей!&lt;/div&gt;

&lt;div&gt;И напоследок &lt;a href=&quot;http://fotki.yandex.ru/users/riffm/album/97237/&quot;&gt;фотографии с коференции&lt;/a&gt;, выложенные Тимофеем. Многие участники на них подписаны.&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://otkds.blogspot.com/feeds/8977284181073040112/comments/default' title='Комментарии к сообщению'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5235863953075762128/8977284181073040112' title='Комментарии: 3'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/8977284181073040112'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/8977284181073040112'/><link rel='alternate' type='text/html' href='http://otkds.blogspot.com/2010/05/devconfpython.html' title='Впечатления от DevConf::Python()'/><author><name>Anonymous</name><uri>http://www.blogger.com/profile/11828750781940428293</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>3</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5235863953075762128.post-1129690309935274051</id><published>2010-05-06T13:41:00.007+04:00</published><updated>2010-05-06T14:48:27.207+04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="devconf"/><category scheme="http://www.blogger.com/atom/ns#" term="конференция"/><title type='text'>DevConf::Python() 2010 17 мая в Москве</title><content type='html'>&lt;div&gt;В Москве 17-18 мая пройдёт &lt;a href=&quot;http://devconf.ru/&quot;&gt;DevConf 2010&lt;/a&gt; &amp;mdash; конференция веб-разработчиков с отдельным потоком, посвящённым Python. По-моему это первое мероприятие по питону в Москве такого масштаба.&lt;/div&gt;

&lt;div&gt;Предварительный список докладов 17 мая:
&lt;ul&gt;
&lt;li&gt;Потоковая генерация XML, Иван Сагалаев&lt;/li&gt;
&lt;li&gt;Разработка cетевых приложений с gevent, Андрей Попп&lt;/li&gt;
&lt;li&gt;Расширение механизма импорта в Питоне, Андрей Светлов&lt;/li&gt;
&lt;li&gt;Внешние языки DSL на funcparserlib, Андрей Власовских&lt;/li&gt;
&lt;li&gt;PyCharm: новая IDE для Python от JetBrains, Дмитрий Жемеров&lt;/li&gt;
&lt;li&gt;Python и Cython, Александр Шигин&lt;/li&gt;
&lt;li&gt;Разработка web-приложений с repoze.bfg, Андрей Попп&lt;/li&gt;
&lt;li&gt;Django ±, Артем Семенов и Виктор Коцеруба&lt;/li&gt;
&lt;li&gt;Разумная альтернатива Django, Александр Соловьев&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;

&lt;div&gt;
Кроме того, 18 мая пройдёт весьма интересный мастер-класс Александра Соловьева &quot;Свой gist.github.com на Mercurial&quot;.
&lt;/div&gt;

&lt;div&gt;
Актуальная информация о программе секции доступна сайте &lt;a href=&quot;http://devconf.ru/python/page/programm/&quot;&gt;DevConf 2010&lt;/a&gt;.
&lt;/div&gt;

&lt;div&gt;Ещё одна очень интересная фишка конференции &amp;mdash; флип-чарты, обсуждения за маркерной доской в перерывах между докладами. Любой из слушателей может перехватить инициативу и рассказать что-то интересное по заявленной теме. Это потрясающая возможность не просто сделать доклад, а провести полноценное обсуждение интересной темы! Доклад в такой форме не требует тщательной подготовки. Ну и ещё один плюс &amp;mdash; для ведущих флип-чартов, попавших в программу, вход на конференцию бесплатный.&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://otkds.blogspot.com/feeds/1129690309935274051/comments/default' title='Комментарии к сообщению'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5235863953075762128/1129690309935274051' title='Комментарии: 2'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/1129690309935274051'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/1129690309935274051'/><link rel='alternate' type='text/html' href='http://otkds.blogspot.com/2010/05/devconfpython-2010-17.html' title='DevConf::Python() 2010 17 мая в Москве'/><author><name>Anonymous</name><uri>http://www.blogger.com/profile/11828750781940428293</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5235863953075762128.post-6590094548661550036</id><published>2010-05-04T20:21:00.007+04:00</published><updated>2010-05-24T16:15:24.915+04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="lock"/><category scheme="http://www.blogger.com/atom/ns#" term="memcache"/><category scheme="http://www.blogger.com/atom/ns#" term="NoSQL"/><category scheme="http://www.blogger.com/atom/ns#" term="web"/><title type='text'>Блокировка объектов при редактировании в админке</title><content type='html'>&lt;div&gt;Одна из недавних встреч питонеров (Moscow Python meetup) была посвещена теме NoSQL. Я отношу скептически к повсеместному переходу на NoSQL, но всё же нахожу ему применение в отдельных задачах. Так, на встрече я рассказал про блокирование редактируемых объектов на базе memcache. Проблема вполне типичная для всех редакторских интерфейсов в CMS. Один и тот же объект могут одновременно начать редактировать несколько пользователей, в этом случае правки одного из пользователей перетираются другим. Более того, иногда возникают ситуации, когда у одного пользователя оказываются открыты несколько окон редактирования одного объекта и он перетирает собственные изменения.&lt;/div&gt;

&lt;div&gt;В моём варианте решения при открытии страницы редактирования берётся блокировка (если объект ещё не заблокирован), а затем со страницы периодически шлётся AJAX запрос на её обновление. Ответ может быть как успешный, так и нет, если другой редактор насильно перехватил блокировку. При завершении редактирования или уходе со страницы блокировка снимается. Кроме того, если блокировку не обновлять, то она через некоторое время протухает автоматически &amp;mdash; это решает проблему снятия блокировки, хоть и с запозданием, при закрытии окна (падении браузера, выключении питания у компьютера и т.д.). Переменная &lt;code&gt;edit_session&lt;/code&gt; необходима для решения проблемы нескольких открытых окон редактирования одного объекта, фактически она содержит идентификатор одного такого окна. Для обновления блокировки используются команды memcache &lt;code&gt;gets&lt;/code&gt; и &lt;code&gt;cas&lt;/code&gt;, чтобы обеспечить атомарность операций (исключить условие гонки). Ниже приведена серверная часть, слегка переписанная, чтобы оторвать от контекста нашего движка (функции и переменные сделаны глобальными).

&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;import os, logging
from time import time

logger = logging.getLogger(__name__)

class LockError(Exception):
    def __str__(self):
        return &#39;Problems with object lock&#39;

class LockedByOther(LockError):
    def __init__(self, user):
        LockError.__init__(self, user)
        self.user = user
    def __str__(self):
        return &#39;Object is already locked by user: %s (%s)&#39; % \
                                (self.user.name, self.user.login)

class LockIsLost(LockError):
    def __str__(self):
        return &#39;The object lock is lost&#39;

def create_lock(obj_key, user, force=False):
    &#39;&#39;&#39;Marks model object as editted. Returns edit session on success or
    raises exception. obj_key is global identifier of object. When force is
    True the current lock is ignored.&#39;&#39;&#39;
    CACHE.clear_cas()
    edit_session = os.urandom(5).encode(&#39;hex&#39;)
    value = dict(edit_session=edit_session,
                 user_id=user.id,
                 time=time())
    if force:
        if CACHE.set(obj_key, value, time=MODEL_LOCK_TIMEOUT):
            return edit_session
        else:
            raise LockError()
    for i in range(3):
        if CACHE.add(obj_key, value, time=MODEL_LOCK_TIMEOUT):
            return edit_session
        old_value = CACHE.gets(obj_key)
        if old_value is None:
            # Should try add() again
            continue
        if not old_value or time()-old_value[&#39;time&#39;] &gt; MODEL_LOCK_TIMEOUT:
            # Somebody&#39;s lock is already expired
            if CACHE.cas(obj_key, value, time=MODEL_LOCK_TIMEOUT):
                return edit_session
            else:
                continue
        # Somebody holds active lock, no farther attempts
        break
    else:
        logger.error(&#39;Failed to lock model object. Problem with memcached?&#39;)
        raise LockError()
    lock_user = get_user(id=old_value[&#39;user_id&#39;])
    assert lock_user is not None
    raise LockedByOther(lock_user)

def update_lock(obj_key, user, edit_session):
    &#39;&#39;&#39;Updates model object lock as being active. Raises exception on
    error. obj_key is global identifier of object.&#39;&#39;&#39;
    CACHE.clear_cas()
    for i in range(3):
        old_value = CACHE.gets(obj_key)
        if not old_value:
            raise LockIsLost()
        elif old_value[&#39;edit_session&#39;]!=edit_session:
            lock_user = get_user(id=old_value[&#39;user_id&#39;])
            assert lock_user is not None
            raise LockedByOther(lock_user)
        new_value = dict(edit_session=edit_session,
                         user_id=user.id,
                         time=time())
        if CACHE.cas(obj_key, new_value, time=MODEL_LOCK_TIMEOUT):
            return
    else:
        # No runtime error here since we want to give user a chance to
        # restore lock.
        raise LockIsLost()

def remove_lock(obj_key, edit_session):
    &#39;&#39;&#39;Removes lock for model object. obj_key is global identifier of
    object.&#39;&#39;&#39;
    CACHE.clear_cas()
    # We can&#39;t garuantee memcache&#39;s delete method will remove only our
    # lock, so we update the record with empty value and minimal (1 sec)
    # timeout.
    old_value = CACHE.gets(obj_key)
    # It&#39;s too late to do something in case of error, so we just ignore
    # returned value.
    if old_value and old_value[&#39;edit_session&#39;]==edit_session:
        CACHE.cas(obj_key, &#39;&#39;, time=1)&lt;/code&gt;&lt;/pre&gt;

Надеюсь, назначение и работа методов понятна из названий и комментариев.
&lt;/div&gt;

&lt;div&gt;А какие способы используете вы для организации одновременного редактирования объектов?&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://otkds.blogspot.com/feeds/6590094548661550036/comments/default' title='Комментарии к сообщению'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5235863953075762128/6590094548661550036' title='Комментарии: 2'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/6590094548661550036'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/6590094548661550036'/><link rel='alternate' type='text/html' href='http://otkds.blogspot.com/2010/05/blog-post.html' title='Блокировка объектов при редактировании в админке'/><author><name>Anonymous</name><uri>http://www.blogger.com/profile/11828750781940428293</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5235863953075762128.post-8983936863544985</id><published>2009-12-04T20:07:00.006+03:00</published><updated>2009-12-04T20:16:38.052+03:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="sqlalchemy"/><title type='text'>Автоматическая фильтрация публичных данных в SQLAlchemy</title><content type='html'>&lt;div&gt;В CMS в таблице для (почти) каждой сущности обычно добавляется поле-флаг, определяющее, должна ли данная сущность показываться на сайте. В коде сайта, соответвенно, необходимо не забывать добавлять соответствующее условие в каждый запрос. При использовании ORM мы автоматичеси получаем связанные сущности, для которых запрос генерируется автоматически. Это удобно, но теперь нам ещё нужно проверять, нужно ли показывать каждый из связанных объектов. Есть ещё множество ситуаций, когда такие проверки или добавление дополнительных условий также необходимы. Шансы, что в большом проекте где-то об этом забудут, близки к 100%. Поэтому очень желательно процесс фильтрации непубличных данных автоматизировать. В django для этих целей используют специально написанный менеджер. В древней библиотеки &lt;a href=&quot;http://ppa.sf.net/#qps&quot;&gt;QPS&lt;/a&gt; с некоторым подобием ORM сделано даже лучше: можно для разных тегов выборки определить разные правила формирования запроса и даже правила переноса тегов на связанные обекты.&lt;/div&gt;

&lt;div&gt;Как же быть с решением проблемы автоматической фильтрации в SQLAlchemy? Существует возможность при создании сессии подставить свой конструктор запроса через атрибут &lt;code&gt;query_cls&lt;/code&gt; в &lt;code&gt;sessionmaker&lt;/code&gt;. Но предлагаемые в архивах &lt;a href=&quot;http://groups.google.ru/group/sqlalchemy&quot;&gt;Google-группы sqlalchemy&lt;/a&gt; &lt;a href=&quot;http://groups.google.com/group/sqlalchemy/browse_thread/thread/bcd10e4a2f5c603d/7dc1515973e5c7ba&quot;&gt;решения&lt;/a&gt; уже не работают, так как метод &lt;code&gt;Query.get()&lt;/code&gt; теперь не предназначен для объектов с условием. Я написал свою реализацию метода, без этого ограничения. Вот что получилось в результате:

&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;class HackedQuery(Query):

    def get(self, ident):
        # Use default implementation when there is no condition
        if not self._criterion:
            return Query.get(self, ident)
        # Copied from Query implementation with some changes.
        if hasattr(ident, &#39;__composite_values__&#39;):
            ident = ident.__composite_values__()
        mapper = self._only_mapper_zero(
                    &quot;get() can only be used against a single mapped class.&quot;)
        key = mapper.identity_key_from_primary_key(ident)
        if ident is None:
            if key is not None:
                ident = key[1]
        else:
            from sqlalchemy import util
            ident = util.to_list(ident)
        if ident is not None:
            columns = list(mapper.primary_key)
            if len(columns)!=len(ident):
                raise TypeError(&quot;Number of values doen&#39;t match number &quot;
                                &#39;of columns in primary key&#39;)
            params = {}
            for column, value in zip(columns, ident):
                params[column.key] = value
            return self.filter_by(**params).first()


def QueryPublic(entities, session=None):
    # It&#39;s not derectly related to the problem, but is useful too.
    query = HackedQuery(entities, session).with_polymorphic(&#39;*&#39;)
    # I haven&#39;t ever seen examples with several entities, so I can test
    # this case.
    assert len(entities)==1, entities
    cls = _class_to_mapper(entities[0]).class_
    public_condition = getattr(cls, &#39;public_condition&#39;, None)
    if public_condition is not None:
        query = query.filter(public_condition)
    return query&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://otkds.blogspot.com/feeds/8983936863544985/comments/default' title='Комментарии к сообщению'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5235863953075762128/8983936863544985' title='Комментарии: 2'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/8983936863544985'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/8983936863544985'/><link rel='alternate' type='text/html' href='http://otkds.blogspot.com/2009/12/sqlalchemy.html' title='Автоматическая фильтрация публичных данных в SQLAlchemy'/><author><name>Anonymous</name><uri>http://www.blogger.com/profile/11828750781940428293</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5235863953075762128.post-8119830491758368427</id><published>2009-10-12T17:53:00.008+04:00</published><updated>2009-11-09T16:31:53.812+03:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Unicode"/><category scheme="http://www.blogger.com/atom/ns#" term="шаблоны"/><title type='text'>Шаблонизатор в Tornado и unicode</title><content type='html'>&lt;div&gt;Шаблонизаторы, основанные на преобразовании кода шаблона в промежуточный питоновский код с последующей его компиляцией, как правило, отличаются простотой реализации и высокой скоростью выполнения. &lt;a href=&quot;http://github.com/facebook/tornado&quot;&gt;Tornado&lt;/a&gt; не исключение. Однако при использовании такого подхода возникает проблема со строками. Как бы меня не уверяли некоторые коллеги, я не верю, что верстальщику будет приятно писать в шаблоне строки в виде &lt;code&gt;u&#39;...&#39;&lt;/code&gt;. Но если этого не делать, то придётся работать с 8-битными строками в некоторой кодировке, как правило UTF-8, со всеми вытекающими последствиями. Одного &lt;code&gt;len()&lt;/code&gt; вполне достаточно. Кроме того, не хотелось бы иметь жёстко зафиксированную кодировку, пока возникает необходимость взаимодествия с сервисами компаний (вроде &lt;a href=&quot;http://yandex.ru/&quot;&gt;Яndex&lt;/a&gt;), не подозревающих о наличие других кодировок, кроме windows-1251.&lt;/div&gt;

&lt;div&gt;При переходе на Python 3 всё станет работать так, как нужно. Но пока многие важные библиотеки ещё с ним не работают, приходится искать другие решения. Когда-то давно я проходил по AST и заменял все &lt;code&gt;str&lt;/code&gt; на &lt;code&gt;unicode&lt;/code&gt;, оставляя нетронутыми строки, содержащие только символы ASCII (это было необходимо, чтобы работали именованные аргументы). Но в Python 2.6 появилась возможность сделать &lt;code&gt;from __future__ import unicode_literals&lt;/code&gt;. Для шаблонизатора же достаточно просто использовать соответствующий флаг при компиляции:

&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;source = u&#39;print repr(&quot;абв&quot;)&#39;
code = compile(source, &#39;&lt;template&gt;&#39;, &#39;exec&#39;,
               __future__.unicode_literals.compiler_flag)
exec code&lt;/code&gt;&lt;/pre&gt;

При выполнении этот код выведет &lt;code&gt;u&#39;\u0430\u0431\u0432&#39;&lt;/code&gt;.&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://otkds.blogspot.com/feeds/8119830491758368427/comments/default' title='Комментарии к сообщению'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5235863953075762128/8119830491758368427' title='Комментарии: 1'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/8119830491758368427'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/8119830491758368427'/><link rel='alternate' type='text/html' href='http://otkds.blogspot.com/2009/10/tornado-unicode.html' title='Шаблонизатор в Tornado и unicode'/><author><name>Anonymous</name><uri>http://www.blogger.com/profile/11828750781940428293</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5235863953075762128.post-4715916507827073910</id><published>2009-05-28T18:38:00.005+04:00</published><updated>2009-05-28T19:13:23.736+04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="list comprehension"/><title type='text'>Мысли по поводу list comprehension</title><content type='html'>&lt;div&gt;List comprehension &amp;mdash; приятная штука, позволяющая во многих ситуациях выразить мысль не только коротко, но и понятно. И всё же иногда кажется, что там чего-то не хватает. Вот несколько вариантов записи одного выражения:
&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;[target for target in [long_expr_of(source) for source in sources]
 if test_expr_of(target)]

filter(lambda x: test_expr_of(x),
       [long_expr_of(source) for source in sources])

[long_expr_of(source) for source in sources
 if test_expr_of(long_expr_of(source))]&lt;/code&gt;&lt;/pre&gt;
Первый способ громоздок, да и читаются вложенные списки не так хорошо. Второй способ выглядит неплохо, только если есть готовая функция test_expr_of и нет необходимости писать lambda-выражение. Третий способ неприятен с точки зрения эффективности. В таких случаях я предпочитаю разбивать вычисление на два выражения:
&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;targets = [long_expr_of(source) for source in sources]
targets = [target for target in targets if test_expr_of(target)]&lt;/code&gt;&lt;/pre&gt;
Вроде всё замечательно и ничего больше не нужно. И всё-таки, вспоминая SQL, хочется записать что-то вроде:
&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;[long_expr_of(source) as target for source in sources
 if test_expr_of(target)]&lt;/code&gt;&lt;/pre&gt;
Выглядит очень приятно. Может стоит написать PEP?&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://otkds.blogspot.com/feeds/4715916507827073910/comments/default' title='Комментарии к сообщению'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5235863953075762128/4715916507827073910' title='Комментарии: 6'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/4715916507827073910'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/4715916507827073910'/><link rel='alternate' type='text/html' href='http://otkds.blogspot.com/2009/05/list-comprehension.html' title='Мысли по поводу list comprehension'/><author><name>Anonymous</name><uri>http://www.blogger.com/profile/11828750781940428293</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>6</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5235863953075762128.post-4266540494296588728</id><published>2009-05-20T20:17:00.009+04:00</published><updated>2009-05-21T12:38:34.316+04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="ip2cc"/><title type='text'>Точность определения страны и региона с помощью ip2cc</title><content type='html'>&lt;div&gt;При использовании таких продуктов, как &lt;a href=&quot;http://bitbucket.org/ods/ip2cc/&quot;&gt;ip2cc&lt;/a&gt; всегда возникает вопрос, насколько точно он позволяет определить местонахождения пользователя. К сожалению, точный ответ на этот вопрос получить невозможно, но можно попытаться оценить её по косвенным признакам. И мне представилась такая возможность. Около полугода назад (свежесть здесь тоже имеет значение, так как блоки IP иногда перерегистрируются) мне посчастливилось участвовать в одном проекте, в котором пользователь при регистрации может указать страну и регион (для России). К настоящему времени таких пользователей набралось около 20000 &amp;mdash; достаточное количество для проведения оценки. Проект ориентирован на русскоязычную аудиторию, поэтому 93% пользователей из России. Благодаря ориентиру на широкую аудиторию, представленными оказались 77 стран и все 83 региона России.&lt;/div&gt;

&lt;div&gt;Вот что получилось при сравнении страны, определённой по IP с помощью базы ip2cc, с введённой пользователем:
&lt;div&gt;&lt;img src=&quot;http://chart.apis.google.com/chart?chco=66FF66%2CFF6666%2C666666&amp;chl=%D0%9F%D1%80%D0%B0%D0%B2%D0%B8%D0%BB%D1%8C%D0%BD%D0%BE+%E2%80%94+96.1%25%7C%D0%9D%D0%B5+%D0%BF%D1%80%D0%B0%D0%B2%D0%B8%D0%BB%D1%8C%D0%BD%D0%BE+%E2%80%94+3.7%25%7C%D0%9D%D0%B5%D1%82+%D0%B4%D0%B0%D0%BD%D0%BD%D1%8B%D1%85+%E2%80%94+0.2%25&amp;chs=500x100&amp;cht=p3&amp;chd=t%3A96.1%2C3.7%2C0.2&quot; alt=&quot;Правильно — 96.1%, Не правильно — 3.7%, Нет данных — 0.2%&quot;&gt;&lt;/div&gt;

Картинка представляется очень приятной, однако следует понимать, что она несколько искажается из-за того, что большинство пользователей из России &amp;mdash; такова специфика проекта. Если брать только пользователей, выбравших при регистрации другую страну, то всё становится не таким радужным:
&lt;div&gt;&lt;img src=&quot;http://chart.apis.google.com/chart?chco=66FF66%2CFF6666%2C666666&amp;chl=%D0%9F%D1%80%D0%B0%D0%B2%D0%B8%D0%BB%D1%8C%D0%BD%D0%BE+%E2%80%94+82.7%25%7C%D0%9D%D0%B5+%D0%BF%D1%80%D0%B0%D0%B2%D0%B8%D0%BB%D1%8C%D0%BD%D0%BE+%E2%80%94+16.5%25%7C%D0%9D%D0%B5%D1%82+%D0%B4%D0%B0%D0%BD%D0%BD%D1%8B%D1%85+%E2%80%94+0.8%25&amp;chs=500x100&amp;cht=p3&amp;chd=t%3A82.7%2C16.5%2C0.8&quot; alt=&quot;Правильно — 82.7%, Не правильно — 16.5%, Нет данных — 0.8%&quot;&gt;&lt;/div&gt;
Подавляющее большинство пользователей, попавших здесь в категорию &quot;нет данных&quot; имеют IP, зарегистрированный на Европу без указания конкретной страны.
&lt;/div&gt;

&lt;div&gt;Ну и, наконец, регионы России. Здесь я выделил в отдельную категорию пользователей из Москвы, Московской области, Санкт-Перербурга и Лениградской области (это всё разные субъекты федерации), для которых удалось определить регион с точностью путаницы город&amp;ndash;область. Дело в том, что значительное количество пользователей живя в области посещают проект с работы, находящейся в областном центре, и наоборот, выбирают при регистрации город, вынесенный вверх списка, не утруждая себя поисками области. Следует их считать определёнными неправильно или нет, зависит от задачи.
&lt;div&gt;&lt;img src=&quot;http://chart.apis.google.com/chart?chco=66FF66%2CEEDD66%2CFF6666%2C666666&amp;chl=%D0%9F%D1%80%D0%B0%D0%B2%D0%B8%D0%BB%D1%8C%D0%BD%D0%BE+%E2%80%94+79.1%25%7C%D0%93%D0%BE%D1%80%D0%BE%D0%B4%2F%D0%BE%D0%B1%D0%BB%D0%B0%D1%81%D1%82%D1%8C+%E2%80%94+4.9%25%7C%D0%9D%D0%B5+%D0%BF%D1%80%D0%B0%D0%B2%D0%B8%D0%BB%D1%8C%D0%BD%D0%BE+%E2%80%94+13.8%25%7C%D0%9D%D0%B5%D1%82+%D0%B4%D0%B0%D0%BD%D0%BD%D1%8B%D1%85+%E2%80%94+2.2%25&amp;chs=500x100&amp;cht=p3&amp;chd=t%3A79.1%2C4.9%2C13.8%2C2.2&quot; alt=&quot;Правильно — 79.1%, Город/область — 4.9%, Не правильно — 13.8%, Нет данных — 2.2%&quot;&gt;&lt;/div&gt;
&lt;/div&gt;

&lt;div&gt;Мне сложно судить, насколько качественная такая оценка. Далеко не всегда пользователи заходят из того региона, к которому они себя относят. Например, мне сложно поверить, чтобы IP, зарегистрированный на европейский университет, в реальности находился бы в России (хотя и такое бывает &amp;mdash; в своё время пул IP-адресов одной из сетей МГУ им. Ломоносова был зарегистрирован на Германию). Скорее всего, человек из России просто проходит там сейчас обучение. Поэтому указанные здесь цифры следует воспринимать только как нижнюю оценку точности, фактически гарантирующие, что определение будет не хуже.&lt;/div&gt;

&lt;div&gt;P.S. Для тех кто не в курсе, приведённые здесь диаграммы сделаны с помощью &lt;a href=&quot;http://code.google.com/intl/ru/apis/chart/&quot;&gt;API Google Chart&lt;/a&gt;. И чтобы как-то разбавить скучные статистические данные этого поста, приведу здесь использованную мной функцию для их получения:

&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;def pie_slice(items):
    values = []
    labels = []
    colors = []
    for label, value, color in items:
        values.append(&#39;%.1f&#39; % value)
        label = u&#39;%s \u2014 %.1f%%&#39; % (label, value)
        labels.append(label.encode(&#39;utf-8&#39;))
        colors.append(color)
    data = dict(
        cht=&#39;p3&#39;,
        chd=&#39;t:&#39;+&#39;,&#39;.join(values),
        chs=&#39;500x100&#39;,
        chl=&#39;|&#39;.join(labels),
        chco=&#39;,&#39;.join(colors)
    )
    url = &#39;http://chart.apis.google.com/chart?&#39;+urlencode(data)
    alt = &#39;, &#39;.join(labels)
    return &#39;&amp;lt;img src=&quot;%s&quot; alt=&quot;%s&quot;&amp;gt;&#39; % (url, alt)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://otkds.blogspot.com/feeds/4266540494296588728/comments/default' title='Комментарии к сообщению'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5235863953075762128/4266540494296588728' title='Комментарии: 0'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/4266540494296588728'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/4266540494296588728'/><link rel='alternate' type='text/html' href='http://otkds.blogspot.com/2009/05/ip2cc.html' title='Точность определения страны и региона с помощью ip2cc'/><author><name>Anonymous</name><uri>http://www.blogger.com/profile/11828750781940428293</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5235863953075762128.post-6916447037975721457</id><published>2009-05-07T19:25:00.005+04:00</published><updated>2009-05-08T14:34:25.699+04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="ip2cc"/><title type='text'>ip2cc 0.5: теперь и регионы России</title><content type='html'>&lt;div&gt;Как-то в далёком 2002 году я написал небольшой модуль ip2cc, позволяющий определять страну по IP и, что более важно, самостоятельно обновлять локальную базу на основании данных из официальных источников. И даже написал по этому поводу &lt;a href=&quot;http://python.ru/2002-06/69.html&quot;&gt;статью&lt;/a&gt;. После этого появились два новых регистратора LACNIC и AfriNIC, обнаружился &lt;a href=&quot;http://python.org/sf/788421&quot;&gt;баг в модуле bsddb&lt;/a&gt;, который сподвиг меня на написание своего узкоспециализированного B-Tree хранилища для IP-адресов. Модуль bsddb с тех пор починили, однако своё хранилище оказалось лучше по производительности, проще, стабильнее &amp;mdash; возращаться назад к bsddb я уже не вижу смысла. С тех пор много воды утекло, определять страну как-то не требовалось, потому я о нём забыл. И вот на днях мне снова потребовалось определять местонахождения пользователя по IP, но не только страну, а ещё и регион России. И вот результат &amp;mdash; &lt;a href=&quot;http://bitbucket.org/ods/ip2cc/&quot;&gt;ip2cc 0.5&lt;/a&gt;, в котором добавлена возможность создания базы IP по регионам России на основе данных проекта &lt;a href=&quot;http://ipgeobase.ru/&quot;&gt;IpGeoBase&lt;/a&gt;. Данные IpGeoBase позволяют определить и город, но у меня такой необходимости пока нет, поэтому я ограничился субъектами федерации.&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://otkds.blogspot.com/feeds/6916447037975721457/comments/default' title='Комментарии к сообщению'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5235863953075762128/6916447037975721457' title='Комментарии: 0'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/6916447037975721457'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/6916447037975721457'/><link rel='alternate' type='text/html' href='http://otkds.blogspot.com/2009/05/ip2cc-05.html' title='ip2cc 0.5: теперь и регионы России'/><author><name>Anonymous</name><uri>http://www.blogger.com/profile/11828750781940428293</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5235863953075762128.post-568545467370650566</id><published>2009-05-04T14:08:00.004+04:00</published><updated>2009-05-04T14:31:31.542+04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="sqlalchemy"/><title type='text'>Несколько баз данных в SQLAlchemy и наследование</title><content type='html'>&lt;div&gt;Как я писал ранее, в SQLAlchemy есть &lt;a href=&quot;http://otkds.blogspot.com/2009/04/sqlalchemy.html&quot;&gt;средства для работы с нескольким базами&lt;/a&gt;. Первая проблема, с которой я столкнулся &amp;mdash; автоматический выбор соединения перестаётся работать для моделей с наследованием. Дело в том, что метод &lt;code&gt;get_bind()&lt;/code&gt; сессии делает поиск соединения с нашем словаре на основе атрибута mapped_table маппера объекта. Для обычных моделей это объект класса Table, на основе которого я и строил словарь с соединениями. Но для производных классов при использовании joined table inheritance это объект класса Join для нескольких таблиц. На самом деле выбор соединения для запросов (каковым объект Join и является) в SQLAlchemy реализован, но почему-то не используется для моделей. Во избежание переписывания всего метода &lt;code&gt;get_bind()&lt;/code&gt; я просто передал &lt;code&gt;mapped_table&lt;/code&gt; вторым аргументом:

&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;class DBSession(sqlalchemy.orm.session.Session):

    def get_bind(self, mapper, clause=None):
        if mapper is not None and clause is None:
            c_mapper = sqlalchemy.orm.util._class_to_mapper(mapper)
            if hasattr(c_mapper, &#39;mapped_table&#39;):
                clause = mapper.mapped_table
        return sqlalchemy.orm.session.Session.get_bind(self, mapper, clause)&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://otkds.blogspot.com/feeds/568545467370650566/comments/default' title='Комментарии к сообщению'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5235863953075762128/568545467370650566' title='Комментарии: 3'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/568545467370650566'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/568545467370650566'/><link rel='alternate' type='text/html' href='http://otkds.blogspot.com/2009/05/sqlalchemy.html' title='Несколько баз данных в SQLAlchemy и наследование'/><author><name>Anonymous</name><uri>http://www.blogger.com/profile/11828750781940428293</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>3</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5235863953075762128.post-6088039815036299979</id><published>2009-04-27T13:10:00.005+04:00</published><updated>2009-05-04T14:10:15.721+04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="sqlalchemy"/><title type='text'>Несколько баз данных в SQLAlchemy</title><content type='html'>&lt;div&gt;Так уж получается, что во многих проектах мне приходится сталкиваться с ситуацией, когда часть хранится в обдной базе данных, а часть в другой. И несмотря на мою нелюбовь к SQLAlchemy, он позволяет организовать такую работу просто и прозрачно. В SQLAlchemy есть объекты класса &lt;code&gt;MetaData&lt;/code&gt;, которые служат в качестве реестра имеющихся таблиц. При описании таблиц (явно или при декларативном описании модели) можно указывать разные объекты &lt;code&gt;MetaData&lt;/code&gt; для данных, которые предполагается хранить в разных местах. Далее вы можете привязать метаданные каждый к своему соединению с базой данных, а можно научить средства ORM SQLAlchemy выбирать нужное соединение во время выполнения. Для этого при создании сессии передаётся параметр &lt;code&gt;binds&lt;/code&gt;. Так как схемы данных для разных баз совершенного естественным образом располагаются в разных модулях (пакетах), мне оказалось удобным использовать в качестве ссылки на метаданные имена модулей, в которых они определены:

&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;binds = {}
for module_name, connection_string in db_config.items():
    metadata = __import__(module_name, None, None, [&#39;metadata&#39;]).metadata
    engine = sqlalchemy.create_engine(connection_string, pool_recycle=True)
    for table in metadata.sorted_tables:
        binds[table] = engine
db = sqlalchemy.orm.sessionmaker(binds=binds)()&lt;/code&gt;&lt;/pre&gt;

После этого я работаю с сессией (&lt;code&gt;db&lt;/code&gt;) не заботясь о том, где на самом деле хранятся данные. Более того, SQLAlchemy позволяет использовать двуфазные транзакции (&lt;code&gt;twophase=True&lt;/code&gt; в &lt;code&gt;sessionmaker&lt;/code&gt;) для обеспечения сохранности данных при такой работе.&lt;/div&gt;

&lt;div&gt;Ситуация, когда разные данные хранятся в отдельных базах, это только одна из возможных задач. SQLAlchemy также имеет средства для распределённого хранения данных или позволяет относительно легко такие средства создавать. Но на самом деле картина не такая радужная, как я здесь нарисовал. При использование некоторых средств проявляются баги и недоработки в SQLAlchemy. Но об этом позже.&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://otkds.blogspot.com/feeds/6088039815036299979/comments/default' title='Комментарии к сообщению'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5235863953075762128/6088039815036299979' title='Комментарии: 3'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/6088039815036299979'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/6088039815036299979'/><link rel='alternate' type='text/html' href='http://otkds.blogspot.com/2009/04/sqlalchemy.html' title='Несколько баз данных в SQLAlchemy'/><author><name>Anonymous</name><uri>http://www.blogger.com/profile/11828750781940428293</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>3</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5235863953075762128.post-1040536165178255368</id><published>2009-02-04T13:43:00.011+03:00</published><updated>2009-02-05T07:56:34.546+03:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="form"/><category scheme="http://www.blogger.com/atom/ns#" term="HTML"/><category scheme="http://www.blogger.com/atom/ns#" term="XML"/><title type='text'>Нетекстовые символы в XML и HTML</title><content type='html'>&lt;div&gt;Большнство разработчиков знают, что некоторые специальные символы в XML и HTML необходимо записывать в виде entity или character reference. Но мало кто подозревает, что XML (а также SGML, на котором базируется HTML) документы могут содержать &lt;a href=&quot;http://www.w3.org/TR/REC-xml/#NT-Char&quot;&gt;только &quot;текстовые&quot; символы&lt;/a&gt;. Привычные питоновские библиотеки также обходят это требование стороной. Средства конструирования XML, которые, казалось бы, должны полностью защищать нас от создания &quot;битых&quot; документов, справляются со своей задачей лишь частично. В результате вполне возможна ситуация, когда библиотека ругается на созданный с её же помощью документ. Вот пример с использованием ElementTree:

&lt;pre&gt;&amp;gt;&amp;gt;&amp;gt; from xml.etree import ElementTree
&amp;gt;&amp;gt;&amp;gt; element = ElementTree.Element(&#39;element&#39;)
&amp;gt;&amp;gt;&amp;gt; element.text = u&#39;\0&#39;
&amp;gt;&amp;gt;&amp;gt; xml = ElementTree.tostring(element, encoding=&#39;utf-8&#39;)
&amp;gt;&amp;gt;&amp;gt; ElementTree.fromstring(xml)
[...]
xml.parsers.expat.ExpatError: not well-formed (invalid token): line 1, column 9&lt;/pre&gt;

И то же самое с minidom:

&lt;pre&gt;&amp;gt;&amp;gt;&amp;gt; from xml.dom import minidom
&amp;gt;&amp;gt;&amp;gt; doc = minidom.getDOMImplementation().createDocument(None, None, None)
&amp;gt;&amp;gt;&amp;gt; element = doc.createElement(&#39;element&#39;)
&amp;gt;&amp;gt;&amp;gt; element.appendChild(doc.createTextNode(u&#39;\0&#39;))
&amp;lt;DOM Text node &quot;&quot;&amp;gt;
&amp;gt;&amp;gt;&amp;gt; doc.appendChild(element)
&amp;lt;DOM Element: element at 0xb7ca688c&amp;gt;
&amp;gt;&amp;gt;&amp;gt; xml = doc.toxml(encoding=&#39;utf-8&#39;)
&amp;gt;&amp;gt;&amp;gt; minidom.parseString(xml)
[...]
xml.parsers.expat.ExpatError: not well-formed (invalid token): line 1, column 47&lt;/pre&gt;
&lt;/div&gt;

&lt;div&gt;
На мой взгляд, в описанной ситуации библиотека должна давать исключение ещё на стадии конструирования. Но даже если так и будет в будущих версиях, в реальной жизни это не решит проблему конкретного приложения, а лишь облегчит диагностику. Для web-приложений основной источник получения потенциально плохих данных &amp;mdash; через форму. И было бы правильно проверять или очищать данные на стадии получения их от пользователя. Однако и здесь всё плохо: из всех известных мне библиотек для обработки и валидации форм только одна (весьма древняя и малоизвестная) решает проблему нетекстовых символов, хотя большинство вполне корректно работают с &quot;битыми&quot; кодировками.
&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://otkds.blogspot.com/feeds/1040536165178255368/comments/default' title='Комментарии к сообщению'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5235863953075762128/1040536165178255368' title='Комментарии: 1'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/1040536165178255368'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/1040536165178255368'/><link rel='alternate' type='text/html' href='http://otkds.blogspot.com/2009/02/xml-html.html' title='Нетекстовые символы в XML и HTML'/><author><name>Anonymous</name><uri>http://www.blogger.com/profile/11828750781940428293</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5235863953075762128.post-4411867239925954057</id><published>2009-01-28T20:03:00.008+03:00</published><updated>2009-02-04T13:42:57.227+03:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="XML"/><category scheme="http://www.blogger.com/atom/ns#" term="собеседование"/><title type='text'>Задачка для собеседования</title><content type='html'>&lt;div&gt;Была у меня в своё время любимая задачка для собеседования. Есть веб-страничка принимающая данные от посетителей сайта:
&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;import cgi

form = cgi.FieldStorage()
message = form.getfirst(&#39;message&#39;, &#39;&#39;)
attr = form.getfirst(&#39;attr&#39;, &#39;&#39;)

result = &#39;&#39;&#39;&amp;lt;?xml version=&#39;1.0&#39;?&amp;gt;
&amp;lt;message attr=&#39;%s&#39;&amp;gt;%s&amp;lt;/message&amp;gt;&#39;&#39;&#39; % (attr, message)&lt;/code&gt;&lt;/pre&gt;
Далее данные отправляются, например, стороннему сервису. Так вот периодически этот сервис нам возвращает ошибку &quot;not well-formed&quot;. И предлагаю найти и исправить ошибки. При этом я сразу оговариваю, что задача не столько на знание XML, сколько на умение решать проблемы, возникающие в ходе разработки. Кроме того, я делаю акцент на том, что пользователь может ввести &lt;em&gt;произвольные&lt;/em&gt; данные.&lt;/div&gt;

&lt;div&gt;Большинство соискателей сходу называют одну ошибку (представление спец-символов) и относительно быстро находят вторую (связанная с кодировкой). А вот третья проблема всегда остаётся незамеченной и &quot;обходит&quot; все предложенные тесты. Кроме того, большинство современных средств построения XML (например, ElementTree) молча пропускают такие ошибки. Как вариант, я предлагаю выбрать первый пришедший в голову blog-движок (как правило, это byteflow на django) с трансляцией RSS или Atom и написать тест, показывающий, что любой комментатор может &quot;сломать&quot; (весьма условно, учитывая что большиство современных читалок умеют переваривать и битый XML) feed комментариев. В короткий срок и без большого количества наводящих подсказок с этой задачей смог справиться только &lt;a href=&quot;http://phd.pp.ru/&quot;&gt;Олег Бройтман&lt;/a&gt;. Типичное время решения с подсказками &amp;mdash; более суток.&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://otkds.blogspot.com/feeds/4411867239925954057/comments/default' title='Комментарии к сообщению'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5235863953075762128/4411867239925954057' title='Комментарии: 16'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/4411867239925954057'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/4411867239925954057'/><link rel='alternate' type='text/html' href='http://otkds.blogspot.com/2009/01/blog-post.html' title='Задачка для собеседования'/><author><name>Anonymous</name><uri>http://www.blogger.com/profile/11828750781940428293</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>16</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5235863953075762128.post-5498712994293489834</id><published>2008-10-21T15:34:00.007+04:00</published><updated>2008-10-21T20:13:53.791+04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="sqlalchemy"/><title type='text'>Сохранение в SQLAlchemy под контролем</title><content type='html'>&lt;div&gt;Большинство задач, для которых используется SQLAlchemy &amp;mdash; это веб-приложения, отличающиеся небольшим количеством действий, выполняемых на один запрос, короткими транзакциями. И для этих целей типовая схема работы, расписанная в документации, подходит очень хорошо. Но работа с базами данных нужна не только в веб-приложениях, и даже в веб-приложениях иногда есть отдельные процессы с более сложными операциями.&lt;/div&gt;

&lt;div&gt;Типовая схемы работы предполагает накопление некоторого количества изменений и вызов метода &lt;code&gt;flush()&lt;/code&gt; у сессии, который сохраняет все изменения в базе. А теперь представьте, что будет, если в ходе работы на одной из итераций мы получаем исключение, мы это исключение обрабатываем и продолжаем работу? Вполне резонно, что часть (&quot;ошибочных&quot;) накопленных изменений должна пропасть, то есть не попасть в базу. Но ведь метод &lt;code&gt;flush()&lt;/code&gt; предполагает сохранения именно всех изменений. Конечно, мы можем очистить сессию и произвести инициализацию заново &amp;mdash; достаточно неудобно, да и зачем снова загружать данные, которые не могли измениться? Кто-то резонно заметит, что в метод &lt;code&gt;flush()&lt;/code&gt; можно передать список объектов для сохранения. Да, это именно то, что нужно. Только следует понимать, что в этом случае сохраняться будут только эти объекты, но не объекты, которые от них зависят, то есть cascade rules перестают работать. В итоге мы не можем использовать &lt;code&gt;autoflush=True&lt;/code&gt; и должны самостоятельно отслеживать каскадные правила при сохранении. Аналогично не стоит использовать &lt;code&gt;transactional=True&lt;/code&gt;, так как в этом случае транзакция открывается сразу же после закрытия предыдущей, и при длительной работе без &lt;code&gt;commit()&lt;/code&gt;-ов могут возникать значительные замедления в работе базы данных.&lt;/div&gt;

&lt;div&gt;Ещё одна неприятная особенность есть у &lt;code&gt;SessionTransaction&lt;/code&gt;. Используя другие библиотеки для работы с базами данных я привык, что можно определить метод с некоторой транзакцией, а затем вызывать его из другого метода, в котором к исходным действиям добавляются ещё какие-то, и всё это, конечно, в одной общей транзакции. Но дело в том, что сессионные транзакции в SQLAlchemy не могут быть вложенными. На самом деле всё гораздо хуже, они могут быть вложенными, но результат будет отличным от ожидаемого: транзакция будет закрыта уже при при вызове &lt;code&gt;commit()&lt;/code&gt; внутренней транзакции. Проблема решается использованием объекта транзакции для соединения, который работает как нужно.&lt;/div&gt;

&lt;div&gt;Подытожу всё сказанное в классе &lt;code&gt;Storage&lt;/code&gt; (недостающие методы не представляют сложности в реализации):

&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker


class Storage(object):

    def __init__(self, dbURL):
        self._engine = create_engine(dbURL)
        self._conn = self._engine.connect()
        self._session = sessionmaker(bind=self._conn, autoflush=False,
                                     transactional=False)()

    def transaction(self):
        return self._conn.begin()

    def store(self, obj):
        with self.transaction():
            self._session.save_or_update(obj)
            from sqlalchemy.orm.session import _cascade_iterator
            cascaded = [o for o, m in _cascade_iterator(&#39;save-update&#39;, obj)]
            self._session.flush([obj]+cascaded)&lt;/pre&gt;

&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://otkds.blogspot.com/feeds/5498712994293489834/comments/default' title='Комментарии к сообщению'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5235863953075762128/5498712994293489834' title='Комментарии: 2'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/5498712994293489834'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/5498712994293489834'/><link rel='alternate' type='text/html' href='http://otkds.blogspot.com/2008/10/sqlalchemy.html' title='Сохранение в SQLAlchemy под контролем'/><author><name>Anonymous</name><uri>http://www.blogger.com/profile/11828750781940428293</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5235863953075762128.post-766520265189015725</id><published>2008-09-25T13:17:00.006+04:00</published><updated>2011-09-30T20:44:13.059+04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="datetime"/><category scheme="http://www.blogger.com/atom/ns#" term="db"/><category scheme="http://www.blogger.com/atom/ns#" term="sqlalchemy"/><title type='text'>Сохранение времени в базе данных</title><content type='html'>&lt;div&gt;Очень часто бывает, что практически все знают, как надо делать правильно, но при этом всё равно постоянно делают неправильно. Один из таких случаев &amp;mdash; сохрание времени в базе данных. Понятно, что на персональном блоге вполне можно обойтись наивных подходом, не учитывающим перевод времени. Но для круглосуточно работающих приложений строгой системы отчётности вроде биллинга это неприемлемо.&lt;/div&gt;

&lt;div&gt;Я не буду здесь рассматривать все возможные варианты корректной работы со временем. Покажу лишь насколько просто можно реализовать самый распространённый вариант &amp;mdash; хранение в базе в UTC &amp;mdash; на примере SQLAlchemy. Для этого достаточно определить новый тип колонки:
&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;from sqlalchemy import types
from dateutil.tz import tzutc
from datetime import datetime

class UTCDateTime(types.TypeDecorator):

    impl = types.DateTime

    def process_bind_param(self, value, engine):
        if value is not None:
            return value.astimezone(tzutc()).replace(tzinfo=None)

    def process_result_value(self, value, engine):
        if value is not None:
            return datetime(value.year, value.month, value.day,
                            value.hour, value.minute, value.second,
                            value.microsecond, tzinfo=tzutc())
&lt;/code&gt;&lt;/pre&gt;
Теперь вы можете сохранять время с произвольной зоной, все преобразования будут сделаны автоматически. Но сохранить время без зоны не получится &amp;mdash; метод &lt;code&gt;astimezone()&lt;/code&gt; выбросит исключение &lt;code&gt;ValueError&lt;/code&gt;, что позволит избежать случайных ошибок.&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://otkds.blogspot.com/feeds/766520265189015725/comments/default' title='Комментарии к сообщению'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5235863953075762128/766520265189015725' title='Комментарии: 0'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/766520265189015725'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/766520265189015725'/><link rel='alternate' type='text/html' href='http://otkds.blogspot.com/2008/09/blog-post_25.html' title='Сохранение времени в базе данных'/><author><name>Anonymous</name><uri>http://www.blogger.com/profile/11828750781940428293</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5235863953075762128.post-2054443088513104144</id><published>2008-09-22T18:27:00.014+04:00</published><updated>2008-09-25T13:37:05.866+04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="db"/><category scheme="http://www.blogger.com/atom/ns#" term="logging"/><category scheme="http://www.blogger.com/atom/ns#" term="recursion"/><title type='text'>Логгинг в базу или борьба с рекурсией</title><content type='html'>&lt;div&gt;Как-то понадобилось мне сделать логгинг в базу для модуля logging. Сразу видна очевидная проблема: внутри такого обработчика идёт сохранения в базу некоторыми принятыми в проекте средствами, которые сами используют logging. В результате обработчик рекусивно будет вызывать себя и зацикливаться. Понятно, что можно отбросить привычные средства и сделать либо логгинг своими средствами без использования стандартного пакета logging, либо в базу писать низкоуровневым кодом, который logging не использует. Но это всё не интересно и ведёт за собой неудобства в использовании.&lt;/div&gt;

&lt;div&gt;Первая мысль была выставлять флаг в обработчике перед записью в базу и снимать его после. Но это без ухищрений не будет работать в многопоточном приложении, а ведь есть ещё и обработчики сигналов. А нельзя ли определить, что обработчик был вызван из самого себя? Оказывается, можно &amp;mdash; с помощью &lt;a href=&quot;http://docs.python.org/lib/inspect-stack.html&quot;&gt;средств работы со стеком интерпретатора в модуле &lt;code&gt;inspect&lt;/code&gt;&lt;/a&gt;. Достаточно убедиться, чтобы текущего метода не было в стеке вызовов. Сам фрейм не подходит для сравнения, так как он создаётся новый на каждый вызов, но можно сравнивать объекты кода. Первая версия на базе &lt;code&gt;inspect.stack()&lt;/code&gt; оказалась достаточно медленной, а ведь запись в лог — это то, что используется постоянно. Дело в том, что эта функция подготавливает много лишней информации, которая нам не нужна. Зато проход по стеку &quot;вручную&quot; оказался достаточно быстрым, примерно на 2 порядка быстрее варианта с &lt;code&gt;inspect.stack()&lt;/code&gt;:
&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;def isRecursive():
   &#39;&#39;&#39;Returns whether it&#39;s recursive call of caller function.&#39;&#39;&#39;
   frame = inspect.currentframe().f_back
   try:
       code = frame.f_code
       while True:
           frame = frame.f_back
           if frame is None:
               break
           if frame.f_code is code:
               return True
       return False
   finally:
       del frame
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;div&gt;Сам обработчик достаточно прост (здесь &lt;code&gt;storage&lt;/code&gt; &amp;mdash; произвольное хранилище, в моём случае оно сделано на базе SQLAlchemy):
&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;class DBServiceLogHandler(logging.Handler):

   def __init__(self, storage):
       logging.Handler.__init__(self)
       self._storage = storage

   def emit(self, record):
       # We can&#39;t check this in filter() method, since recursive call is from
       # emit, not filter.
       if isRecursive():
           return
       traceback = None
       if record.exc_info:
           # It&#39;s used by logging to cache
           if not record.exc_text:
               record.exc_text = self.formatException(record.exc_info)
           traceback = record.exc_text
       entry = LogEntry(name=record.name, level=record.levelno,
                        message=record.getMessage(), traceback=traceback)
       try:
           self._storage.store(entry)
       except self._storage.Error:
           logger.exception(&#39;Error logging to DB (%s):&#39;, record.getMessage())
       self._storage.clear()
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://otkds.blogspot.com/feeds/2054443088513104144/comments/default' title='Комментарии к сообщению'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5235863953075762128/2054443088513104144' title='Комментарии: 2'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/2054443088513104144'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/2054443088513104144'/><link rel='alternate' type='text/html' href='http://otkds.blogspot.com/2008/09/blog-post.html' title='Логгинг в базу или борьба с рекурсией'/><author><name>Anonymous</name><uri>http://www.blogger.com/profile/11828750781940428293</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5235863953075762128.post-1109201124218370851</id><published>2008-08-07T12:32:00.004+04:00</published><updated>2008-08-07T13:04:44.579+04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="ssh"/><title type='text'>Всемогущий ssh</title><content type='html'>&lt;div&gt;Думаю, сложно найти веб-разработичка, который бы не использовал &lt;kbd&gt;ssh&lt;/kbd&gt; в своей работе. Но далеко не каждый знает о его возможностях. Когда-то очень давно я испытал ребяческий восторг впервые установив прямой канал между двумя компьютерами в несвязанных друг с другом private network. Сейчас уже стало привычным попросить человека в заNATченной сетке запустить на пару минут команду вида 
&lt;pre&gt;&lt;kbd&gt;ssh -R &lt;var&gt;publicport&lt;/var&gt;:&lt;var&gt;privatehost&lt;/var&gt;:22 &lt;var&gt;user&lt;/var&gt;@&lt;var&gt;publichost&lt;/var&gt;&lt;/kbd&gt;&lt;/pre&gt;
и уже самому с &lt;var&gt;privatehost&lt;/var&gt; запустить аналогичную команду, но уже через &lt;kbd&gt;autossh&lt;/kbd&gt;. А тут вчера мой коллега из дальнего зарубежья вдруг просит дать ему попробовать кое-какие действия по HTTP с российских IP. Поднимать локальный прокси-сервер мне не хотелось, поэтому первая же моя мысль была, а не поможет ли мне здесь &lt;kbd&gt;ssh&lt;/kbd&gt;? Но одно дело перенаправить соединение на определенный порт определенной машины, а другое дело дать возможность устанавливать соединение куда угодно. Надо отдать должное, и в этот раз нашлась нужная опция. Всего две команды
&lt;pre&gt;&lt;kbd&gt;ssh -R &lt;var&gt;remotesshport&lt;/var&gt;:localhost:22 &lt;var&gt;me&lt;/var&gt;@&lt;var&gt;remotehost&lt;/var&gt;&lt;/kbd&gt;&lt;/pre&gt;
и уже оттуда
&lt;pre&gt;&lt;kbd&gt;ssh -D &#39;*:&lt;var&gt;proxyport&lt;/var&gt;&#39; -p &lt;var&gt;remotesshport&lt;/var&gt; &lt;var&gt;me&lt;/var&gt;@localhost&lt;/kbd&gt;&lt;/pre&gt;
и SOCKS прокси настроен.&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://otkds.blogspot.com/feeds/1109201124218370851/comments/default' title='Комментарии к сообщению'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5235863953075762128/1109201124218370851' title='Комментарии: 0'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/1109201124218370851'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/1109201124218370851'/><link rel='alternate' type='text/html' href='http://otkds.blogspot.com/2008/08/ssh.html' title='Всемогущий ssh'/><author><name>Anonymous</name><uri>http://www.blogger.com/profile/11828750781940428293</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-5235863953075762128.post-1940089500563834309</id><published>2008-06-19T17:48:00.005+04:00</published><updated>2008-06-19T18:36:17.603+04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="sqlalchemy"/><title type='text'>Борьба с обNULLением в SQLAlchemy</title><content type='html'>&lt;div&gt;SQLAlchemy, пожалуй, самый продвинутый ORM для питона. Но, к сожалению, он постоянно подбрасывает неприятные сюрпризы. В очередной раз натолкнувшись на одну из проблем и потратив время на повторный поиск её решения, я решил его задокументировать. Речь об установки в &lt;code&gt;NULL&lt;/code&gt; поля с идентификатором при удалении объекта, на который он ссылается, если в маппере для связи используется &lt;code&gt;relation&lt;/code&gt;. Для тех, кто привык работать с SQL, такое поведение по умолчанию в лучшем случае вызывает недоумение. Фактически оно означает использование на уровне кода по умолчанию правила &lt;code&gt;ON DELETE SET NULL&lt;/code&gt;, вместо привычного (и логичного!) &lt;code&gt;ON DELETE RESTRICT&lt;/code&gt;. Если бы не моя чрезмерная педантичность в проставлении &lt;code&gt;nullable=False&lt;/code&gt; для полей с &lt;code&gt;FOREIGN KEY&lt;/code&gt;, этот сюрприз мог бы привести к весьма печальным последствиям - потери данных. Упоминание об описанном поведении в документации к SQLAlchemy встречается только один раз - при описании ключа &lt;code&gt;passive_deletes&lt;/code&gt; функции &lt;code&gt;relation()&lt;/code&gt;. Собственно его установка в &lt;code&gt;&#39;all&#39;&lt;/code&gt; и решает проблему. Так как имя ключа ничего не говорит о его истинном назначении, то соответствующий комментарий явно не помешает.&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://otkds.blogspot.com/feeds/1940089500563834309/comments/default' title='Комментарии к сообщению'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/5235863953075762128/1940089500563834309' title='Комментарии: 3'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/1940089500563834309'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/5235863953075762128/posts/default/1940089500563834309'/><link rel='alternate' type='text/html' href='http://otkds.blogspot.com/2008/06/null-sqlalchemy.html' title='Борьба с обNULLением в SQLAlchemy'/><author><name>Anonymous</name><uri>http://www.blogger.com/profile/11828750781940428293</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>3</thr:total></entry></feed>