New Upstream Release - python-psycopg2cffi

Ready changes

Summary

Merged new upstream version: 2.9.0 (was: 2.8.1).

Resulting package

Built on 2022-12-30T21:41 (took 3m50s)

The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:

apt install -t fresh-releases python3-psycopg2cffi-dbgsymapt install -t fresh-releases python3-psycopg2cffi

Lintian Result

Diff

diff --git a/.travis.yml b/.travis.yml
index 7171168..4dc5a83 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -17,10 +17,7 @@ after_script:
 matrix:
   fast_finish: true
   include:
-    - python: "2.6"
     - python: "2.7"
-    - python: "3.3"
-    - python: "3.4"
     - python: "3.5"
     - python: "3.6"
     - python: "pypy"
@@ -28,3 +25,6 @@ matrix:
     - python: 3.7
       dist: xenial
       sudo: true
+    - python: 3.8
+      dist: xenial
+      sudo: true
diff --git a/README.rst b/README.rst
index a15fc51..a299c21 100644
--- a/README.rst
+++ b/README.rst
@@ -25,7 +25,7 @@ Installation was tested on Ubuntu 12.04, Ubuntu 14.04, CentOS (RHEL 5.0),
 OS X 10.8 - 10.10.
 It should be possible to make it work on Windows, but I did not test it.
 
-This module works under CPython 2.6+, CPython 3.2+, PyPy 2 and PyPy 3
+This module works under CPython 2.7+, CPython 3.5+, PyPy 2 and PyPy 3
 (PyPy version should be at least 2.0, which is ancient history now).
 
 To use this package with Django or SQLAlchemy invoke a compatibility
@@ -78,6 +78,25 @@ https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/unit-t
 Release notes
 -------------
 
+2.9.0 (27 Jan 2021)
++++++++++++++++++++
+
+New features:
+
+- Add execute_batch and execute_values to psycopg2cffi.extras by @fake-name in #98
+- psycopg2cffi.extras: add fetch argument to execute_values() by @intelfx in #119
+
+Bug fixes:
+
+- Fix for async keyword argument when creating a connection by @donalm in #104
+- Allow adapters to be passed as arguments of cursor's execute() by @amigrave in #107
+- Fix installation with old cffi by dand-oss in #116
+
+Test changes:
+
+- Dropped support for python 2.6, 3.3, 3.4 by @thedrow in #109
+- Added support for python 3.8 by @thedrow in #108
+
 2.8.1 (31 July 2018)
 ++++++++++++++++++++
 
diff --git a/debian/changelog b/debian/changelog
index e4c5366..de4ef98 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+python-psycopg2cffi (2.9.0-1) UNRELEASED; urgency=low
+
+  * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Fri, 30 Dec 2022 21:37:41 -0000
+
 python-psycopg2cffi (2.8.1-2) unstable; urgency=medium
 
   * Re-upload source-only.
diff --git a/psycopg2cffi/__init__.py b/psycopg2cffi/__init__.py
index adc70e7..6b9a2cd 100644
--- a/psycopg2cffi/__init__.py
+++ b/psycopg2cffi/__init__.py
@@ -10,7 +10,7 @@ from psycopg2cffi._impl.connection import _connect
 from psycopg2cffi._impl.exceptions import *
 from psycopg2cffi._impl.typecasts import BINARY, DATETIME, NUMBER, ROWID, STRING
 
-__version__ = '2.8.1'
+__version__ = '2.9.0'
 apilevel = '2.0'
 paramstyle = 'pyformat'
 threadsafety = 2
@@ -88,11 +88,11 @@ def connect(dsn=None,
     if port is not None:
         items.append(('port', port))
 
-    kwasync = {}
+    async_ = False
     if 'async' in kwargs:
-        kwasync['async'] = kwargs.pop('async')
+        async_ = kwargs.pop('async')
     if 'async_' in kwargs:
-        kwasync['async_'] = kwargs.pop('async_')
+        async_ = kwargs.pop('async_')
 
     items.extend([(k, v) for (k, v) in kwargs.items() if v is not None])
 
@@ -108,7 +108,7 @@ def connect(dsn=None,
             dsn = " ".join(["%s=%s" % (k, _param_escape(str(v)))
                 for (k, v) in items])
 
-    conn = _connect(dsn, connection_factory=connection_factory, **kwasync)
+    conn = _connect(dsn, connection_factory=connection_factory, async_=async_)
     if cursor_factory is not None:
         conn.cursor_factory = cursor_factory
 
diff --git a/psycopg2cffi/_impl/adapters.py b/psycopg2cffi/_impl/adapters.py
index 108a7da..20e91cf 100644
--- a/psycopg2cffi/_impl/adapters.py
+++ b/psycopg2cffi/_impl/adapters.py
@@ -289,7 +289,10 @@ def _getquoted(param, conn):
     """Helper method"""
     if param is None:
         return b'NULL'
-    adapter = adapt(param)
+    if isinstance(param, _BaseAdapter):
+        adapter = param
+    else:
+        adapter = adapt(param)
     try:
         adapter.prepare(conn)
     except AttributeError:
diff --git a/psycopg2cffi/extras.py b/psycopg2cffi/extras.py
index 2c1b8e2..9158d6f 100644
--- a/psycopg2cffi/extras.py
+++ b/psycopg2cffi/extras.py
@@ -999,6 +999,166 @@ def register_composite(name, conn_or_curs, globally=False, factory=None):
     return caster
 
 
+
+def _paginate(seq, page_size):
+    """Consume an iterable and return it in chunks.
+
+    Every chunk is at most `page_size`. Never return an empty chunk.
+    """
+    page = []
+    it = iter(seq)
+    while 1:
+        try:
+            for i in range(page_size):
+                page.append(next(it))
+            yield page
+            page = []
+        except StopIteration:
+            if page:
+                yield page
+            return
+
+
+def execute_batch(cur, sql, argslist, page_size=100):
+    r"""Execute groups of statements in fewer server roundtrips.
+
+    Execute *sql* several times, against all parameters set (sequences or
+    mappings) found in *argslist*.
+
+    The function is semantically similar to
+
+    .. parsed-literal::
+
+        *cur*\.\ `~cursor.executemany`\ (\ *sql*\ , *argslist*\ )
+
+    but has a different implementation: Psycopg will join the statements into
+    fewer multi-statement commands, each one containing at most *page_size*
+    statements, resulting in a reduced number of server roundtrips.
+
+    After the execution of the function the `cursor.rowcount` property will
+    **not** contain a total result.
+
+    """
+    for page in _paginate(argslist, page_size=page_size):
+        sqls = [cur.mogrify(sql, args) for args in page]
+        cur.execute(b";".join(sqls))
+
+
+def execute_values(cur, sql, argslist, template=None, page_size=100, fetch=False):
+    '''Execute a statement using :sql:`VALUES` with a sequence of parameters.
+
+    :param cur: the cursor to use to execute the query.
+
+    :param sql: the query to execute. It must contain a single ``%s``
+        placeholder, which will be replaced by a `VALUES list`__.
+        Example: ``"INSERT INTO mytable (id, f1, f2) VALUES %s"``.
+
+    :param argslist: sequence of sequences or dictionaries with the arguments
+        to send to the query. The type and content must be consistent with
+        *template*.
+
+    :param template: the snippet to merge to every item in *argslist* to
+        compose the query.
+
+        - If the *argslist* items are sequences it should contain positional
+          placeholders (e.g. ``"(%s, %s, %s)"``, or ``"(%s, %s, 42)``" if there
+          are constants value...).
+
+        - If the *argslist* items are mappings it should contain named
+          placeholders (e.g. ``"(%(id)s, %(f1)s, 42)"``).
+
+        If not specified, assume the arguments are sequence and use a simple
+        positional template (i.e.  ``(%s, %s, ...)``), with the number of
+        placeholders sniffed by the first element in *argslist*.
+
+    :param page_size: maximum number of *argslist* items to include in every
+        statement. If there are more items the function will execute more than
+        one statement.
+
+    :param fetch: if `!True` return the query results into a list (like in a
+        `~cursor.fetchall()`).  Useful for queries with :sql:`RETURNING`
+        clause.
+
+    .. __: https://www.postgresql.org/docs/current/static/queries-values.html
+
+    After the execution of the function the `cursor.rowcount` property will
+    **not** contain a total result.
+
+    While :sql:`INSERT` is an obvious candidate for this function it is
+    possible to use it with other statements, for example::
+
+        >>> cur.execute(
+        ... "create table test (id int primary key, v1 int, v2 int)")
+
+        >>> execute_values(cur,
+        ... "INSERT INTO test (id, v1, v2) VALUES %s",
+        ... [(1, 2, 3), (4, 5, 6), (7, 8, 9)])
+
+        >>> execute_values(cur,
+        ... """UPDATE test SET v1 = data.v1 FROM (VALUES %s) AS data (id, v1)
+        ... WHERE test.id = data.id""",
+        ... [(1, 20), (4, 50)])
+
+        >>> cur.execute("select * from test order by id")
+        >>> cur.fetchall()
+        [(1, 20, 3), (4, 50, 6), (7, 8, 9)])
+
+    '''
+    # we can't just use sql % vals because vals is bytes: if sql is bytes
+    # there will be some decoding error because of stupid codec used, and Py3
+    # doesn't implement % on bytes.
+    if not isinstance(sql, bytes):
+        sql = sql.encode(_ext.encodings[cur.connection.encoding])
+    pre, post = _split_sql(sql)
+
+    result = [] if fetch else None
+    for page in _paginate(argslist, page_size=page_size):
+        if template is None:
+            template = b'(' + b','.join([b'%s'] * len(page[0])) + b')'
+        parts = pre[:]
+        for args in page:
+            parts.append(cur.mogrify(template, args))
+            parts.append(b',')
+        parts[-1:] = post
+        cur.execute(b''.join(parts))
+        if fetch:
+            result.extend(cur.fetchall())
+
+    return result
+
+
+def _split_sql(sql):
+    """Split *sql* on a single ``%s`` placeholder.
+
+    Split on the %s, perform %% replacement and return pre, post lists of
+    snippets.
+    """
+    curr = pre = []
+    post = []
+    tokens = _re.split(br'(%.)', sql)
+    for token in tokens:
+        if len(token) != 2 or token[:1] != b'%':
+            curr.append(token)
+            continue
+
+        if token[1:] == b's':
+            if curr is pre:
+                curr = post
+            else:
+                raise ValueError(
+                    "the query contains more than one '%s' placeholder")
+        elif token[1:] == b'%':
+            curr.append(b'%')
+        else:
+            raise ValueError("unsupported format character: '%s'"
+                % token[1:].decode('ascii', 'replace'))
+
+    if curr is pre:
+        raise ValueError("the query doesn't contain any '%s' placeholder")
+
+    return pre, post
+
+
 # expose the json adaptation stuff into the module
 from psycopg2cffi._json import json, Json, register_json, register_default_json
 from psycopg2cffi._json import register_default_json, register_default_jsonb
diff --git a/psycopg2cffi/tests/psycopg2_tests/test_fast_executemany.py b/psycopg2cffi/tests/psycopg2_tests/test_fast_executemany.py
new file mode 100644
index 0000000..3a28bab
--- /dev/null
+++ b/psycopg2cffi/tests/psycopg2_tests/test_fast_executemany.py
@@ -0,0 +1,242 @@
+#!/usr/bin/env python
+#
+# test_fast_executemany.py - tests for fast executemany implementations
+#
+# Copyright (C) 2017 Daniele Varrazzo  <daniele.varrazzo@gmail.com>
+#
+# psycopg2 is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# psycopg2 is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
+# License for more details.
+
+from datetime import date
+
+from . import testutils
+import unittest
+
+import psycopg2cffi as psycopg2
+from psycopg2cffi import extras
+import psycopg2cffi.extensions as ext
+
+
+class TestPaginate(unittest.TestCase):
+    def test_paginate(self):
+        def pag(seq):
+            return psycopg2.extras._paginate(seq, 100)
+
+        self.assertEqual(list(pag([])), [])
+        self.assertEqual(list(pag([1])), [[1]])
+        self.assertEqual(list(pag(range(99))), [list(range(99))])
+        self.assertEqual(list(pag(range(100))), [list(range(100))])
+        self.assertEqual(list(pag(range(101))), [list(range(100)), [100]])
+        self.assertEqual(
+            list(pag(range(200))), [list(range(100)), list(range(100, 200))])
+        self.assertEqual(
+            list(pag(range(1000))),
+            [list(range(i * 100, (i + 1) * 100)) for i in range(10)])
+
+
+class FastExecuteTestMixin(object):
+    def setUp(self):
+        super(FastExecuteTestMixin, self).setUp()
+        cur = self.conn.cursor()
+        cur.execute("""create table testfast (
+            id serial primary key, date date, val int, data text)""")
+
+
+class TestExecuteBatch(FastExecuteTestMixin, testutils.ConnectingTestCase):
+    def test_empty(self):
+        cur = self.conn.cursor()
+        psycopg2.extras.execute_batch(cur,
+            "insert into testfast (id, val) values (%s, %s)",
+            [])
+        cur.execute("select * from testfast order by id")
+        self.assertEqual(cur.fetchall(), [])
+
+    def test_one(self):
+        cur = self.conn.cursor()
+        psycopg2.extras.execute_batch(cur,
+            "insert into testfast (id, val) values (%s, %s)",
+            iter([(1, 10)]))
+        cur.execute("select id, val from testfast order by id")
+        self.assertEqual(cur.fetchall(), [(1, 10)])
+
+    def test_tuples(self):
+        cur = self.conn.cursor()
+        psycopg2.extras.execute_batch(cur,
+            "insert into testfast (id, date, val) values (%s, %s, %s)",
+            ((i, date(2017, 1, i + 1), i * 10) for i in range(10)))
+        cur.execute("select id, date, val from testfast order by id")
+        self.assertEqual(cur.fetchall(),
+            [(i, date(2017, 1, i + 1), i * 10) for i in range(10)])
+
+    def test_many(self):
+        cur = self.conn.cursor()
+        psycopg2.extras.execute_batch(cur,
+            "insert into testfast (id, val) values (%s, %s)",
+            ((i, i * 10) for i in range(1000)))
+        cur.execute("select id, val from testfast order by id")
+        self.assertEqual(cur.fetchall(), [(i, i * 10) for i in range(1000)])
+
+    def test_pages(self):
+        cur = self.conn.cursor()
+        psycopg2.extras.execute_batch(cur,
+            "insert into testfast (id, val) values (%s, %s)",
+            ((i, i * 10) for i in range(25)),
+            page_size=10)
+
+        # last command was 5 statements
+        self.assertEqual(sum(c == u';' for c in cur.query.decode('ascii')), 4)
+
+        cur.execute("select id, val from testfast order by id")
+        self.assertEqual(cur.fetchall(), [(i, i * 10) for i in range(25)])
+
+    @testutils.skip_before_postgres(8, 0)
+    def test_unicode(self):
+        cur = self.conn.cursor()
+        ext.register_type(ext.UNICODE, cur)
+        snowman = u"\u2603"
+
+        # unicode in statement
+        psycopg2.extras.execute_batch(cur,
+            "insert into testfast (id, data) values (%%s, %%s) -- %s" % snowman,
+            [(1, 'x')])
+        cur.execute("select id, data from testfast where id = 1")
+        self.assertEqual(cur.fetchone(), (1, 'x'))
+
+        # unicode in data
+        psycopg2.extras.execute_batch(cur,
+            "insert into testfast (id, data) values (%s, %s)",
+            [(2, snowman)])
+        cur.execute("select id, data from testfast where id = 2")
+        self.assertEqual(cur.fetchone(), (2, snowman))
+
+        # unicode in both
+        psycopg2.extras.execute_batch(cur,
+            "insert into testfast (id, data) values (%%s, %%s) -- %s" % snowman,
+            [(3, snowman)])
+        cur.execute("select id, data from testfast where id = 3")
+        self.assertEqual(cur.fetchone(), (3, snowman))
+
+
+class TestExecuteValues(FastExecuteTestMixin, testutils.ConnectingTestCase):
+    def test_empty(self):
+        cur = self.conn.cursor()
+        psycopg2.extras.execute_values(cur,
+            "insert into testfast (id, val) values %s",
+            [])
+        cur.execute("select * from testfast order by id")
+        self.assertEqual(cur.fetchall(), [])
+
+    def test_one(self):
+        cur = self.conn.cursor()
+        psycopg2.extras.execute_values(cur,
+            "insert into testfast (id, val) values %s",
+            iter([(1, 10)]))
+        cur.execute("select id, val from testfast order by id")
+        self.assertEqual(cur.fetchall(), [(1, 10)])
+
+    def test_tuples(self):
+        cur = self.conn.cursor()
+        psycopg2.extras.execute_values(cur,
+            "insert into testfast (id, date, val) values %s",
+            ((i, date(2017, 1, i + 1), i * 10) for i in range(10)))
+        cur.execute("select id, date, val from testfast order by id")
+        self.assertEqual(cur.fetchall(),
+            [(i, date(2017, 1, i + 1), i * 10) for i in range(10)])
+
+    def test_dicts(self):
+        cur = self.conn.cursor()
+        psycopg2.extras.execute_values(cur,
+            "insert into testfast (id, date, val) values %s",
+            (dict(id=i, date=date(2017, 1, i + 1), val=i * 10, foo="bar")
+                for i in range(10)),
+            template='(%(id)s, %(date)s, %(val)s)')
+        cur.execute("select id, date, val from testfast order by id")
+        self.assertEqual(cur.fetchall(),
+            [(i, date(2017, 1, i + 1), i * 10) for i in range(10)])
+
+    def test_many(self):
+        cur = self.conn.cursor()
+        psycopg2.extras.execute_values(cur,
+            "insert into testfast (id, val) values %s",
+            ((i, i * 10) for i in range(1000)))
+        cur.execute("select id, val from testfast order by id")
+        self.assertEqual(cur.fetchall(), [(i, i * 10) for i in range(1000)])
+
+    def test_pages(self):
+        cur = self.conn.cursor()
+        psycopg2.extras.execute_values(cur,
+            "insert into testfast (id, val) values %s",
+            ((i, i * 10) for i in range(25)),
+            page_size=10)
+
+        # last statement was 5 tuples (one parens is for the fields list)
+        self.assertEqual(sum(c == '(' for c in cur.query.decode('ascii')), 6)
+
+        cur.execute("select id, val from testfast order by id")
+        self.assertEqual(cur.fetchall(), [(i, i * 10) for i in range(25)])
+
+    def test_unicode(self):
+        cur = self.conn.cursor()
+        ext.register_type(ext.UNICODE, cur)
+        snowman = u"\u2603"
+
+        # unicode in statement
+        psycopg2.extras.execute_values(cur,
+            "insert into testfast (id, data) values %%s -- %s" % snowman,
+            [(1, 'x')])
+        cur.execute("select id, data from testfast where id = 1")
+        self.assertEqual(cur.fetchone(), (1, 'x'))
+
+        # unicode in data
+        psycopg2.extras.execute_values(cur,
+            "insert into testfast (id, data) values %s",
+            [(2, snowman)])
+        cur.execute("select id, data from testfast where id = 2")
+        self.assertEqual(cur.fetchone(), (2, snowman))
+
+        # unicode in both
+        psycopg2.extras.execute_values(cur,
+            "insert into testfast (id, data) values %%s -- %s" % snowman,
+            [(3, snowman)])
+        cur.execute("select id, data from testfast where id = 3")
+        self.assertEqual(cur.fetchone(), (3, snowman))
+
+    def test_invalid_sql(self):
+        cur = self.conn.cursor()
+        self.assertRaises(ValueError, psycopg2.extras.execute_values, cur,
+            "insert", [])
+        self.assertRaises(ValueError, psycopg2.extras.execute_values, cur,
+            "insert %s and %s", [])
+        self.assertRaises(ValueError, psycopg2.extras.execute_values, cur,
+            "insert %f", [])
+        self.assertRaises(ValueError, psycopg2.extras.execute_values, cur,
+            "insert %f %s", [])
+
+    def test_percent_escape(self):
+        cur = self.conn.cursor()
+        psycopg2.extras.execute_values(cur,
+            "insert into testfast (id, data) values %s -- a%%b",
+            [(1, 'hi')])
+        self.assert_(b'a%%b' not in cur.query)
+        self.assert_(b'a%b' in cur.query)
+
+        cur.execute("select id, data from testfast")
+        self.assertEqual(cur.fetchall(), [(1, 'hi')])
+
+
+testutils.decorate_all_tests(TestExecuteValues,
+    testutils.skip_before_postgres(8, 2))
+
+
+def test_suite():
+    return unittest.TestLoader().loadTestsFromName(__name__)
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/setup.py b/setup.py
index 9fc7dc4..a33c060 100644
--- a/setup.py
+++ b/setup.py
@@ -42,14 +42,11 @@ setup_kwargs = dict(
         'Development Status :: 3 - Alpha',
         'License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)',
         'Intended Audience :: Developers',
-        'Programming Language :: Python :: 2.6',
         'Programming Language :: Python :: 2.7',
-        'Programming Language :: Python :: 3.2',
-        'Programming Language :: Python :: 3.3',
-        'Programming Language :: Python :: 3.4',
         'Programming Language :: Python :: 3.5',
         'Programming Language :: Python :: 3.6',
         'Programming Language :: Python :: 3.7',
+        'Programming Language :: Python :: 3.8',
         'Programming Language :: Python :: Implementation :: CPython',
         'Programming Language :: Python :: Implementation :: PyPy',
         'Programming Language :: SQL',
@@ -75,7 +72,7 @@ if new_cffi:
             ],
         ))
 else:
-    from distutils.command.build_py import build_py as _build_py
+    from setuptools.command.build_py import build_py as _build_py
 
     class build_py(_build_py):
         has_been_run = False
diff --git a/tox.ini b/tox.ini
index 37597f8..bcc695a 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
 [tox]
-envlist=py26,py27,py32,py33,py34,py35,py36,py37,pypy,pypy3
+envlist=py27,py35,py36,py37,py38,pypy,pypy3
 
 [testenv]
 deps=

Debdiff

[The following lists of changes regard files as different if they have different names, permissions or owners.]

Files in second set of .debs but not in first

-rw-r--r--  root/root   /usr/lib/debug/.build-id/be/3303439ad6d2fb0d64c1a15d42ef9e6368d01e.debug
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/psycopg2cffi-2.9.0.egg-info/PKG-INFO
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/psycopg2cffi-2.9.0.egg-info/dependency_links.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/psycopg2cffi-2.9.0.egg-info/requires.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/psycopg2cffi-2.9.0.egg-info/top_level.txt

Files in first set of .debs but not in second

-rw-r--r--  root/root   /usr/lib/debug/.build-id/6e/c2bf74d63fbee37988ab4e6cec026cf8e55b9c.debug
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/psycopg2cffi-2.8.1.egg-info/PKG-INFO
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/psycopg2cffi-2.8.1.egg-info/dependency_links.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/psycopg2cffi-2.8.1.egg-info/requires.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/psycopg2cffi-2.8.1.egg-info/top_level.txt

No differences were encountered between the control files of package python3-psycopg2cffi

Control files of package python3-psycopg2cffi-dbgsym: lines which differ (wdiff format)

  • Build-Ids: 6ec2bf74d63fbee37988ab4e6cec026cf8e55b9c be3303439ad6d2fb0d64c1a15d42ef9e6368d01e

More details

Full run details