<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/rss/styles.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Matthew Brown</title><description>My little corner of the vast internet</description><link>https://matthewbrown.io/</link><language>en-us</language><lastBuildDate>Tue, 03 Feb 2026 11:11:21 GMT</lastBuildDate><managingEditor>me@matthewbrown.io (Matthew Brown)</managingEditor><webMaster>me@matthewbrown.io (Matthew Brown)</webMaster><item><title>FastAPI database session dependency injection considered harmful</title><link>https://matthewbrown.io/2026/02/03/fastapi-session-dependency-injection/</link><guid isPermaLink="true">https://matthewbrown.io/2026/02/03/fastapi-session-dependency-injection/</guid><pubDate>Tue, 03 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The common FastAPI pattern of injecting a database session via &lt;code&gt;Depends()&lt;/code&gt; creates a long-lived session that spans the entire request lifecycle. This causes subtle bugs.&lt;/p&gt;
&lt;h2&gt;The pattern&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

@app.get(&amp;quot;/users/{user_id}&amp;quot;)
def get_user(user_id: int, db: Session = Depends(get_db)):
    return db.query(User).filter(User.id == user_id).first()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The problems&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Stale reads.&lt;/strong&gt; SQLAlchemy&amp;#39;s identity map caches objects. If you read a user early in the request, then call an external service that modifies that user, subsequent reads return stale cached data.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;@app.post(&amp;quot;/process&amp;quot;)
def process(db: Session = Depends(get_db)):
    user = db.query(User).get(1)  # cached
    external_service.update_user(1)  # modifies user in another transaction
    user = db.query(User).get(1)  # still returns cached version
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Memory accumulation.&lt;/strong&gt; Every object loaded stays in the session&amp;#39;s identity map. Long-running requests that process many records accumulate memory.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Background tasks inherit dead sessions.&lt;/strong&gt; FastAPI background tasks run after the response is sent, but the session is already closed.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;@app.post(&amp;quot;/submit&amp;quot;)
def submit(background_tasks: BackgroundTasks, db: Session = Depends(get_db)):
    background_tasks.add_task(send_email, db)  # db is closed when this runs
    return {&amp;quot;status&amp;quot;: &amp;quot;ok&amp;quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Alternatives&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Explicit session management.&lt;/strong&gt; Create sessions where you need them with clear boundaries.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;@app.post(&amp;quot;/process&amp;quot;)
def process():
    with SessionLocal() as db:
        user = db.query(User).get(1)
        # do work
    # session closed, cache cleared

    with SessionLocal() as db:
        user = db.query(User).get(1)  # fresh read
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Expire on commit.&lt;/strong&gt; Use &lt;code&gt;expire_on_commit=True&lt;/code&gt; and commit frequently to force fresh reads. This is the default but often disabled.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Session-per-operation.&lt;/strong&gt; For read-heavy endpoints, use &lt;code&gt;db.expire_all()&lt;/code&gt; or create short-lived sessions for each logical operation.&lt;/p&gt;
&lt;p&gt;The convenience of dependency injection hides the session lifecycle from you. Sometimes that&amp;#39;s fine. Often it&amp;#39;s not.&lt;/p&gt;
</content:encoded><category>notes</category><author>Matthew Brown &lt;me@matthewbrown.io&gt;</author></item><item><title>pytest asyncio testing without plugins</title><link>https://matthewbrown.io/2026/02/03/pytest-asyncio-without-plugins/</link><guid isPermaLink="true">https://matthewbrown.io/2026/02/03/pytest-asyncio-without-plugins/</guid><description>Python 3.11 introduced asyncio.Runner which allows async test support without pytest-asyncio.</description><pubDate>Tue, 03 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Python 3.11 introduced &lt;code&gt;asyncio.Runner&lt;/code&gt; which makes it possible to run async tests in pytest without the &lt;code&gt;pytest-asyncio&lt;/code&gt; plugin. We&amp;#39;ve been using this approach for a few months now.&lt;/p&gt;
&lt;h2&gt;Configuration&lt;/h2&gt;
&lt;p&gt;Add this to your root &lt;code&gt;conftest.py&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import asyncio
import inspect
from contextlib import contextmanager
from typing import AsyncGenerator, Generator, TypeVar

import pytest

T = TypeVar(&amp;quot;T&amp;quot;)


@contextmanager
def async_to_sync_generator(
    runner: asyncio.Runner, agen: AsyncGenerator[T, None]
) -&amp;gt; Generator[T | None, None, None]:
    async def get_next(agen):
        try:
            return await agen.__anext__(), None
        except StopAsyncIteration:
            return None, StopIteration
        except Exception as e:
            return None, e

    try:
        while True:
            value, exc = runner.run(get_next(agen))
            if exc is not None:
                if exc is StopIteration:
                    break
                raise exc
            yield value
    finally:
        runner.run(agen.aclose())


def pytest_sessionstart(session):
    session.asyncio_runner = asyncio.Runner()


def pytest_sessionfinish(session):
    if hasattr(session, &amp;quot;asyncio_runner&amp;quot;):
        session.asyncio_runner.close()


@pytest.hookimpl(hookwrapper=True)
def pytest_fixture_setup(fixturedef, request):
    if inspect.isasyncgenfunction(fixturedef.func):
        generator = fixturedef.func

        def gen_wrapper(*args, **kwargs):
            runner = request.session.asyncio_runner
            with async_to_sync_generator(runner, generator(*args, **kwargs)) as v:
                yield v

        fixturedef.func = gen_wrapper

    elif inspect.iscoroutinefunction(fixturedef.func):
        coro = fixturedef.func

        def coro_wrapper(*args, **kwargs):
            return request.session.asyncio_runner.run(coro(*args, **kwargs))

        fixturedef.func = coro_wrapper

    yield


@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_call(item):
    if inspect.iscoroutinefunction(item.obj):
        runner = item.session.asyncio_runner
        original = item.obj

        def wrapper(*args, **kwargs):
            return runner.run(original(*args, **kwargs))

        item.obj = wrapper

    yield
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Writing tests&lt;/h2&gt;
&lt;p&gt;No decorators needed. Just write async functions:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;async def test_create_user(session, factory):
    user = await factory(UserFactory, name=&amp;quot;test&amp;quot;)
    assert user.id is not None

async def test_fetch_user(session, factory):
    user = await factory(UserFactory)
    result = await get_user(session, user.id)
    assert result.name == user.name
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Async fixtures work the same way:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;@pytest.fixture
async def user(factory):
    return await factory(UserFactory)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Database test isolation&lt;/h2&gt;
&lt;p&gt;Wrap each test in a transaction that rolls back:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;@pytest.fixture(scope=&amp;quot;function&amp;quot;, autouse=True)
async def test_transaction(engine: AsyncEngine):
    async with engine.connect() as connection:
        transaction = await connection.begin()
        try:
            session = AsyncSession(bind=connection, expire_on_commit=False)
            yield session
        finally:
            await session.close()
            await transaction.rollback()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All changes made during a test are rolled back automatically.&lt;/p&gt;
&lt;h2&gt;Factory fixture&lt;/h2&gt;
&lt;p&gt;Use factories with explicit session management instead of mocking:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;T = TypeVar(&amp;quot;T&amp;quot;, bound=BaseFactory)

@pytest.fixture
async def factory(session: AsyncSession):
    async def fn(factory: type[T], **overrides):
        instance = factory.build(**overrides)
        session.add(instance)
        await session.flush()
        return instance

    return fn
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Usage:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;async def test_order_total(factory):
    user = await factory(UserFactory)
    order = await factory(OrderFactory, user=user, amount=100)
    assert order.user_id == user.id
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Why a single event loop matters&lt;/h2&gt;
&lt;p&gt;The default &lt;code&gt;pytest-asyncio&lt;/code&gt; behaviour creates a new event loop per test. This causes problems:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Database connection pooling breaks.&lt;/strong&gt; SQLAlchemy&amp;#39;s async engine maintains connections on the event loop. New loop = new connections = pool exhaustion.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fixtures and tests on different loops.&lt;/strong&gt; &lt;a href=&quot;https://github.com/pytest-dev/pytest-asyncio/issues/744&quot;&gt;A known issue&lt;/a&gt; where session-scoped fixtures run on a different loop than tests.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;aiohttp client sessions.&lt;/strong&gt; These need to be created and closed on the same loop.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;code&gt;asyncio.Runner&lt;/code&gt; approach uses one loop for the entire session, avoiding all of this.&lt;/p&gt;
&lt;h2&gt;Known pitfalls&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Silent test failures.&lt;/strong&gt; The biggest risk with any async detection approach. If an async test isn&amp;#39;t properly awaited, it passes without running. Earlier pytest versions &lt;a href=&quot;https://quantlane.com/blog/make-sure-asyncio-tests-always-run/&quot;&gt;silently ignored&lt;/a&gt; unmarked async tests entirely. The hook-based approach handles this, but watch for the warning: &lt;code&gt;RuntimeWarning: coroutine &amp;#39;test_x&amp;#39; was never awaited&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Async generator cleanup.&lt;/strong&gt; The &lt;code&gt;async_to_sync_generator&lt;/code&gt; must call &lt;code&gt;aclose()&lt;/code&gt; in the finally block. Missing this causes resource leaks with async context managers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;No multi-framework support.&lt;/strong&gt; This only works with asyncio. If you need trio or curio, use &lt;code&gt;pytest-asyncio&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Similar projects&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/delfick/alt-pytest-asyncio&quot;&gt;alt-pytest-asyncio&lt;/a&gt; takes the same approach as a proper plugin:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Single event loop for the session&lt;/li&gt;
&lt;li&gt;Auto-detects async tests (no decorators)&lt;/li&gt;
&lt;li&gt;Cleaner error tracebacks (no asyncio internals)&lt;/li&gt;
&lt;li&gt;Python 3.11+ only&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you want a maintained plugin rather than copying conftest code, use that.&lt;/p&gt;
&lt;h2&gt;Comparison&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Advantages:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No &lt;code&gt;@pytest.mark.asyncio&lt;/code&gt; decorators&lt;/li&gt;
&lt;li&gt;Single event loop per session (connection pooling works)&lt;/li&gt;
&lt;li&gt;Fixtures and tests guaranteed same loop&lt;/li&gt;
&lt;li&gt;No plugin version compatibility issues&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Limitations:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;More setup code in conftest.py&lt;/li&gt;
&lt;li&gt;Less documentation&lt;/li&gt;
&lt;li&gt;Python 3.11+ only&lt;/li&gt;
&lt;li&gt;asyncio only (no trio/curio)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>notes</category><author>Matthew Brown &lt;me@matthewbrown.io&gt;</author></item><item><title>Using the node 18+ native test runner with TypeScript and React</title><link>https://matthewbrown.io/2025/09/04/node-test-runner/</link><guid isPermaLink="true">https://matthewbrown.io/2025/09/04/node-test-runner/</guid><description>Node.js 18 introduced a native test runner that eliminates the need for jest, mocha or vitest in basic scenarios.</description><pubDate>Thu, 04 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Node.js 18 introduced a native test runner that eliminates the need for jest, mocha or vitest in basic scenarios. We&amp;#39;ve been using it exclusively for the past couple of weeks with great success.&lt;/p&gt;
&lt;h2&gt;Configuration&lt;/h2&gt;
&lt;p&gt;The test script in package.json:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;scripts&amp;quot;: {
    &amp;quot;test&amp;quot;: &amp;quot;node --import global-jsdom/register --import tsx --experimental-transform-types --experimental-test-module-mocks --test&amp;quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Flag breakdown:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--import tsx&lt;/code&gt;: Handles TypeScript compilation&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--import global-jsdom/register&lt;/code&gt;: Provides DOM environment for React components&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--experimental-transform-types&lt;/code&gt;: Strips TypeScript types during execution&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--experimental-test-module-mocks&lt;/code&gt;: Enables module mocking&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--test&lt;/code&gt;: Activates Node&amp;#39;s test runner&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Dependencies&lt;/h2&gt;
&lt;p&gt;Required dev dependencies:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pnpm add --save-dev @testing-library/react global-jsdom jsdom tsx
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;TypeScript Unit Tests&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { describe, test } from &amp;quot;node:test&amp;quot;;
import { strict as assert } from &amp;quot;node:assert&amp;quot;;
import { getInitials } from &amp;quot;./stringUtils&amp;quot;;

describe(&amp;quot;String Utils&amp;quot;, () =&amp;gt; {
  test(&amp;quot;should return correct initials&amp;quot;, () =&amp;gt; {
    const result = getInitials(&amp;quot;John Smith&amp;quot;);
    assert.equal(result, &amp;quot;JS&amp;quot;);
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Uses Node&amp;#39;s built-in assert module for assertions.&lt;/p&gt;
&lt;h2&gt;React Component Testing&lt;/h2&gt;
&lt;p&gt;React Testing Library can still work with jsdom to provide a DOM.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import React from &amp;quot;react&amp;quot;;
import { describe, test } from &amp;quot;node:test&amp;quot;;
import { strict as assert } from &amp;quot;node:assert&amp;quot;;
import { render, fireEvent, screen, cleanup } from &amp;quot;@testing-library/react&amp;quot;;
import { Toggle } from &amp;quot;./Toggle&amp;quot;;

beforeEach(() =&amp;gt; {
  cleanup(); // cleans the dom state between each test
});

describe(&amp;quot;Toggle&amp;quot;, () =&amp;gt; {
  test(&amp;quot;calls onToggleClick when clicked&amp;quot;, () =&amp;gt; {
    let callCount = 0;
    const { getByText } = render(
      &amp;lt;Toggle 
        options={[&amp;quot;On&amp;quot;, &amp;quot;Off&amp;quot;]}
        selected=&amp;quot;On&amp;quot;
        onToggleClick={() =&amp;gt; callCount++}
      /&amp;gt;
    );
    
    fireEvent.click(getByText(&amp;quot;On&amp;quot;));
    assert.equal(callCount, 1);
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Running Tests&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# All tests
pnpm test

# Watch mode
pnpm test -- --watch

# Specific file
node --import global-jsdom/register --import tsx --experimental-transform-types --test src/components/Toggle.test.tsx
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Comparison&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Advantages:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No configuration files required&lt;/li&gt;
&lt;li&gt;Direct TypeScript execution&lt;/li&gt;
&lt;li&gt;Built into Node.js runtime&lt;/li&gt;
&lt;li&gt;Smaller dependency footprint&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Limitations:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Requires Node 18+&lt;/li&gt;
&lt;li&gt;Uses experimental flags&lt;/li&gt;
&lt;li&gt;Smaller ecosystem than Jest, vitest, etc. This means fewer examples&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>notes</category><author>Matthew Brown &lt;me@matthewbrown.io&gt;</author></item><item><title>20 days, one porsche, and a greek wedding.</title><link>https://matthewbrown.io/2015/07/14/london-to-greece-porsche-997/</link><guid isPermaLink="true">https://matthewbrown.io/2015/07/14/london-to-greece-porsche-997/</guid><pubDate>Sat, 14 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Image } from &amp;#39;astro:assets&amp;#39;;
import readyToGo from &amp;#39;../images/greece/ready to go in london.jpeg&amp;#39;;
import waitingFolkestone from &amp;#39;../images/greece/waiting in folkstone.jpeg&amp;#39;;
import goodbyeLondon from &amp;#39;../images/greece/goodbye london.jpeg&amp;#39;;
import antwerp1 from &amp;#39;../images/greece/antwerp.jpeg&amp;#39;;
import antwerp2 from &amp;#39;../images/greece/antwerp2.jpeg&amp;#39;;
import antwerp3 from &amp;#39;../images/greece/antwerp3.jpeg&amp;#39;;
import germanCoffee from &amp;#39;../images/greece/german service station iced coffee.jpeg&amp;#39;;
import dresdenArrival from &amp;#39;../images/greece/arriving in dresden.JPG&amp;#39;;
import dresdenFriends from &amp;#39;../images/greece/dresden friends.JPG&amp;#39;;
import lamboLeMans from &amp;#39;../images/greece/lambo to le mans.jpeg&amp;#39;;&lt;/p&gt;
&lt;h2&gt;The Setup&lt;/h2&gt;
&lt;p&gt;I&amp;#39;ve done my fair share of long road trips but when my Greek boss invited me to his &amp;quot;big fat Greek wedding&amp;quot;, I had to make something of it.. yet another crazy plan.. could I buy a fun car, and skip the summer airport choas, and see some of the balkans enroute. &lt;/p&gt;
&lt;p&gt;My wife had to work the week before the wedding, so the journey would start solo - we&amp;#39;d reunite in Greece and make the return journey together as a dynamic duo.&lt;/p&gt;
&lt;h2&gt;Finding the Perfect Steed&lt;/h2&gt;
&lt;p&gt;Over a couple of months, I became a little obsessed over the decision. Coupe or cabriolet? 996, 997, or Boxster 981? The internet unanimously voted cabriolet, but being a &amp;quot;ranga&amp;quot; (albeit an Aussie ranga), I knew better than to subject my pale skin to 20 days of European summer sun. &lt;/p&gt;
&lt;p&gt;After test driving two 997s and a Boxster 981 S PDK, I couldn&amp;#39;t feel much difference in performance – but the 997 looked 100x better. I found my match: a very well-maintained 2005 Tiptronic 997.1 (rego FMZ 997), which catches the light just right and looks blue in certain conditions. Given this blue tinge it was almost immediately nicknamed: &amp;quot;Bluey,&amp;quot; after the world&amp;#39;s most beloved Aussie dog.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Bore scope verified ✓&lt;/li&gt;
&lt;li&gt;MOT and maintenance history reviewed ✓&lt;/li&gt;
&lt;li&gt;IMS bearing variant checked against engine number ✓&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I rang the specialist (GT One) that had recently serviced it - they noted nothing of interest on the post-service report, and given it&amp;#39;s recent service history, bore scope and great maintenance record I didn&amp;#39;t do the standard PPI. Time will tell if this was a good idea or not.&lt;/p&gt;
&lt;p&gt;There&amp;#39;s always risk with these long road trips. Really, the only car I&amp;#39;d 100% trust to make it there and back is a Toyota Land Cruiser – but it&amp;#39;s neither economical nor European, and where&amp;#39;s the fun in that?&lt;/p&gt;
&lt;p&gt;Insuring it was a challenge (many specialist insurers didn&amp;#39;t want to insure it with on-street parking in London) but Admiral were happy to - after I gave them one of my kidneys. Also got European wide road side assistance with AA - just in case.&lt;/p&gt;
&lt;p&gt;High vis and UK sticker purchased - it was time to get a move on.&lt;/p&gt;
&lt;h2&gt;The Route (ever changing)&lt;/h2&gt;
&lt;p&gt;Originally, I planned to take the scenic route through Western Europe to Slovenia, then down the Adriatic coast through Croatia, Bosnia &amp;amp; Herzegovina, Montenegro, and Albania. Insurance and time constraints had other ideas – I needed to stay within the EU.&lt;/p&gt;
&lt;p&gt;A last-minute route change (now via Serbia and Bulgaria) meant I did get to add in a short stop in Dresden to visit old friends.&lt;/p&gt;
&lt;h2&gt;The Journey Begins&lt;/h2&gt;
&lt;h3&gt;Day 1: London to Antwerp&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;UK, France, Belgium&lt;/em&gt;&lt;/p&gt;
&lt;Image src={readyToGo} alt=&quot;Ready to go in London&quot; class=&quot;portrait&quot; /&gt;


&lt;p&gt;Woke up early Thursday morning after not sleeping super well - put that down to pre-trip nerves. Made the 9:36 (albeit delayed by an hour) Le Shuttle to Calais – there&amp;#39;s something super cool about driving your car onto a train that goes under the sea. The train was absolutely packed with supercars heading to Le Mans, so I slipped past all the kids with cameras in Calais like a no descript 15-year-old Ford Focus. &lt;/p&gt;
&lt;Image src={lamboLeMans} alt=&quot;Lambo to Le Mans&quot; class=&quot;portrait&quot; /&gt;

&lt;Image src={waitingFolkestone} alt=&quot;Waiting in Folkestone&quot; class=&quot;portrait&quot; /&gt;

&lt;Image src={goodbyeLondon} alt=&quot;Goodbye London&quot; class=&quot;portrait&quot; /&gt;

&lt;p&gt;The hop from Calais to Antwerp was quick and uneventful, arriving at the hotel by 3 PM. First concern: Bluey was making a funny &amp;quot;clonking&amp;quot; noise from the front suspension. Apparently common with 997s, but not exactly what you want to hear at the start of a 5,000km journey. It also only happens after highway driving for hours, so had not made itself known on the 200 mile shake down trip I&amp;#39;d done the weekend after I bought it.&lt;/p&gt;
&lt;p&gt;Did a bit of work (always more to do), then took a bus into Antwerp&amp;#39;s city center. Found a proper frituur for dinner – because when in Belgium, you eat frites – before jumping into bed early. Tomorrow would be the first real test.&lt;/p&gt;
&lt;Image src={antwerp1} alt=&quot;Antwerp&quot; class=&quot;portrait&quot; /&gt;

&lt;Image src={antwerp2} alt=&quot;Antwerp 2&quot; class=&quot;portrait&quot; /&gt;

&lt;Image src={antwerp3} alt=&quot;Antwerp 3&quot; class=&quot;portrait&quot; /&gt;

&lt;h3&gt;Day 2: Antwerp to Dresden&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;Belgium, Netherlands, Germany&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Left Antwerp just after 7 AM, pushing through thick Belgian and Dutch commuter traffic. Nothing quite prepares you for the organized chaos of morning rush hour in the Low Countries.&lt;/p&gt;
&lt;p&gt;Another route change on the fly – realized I didn&amp;#39;t have the appropriate environmental stickers (Umweltplakette) for Dortmund or Düsseldorf. Headed south instead via Köln, then southeast toward Frankfurt before turning north again on the A4 toward Dresden. &lt;/p&gt;
&lt;p&gt;Multiple fuel stops along the way – the 997 getting ~550km to a tank but still chews through fuel. I also had some truely bad road side coffee. I can&amp;#39;t wait for speciality coffee to arrive at service stations.&lt;/p&gt;
&lt;Image src={germanCoffee} alt=&quot;German service station iced coffee&quot; class=&quot;portrait&quot; /&gt;

&lt;p&gt;Arrived in Dresden with Bluey running strong, though that suspension clonk re-appeared, made extra apparent by the cobblestones into Dresden. I&amp;#39;ve since quasi-diagnosed it as one of the common 997 suspension issues that can wait until we&amp;#39;re back in the UK. (Famous last words?)&lt;/p&gt;
&lt;Image src={dresdenArrival} alt=&quot;Arriving in Dresden&quot; class=&quot;portrait&quot; /&gt;

&lt;p&gt;My German friends (the Uli&amp;#39;s) always open their arms and house with incredible hospitality so I&amp;#39;ve spent the last 24 hours surrounded by friends new and old.&lt;/p&gt;
&lt;Image src={dresdenFriends} alt=&quot;Dresden friends&quot; class=&quot;landscape&quot; /&gt;


&lt;p&gt;&lt;strong&gt;more to come...&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Impressions&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;The Good:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The power is immense. You can be cruising at 150km/h, apply a little more throttle, and the car jumps forward like a startled thoroughbred. The flat-six pulls hard.&lt;/li&gt;
&lt;li&gt;The 8-disc CD changer is surprisingly awesome in 2024. Endless music without touching your phone. Also spent time scanning local radio stations, listening to incomprehensible chatter about news, gossip, and politics.&lt;/li&gt;
&lt;li&gt;The sunroof was absolutely the right choice. Windows down, sunroof open – you feel connected to the world. Button everything up, and you&amp;#39;re wrapped in a cocoon of German engineering.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;The Reality Check:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Road noise really grates after a couple of hours. I&amp;#39;m wearing high-fidelity earplugs that help, but it&amp;#39;s still prominent.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>travel</category><author>Matthew Brown &lt;me@matthewbrown.io&gt;</author></item><item><title>Defining AI tools in python</title><link>https://matthewbrown.io/2025/03/26/ai-tools-in-python/</link><guid isPermaLink="true">https://matthewbrown.io/2025/03/26/ai-tools-in-python/</guid><pubDate>Wed, 26 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;With the explosion of agent frameworks we’re seeing different approaches to defining agent “tools” in python. The two immediately obvious approaches are using the &lt;code&gt;docstring&lt;/code&gt; or using a &lt;code&gt;pydantic model&lt;/code&gt; to define the tool arguments. &lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Opinions wanted&lt;/strong&gt;: I don’t have an agent framework to shill, nor do I really have a strong opinion about either right now. &lt;/p&gt;
&lt;p&gt;I also reserve the right to change the pros and cons as they become obvious over time. &lt;/p&gt;
&lt;p&gt;What do you think? Comment on &lt;a href=&quot;https://news.ycombinator.com/item?id=43480745&quot;&gt;Hacker News&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Option 1: docstring&lt;/h2&gt;
&lt;p&gt;A simple python function with an optional docstring to define additional context (like field descriptions).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def search_customer(postcode: str | None = None):
	&amp;quot;&amp;quot;&amp;quot;Search for customers.

    Args:
        postcode: The customer&amp;#39;s postcode
    &amp;quot;&amp;quot;&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Pros&lt;/h4&gt;
&lt;p&gt;&lt;em&gt;This style is used by lots of agent frameworks:&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PydanticAI (&lt;a href=&quot;https://ai.pydantic.dev/tools/#function-tools-and-schema&quot;&gt;see docs&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;OpenAI Agents Framework (&lt;a href=&quot;https://openai.github.io/openai-agents-python/tools/&quot;&gt;see docs&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;MCP Python SDK (&lt;a href=&quot;https://github.com/modelcontextprotocol/python-sdk?tab=readme-ov-file#quickstart&quot;&gt;see docs&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Internally all of these tools convert the signature + docstring into a pydantic model…&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Can’t do funny things with models like union types.&lt;/em&gt; For example &lt;code&gt;SearchCustomerParamsV1 | SearchCustomerParamsV2&lt;/code&gt; which produces &lt;code&gt;anyOf&lt;/code&gt; schemas which aren’t supported by the models.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;You’re just writing normal python functions.&lt;/em&gt; Type hints in function signatures are more visible in IDEs without having to look at model definitions. Better integration with existing tooling (mypy, pylint, etc.)&lt;/p&gt;
&lt;h4&gt;Cons&lt;/h4&gt;
&lt;p&gt;&lt;em&gt;Action arguments interleaved with magic/injected variables.&lt;/em&gt;  For example in the below example it may not be super clear which arguments are model provided vs dependency injected.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;@tool()
def search_customer(
	postcode: str | None = None, # tool argument
	ctx: Context,  # special variable name/type which is injected by us
	# dependency injected arguments (FastAPI style)
	crm_client: Client = Depends(get_crm_client),
    crm_customer: Customer | None = Depends(get_crm_customer),
    session: Session = Depends(get_db_session),
):
	&amp;quot;&amp;quot;&amp;quot;Search for customers.

    Args:
        postcode: The customer&amp;#39;s postcode
  &amp;quot;&amp;quot;&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Documentation and constraints are separated from the type definitions&lt;/em&gt; (i.e. no pydantic validation). Also &lt;code&gt;docstrings&lt;/code&gt; can be unwieldy and long winded. &lt;/p&gt;
&lt;h2&gt;Option 2: pydantic model&lt;/h2&gt;
&lt;p&gt;In this case the function has one special argument called &lt;code&gt;arguments&lt;/code&gt; or &lt;code&gt;params&lt;/code&gt; which is a pydantic model that represents the tool arguments. &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class SearchCustomerArguments(BaseModel):
    postcode: str | None = Field(description=&amp;quot;The customer&amp;#39;s postcode&amp;quot;)

@tool()
def search_customer(arguments: SearchCustomerArguments):
	...
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Pros&lt;/h4&gt;
&lt;p&gt;&lt;em&gt;Very similar to FastAPI style&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;@tool()
async def create_diary_event(
    arguments: CreateDiaryEventV1Args,
    crm_client: Client = Depends(get_crm_client),
    crm_customer: Customer | None = Depends(get_crm_customer),
	session: Session = Depends(get_db_session),
):
    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Access to some useful pydantic features&lt;/em&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Validation (field_validator, model_validator,  Field(gt=0), etc)&lt;/li&gt;
&lt;li&gt;Field descriptions&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;Cons:&lt;/h4&gt;
&lt;p&gt;&lt;em&gt;Not easily adaptable:&lt;/em&gt; Difficult to adapt to other MCP server with your tools we’d you&amp;#39;d have to explode the pydantic model into a standard python function, or dive into the internals of the agent frameworks.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Unsupported pydantic features:&lt;/em&gt; Can use pydantic features that are not supported by models (union → anyOf, nested models, etc) &lt;/p&gt;
&lt;h3&gt;Option 3: hybrid/support both&lt;/h3&gt;
&lt;p&gt;In this scenario the &lt;code&gt;@tool()&lt;/code&gt; magic would inspect the function signature:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If it finds an argument called &lt;code&gt;arguments&lt;/code&gt; assumes it’s a pydantic model uses “Option 2”, or&lt;/li&gt;
&lt;li&gt;It uses Option 1&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In this case we could use the docstring approach for simple functions with few parameters/limited validation, and pydantic models where we want validation.&lt;/p&gt;
&lt;p&gt;This is similar to &lt;a href=&quot;https://python.langchain.com/docs/how_to/custom_tools/#creating-tools-from-functions&quot;&gt;LangChain&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;What are your thoughts?&lt;/p&gt;
</content:encoded><category>notes</category><author>Matthew Brown &lt;me@matthewbrown.io&gt;</author></item><item><title>pytest snippets for python 3.11+</title><link>https://matthewbrown.io/2024/05/19/notes-on-pytest/</link><guid isPermaLink="true">https://matthewbrown.io/2024/05/19/notes-on-pytest/</guid><pubDate>Sun, 23 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Below is a set of snippets that I used frequently when using pytest in the later versions of python.&lt;/p&gt;
&lt;h3&gt;asyncio tests without any plugins&lt;/h3&gt;
&lt;p&gt;The following snippet adds very basic support to pytest for async tests in python 3.11 and above.
Add it to your &lt;code&gt;conftest.py&lt;/code&gt; and async tests will run in an an &lt;code&gt;asyncio.Runner&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import asyncio
import inspect
import logging
from contextlib import contextmanager
from typing import Any, AsyncGenerator, Generator, TypeVar

import pytest

# Set up logging for debugging
logger = logging.getLogger(__name__)

T = TypeVar(&amp;quot;T&amp;quot;)


@contextmanager
def async_to_sync_generator(
    runner: asyncio.Runner, agen: AsyncGenerator[T, Any]
) -&amp;gt; Generator[T | None, None, None]:
    &amp;quot;&amp;quot;&amp;quot;
    Converts an async generator into a synchronous generator using the provided asyncio.Runner.
    &amp;quot;&amp;quot;&amp;quot;

    async def get_next(agen):
        try:
            return await anext(agen), None
        except StopAsyncIteration:
            return None, StopIteration
        except Exception as e:
            return None, e

    try:
        while True:
            value, exc = runner.run(get_next(agen))
            if exc is not None:
                if exc is StopIteration:
                    break
                raise exc
            yield value
    finally:
        # Ensure cleanup for the async generator
        runner.run(agen.aclose())


def pytest_sessionstart(session):
    &amp;quot;&amp;quot;&amp;quot;Initialize an asyncio.Runner for the pytest session.&amp;quot;&amp;quot;&amp;quot;
    if not hasattr(asyncio, &amp;quot;Runner&amp;quot;):
        pytest.exit(&amp;quot;This plugin requires Python 3.11 or higher&amp;quot;, returncode=1)
    session.asyncio_runner = asyncio.Runner()
    logger.debug(&amp;quot;Asyncio Runner initialized for the session.&amp;quot;)


def pytest_sessionfinish(session):
    &amp;quot;&amp;quot;&amp;quot;Close the asyncio.Runner after the session ends.&amp;quot;&amp;quot;&amp;quot;
    if hasattr(session, &amp;quot;asyncio_runner&amp;quot;):
        try:
            session.asyncio_runner.close()
            logger.debug(&amp;quot;Asyncio Runner closed after the session.&amp;quot;)
        except Exception as e:
            logger.error(f&amp;quot;Error closing asyncio Runner: {e}&amp;quot;)


async def anext(agen: AsyncGenerator):
    &amp;quot;&amp;quot;&amp;quot;Fetch the next item from an async generator.&amp;quot;&amp;quot;&amp;quot;
    return await agen.__anext__()


@pytest.hookimpl(hookwrapper=True)
def pytest_fixture_setup(fixturedef, request):
    &amp;quot;&amp;quot;&amp;quot;
    Convert async fixtures into their synchronous counterparts, handling both async generators and coroutines.
    &amp;quot;&amp;quot;&amp;quot;
    if inspect.isasyncgenfunction(fixturedef.func):
        generator = fixturedef.func

        def gen_wrapper(*args, **kwargs):
            runner: asyncio.Runner = request.session.asyncio_runner
            gen_obj = generator(*args, **kwargs)
            with async_to_sync_generator(runner, gen_obj) as v:
                yield v

        fixturedef.func = gen_wrapper
        logger.debug(f&amp;quot;Converted async generator fixture {fixturedef.func} to sync.&amp;quot;)

    elif inspect.iscoroutinefunction(fixturedef.func):
        coro = fixturedef.func
        runner: asyncio.Runner = request.session.asyncio_runner

        def coro_wrapper(*args, **kwargs):
            return runner.run(coro(*args, **kwargs))

        fixturedef.func = coro_wrapper
        logger.debug(f&amp;quot;Converted coroutine fixture {fixturedef.func} to sync.&amp;quot;)

    yield


@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_call(item):
    &amp;quot;&amp;quot;&amp;quot;
    Convert async test functions to sync functions by running them in the asyncio.Runner.
    &amp;quot;&amp;quot;&amp;quot;
    if inspect.iscoroutinefunction(item.obj):
        runner = item.session.asyncio_runner
        original = item.obj

        def item_wrapper(*args, **kwargs):
            return runner.run(original(*args, **kwargs))

        item.obj = item_wrapper
        logger.debug(f&amp;quot;Converted async test {item.obj} to sync.&amp;quot;)

    yield
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;integration tests&lt;/h4&gt;
&lt;p&gt;Mark tests as integration tests that don&amp;#39;t run by default.
You have to run pytest with the &lt;code&gt;--integration&lt;/code&gt; flag to run them.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def pytest_addoption(parser):
    parser.addoption(
        &amp;quot;--integration&amp;quot;,
        action=&amp;quot;store_true&amp;quot;,
        default=False,
        help=&amp;quot;Run tests marked with @pytest.mark.integration&amp;quot;,
    )


def pytest_configure(config):
    config.addinivalue_line(
        &amp;quot;markers&amp;quot;, &amp;quot;integration: mark test to run only with --integration&amp;quot;
    )


def pytest_collection_modifyitems(config, items):
    if config.getoption(&amp;quot;--integration&amp;quot;):
        # Only run tests marked with &amp;#39;integration&amp;#39;
        integration_items = [item for item in items if &amp;quot;integration&amp;quot; in item.keywords]
        items[:] = integration_items
    else:
        # Skip tests marked with &amp;#39;integration&amp;#39;
        skip_integration = pytest.mark.skip(reason=&amp;quot;Need --integration option to run&amp;quot;)
        for item in items:
            if &amp;quot;integration&amp;quot; in item.keywords:
                item.add_marker(skip_integration)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;sqlalchemy async support with factory-boy&lt;/h4&gt;
&lt;p&gt;I find passing in an explicit session easier than the built in factory-boy session local behaviour.
So this is a little snippet adds factory generated instances to the current session.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;
# factory
T = TypeVar(&amp;quot;T&amp;quot;, bound=BaseFactory)
Factory = Callable[Concatenate[Type[T], ...], Awaitable[T]]


@pytest.fixture(scope=&amp;quot;function&amp;quot;, name=&amp;quot;factory&amp;quot;)
async def factory_fixture(session: AsyncSession):
    &amp;quot;&amp;quot;&amp;quot;
    Creates a factory fixture for creating models.
    Explicitly uses the current active session
    &amp;quot;&amp;quot;&amp;quot;

    async def fn(factory: Type[T], **overrides):
        instance = factory.build(**overrides)
        session.add(instance)
        await session.flush()
        return instance

    return fn

### usage

async def test_user(factory: Factory):
    instance = await factory(UserFactory)
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>notes</category><author>Matthew Brown &lt;me@matthewbrown.io&gt;</author></item><item><title>Flying notes</title><link>https://matthewbrown.io/2022/09/23/flying-notes/</link><guid isPermaLink="true">https://matthewbrown.io/2022/09/23/flying-notes/</guid><pubDate>Fri, 23 Sep 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The following is my notes about flying GA to Europe from the UK.
Last updated: 2022-09-23&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.sia.aviation-civile.gouv.fr/documents/htmlshow?f=dvd/eAIP_08_SEP_2022/FRANCE/home.html&quot;&gt;French AIP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://eaip.lvnl.nl/2022-09-22-AIRAC/html/index-en-GB.html&quot;&gt;Dutch AIP&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Le Touquet, France&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Notice: 2 hours before ETD (but seem flexible)&lt;/li&gt;
&lt;li&gt;Service Hours: ATS&lt;ul&gt;
&lt;li&gt;Summer: 0700-1130, 1300-1700.&lt;/li&gt;
&lt;li&gt;Winter: 0800-1230, 1330-1800.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Handling: Not required&lt;/li&gt;
&lt;li&gt;Landing fee: 25 euro (&lt;a href=&quot;https://www.aeroport-letouquet.com/wp-content/uploads/2019/06/TARIFS_UK-010119.pdf&quot;&gt;Schedule&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Runways: 13/31&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Before you go:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;File a flight plan through SkyDeamon. If flying VFR you don&amp;#39;t need to enter an altitude.&lt;/li&gt;
&lt;li&gt;UK GAR via &lt;a href=&quot;https://www.onlinegar.com/&quot;&gt;OnlineGAR&lt;/a&gt; for the outbound and return.&lt;/li&gt;
&lt;li&gt;LeTouquet customs notification via &lt;a href=&quot;https://www.aeroport-letouquet.com/douanes-informations-de-vol/?lang=en&quot;&gt;https://www.aeroport-letouquet.com/douanes-informations-de-vol/?lang=en&lt;/a&gt; (onlinegar can also send this)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Calais, France&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Notice: 2 hours before ETA&lt;/li&gt;
&lt;li&gt;Service: AFIS&lt;ul&gt;
&lt;li&gt;From 01/03 to 30/09, WED-FRI 0800-1600, SAT-SUN 0800-1100, 1200-1600.&lt;/li&gt;
&lt;li&gt;From 01/10 to 29/02 : TUE-SAT 0800-1500.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Handling: Not required&lt;/li&gt;
&lt;li&gt;Landing Fee: 10 euro (&lt;a href=&quot;http://www.aeroport.capcalaisis.fr/wp-content/uploads/2021/12/TARIFS-AEROPORT-2021-V2.pdf&quot;&gt;Schedule&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Runways: 06/24&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Before you go:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;File a flight plan through SkyDeamon. If flying VFR you don&amp;#39;t need to enter an altitude.&lt;/li&gt;
&lt;li&gt;UK GAR via &lt;a href=&quot;https://www.onlinegar.com/&quot;&gt;OnlineGAR&lt;/a&gt; for the outbound and return.&lt;/li&gt;
&lt;li&gt;FR customs notification via &lt;a href=&quot;http://www.aeroport.capcalaisis.fr/pilot/&quot;&gt;http://www.aeroport.capcalaisis.fr/pilot/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Ostend, Belgium&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://www.ostendebruges-aeroport.com/wp-content/uploads/2021/04/General-Aviation-Info-Sheet-v28APR2021-1.pdf&quot;&gt;Information Sheet&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Notice: Not required&lt;/li&gt;
&lt;li&gt;Hours: ATS 0700-2200&lt;/li&gt;
&lt;li&gt;Handling: Optional&lt;/li&gt;
&lt;li&gt;Landing fee: ~25 euros (&lt;a href=&quot;https://www.ostendebruges-aeroport.com/wp-content/uploads/2022/03/EBOS-LEM-AIRPORT-FEES-2022.pdf&quot;&gt;Schedule&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Runways: 08/26&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Before you go:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;File a flight plan through SkyDeamon. If flying VFR you don&amp;#39;t need to enter an altitude.&lt;/li&gt;
&lt;li&gt;UK GAR via &lt;a href=&quot;https://www.onlinegar.com/&quot;&gt;OnlineGAR&lt;/a&gt; for the outbound and return.&lt;/li&gt;
&lt;li&gt;BE Gendec via &lt;a href=&quot;https://www.police.be/bordercontrol/en/general-declaration#&quot;&gt;https://www.police.be/bordercontrol/en/general-declaration#&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Midden-Zeeland, Netherlands&lt;/h3&gt;
&lt;p&gt;Non-standard circuit, glider and skydiving ops. Circuit height 700ft AGL
&lt;a href=&quot;https://www.vliegveldzeeland.nl/piloten/&quot;&gt;https://www.vliegveldzeeland.nl/piloten/&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Notice: 2 hours before ETD&lt;/li&gt;
&lt;li&gt;Service: Radio 0800-1900&lt;/li&gt;
&lt;li&gt;Handling: Not required&lt;/li&gt;
&lt;li&gt;Landing Fee:&lt;/li&gt;
&lt;li&gt;Runways: 09/27&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Before you go:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;File a flight plan through SkyDeamon. If flying VFR you don&amp;#39;t need to enter an altitude.&lt;/li&gt;
&lt;li&gt;UK GAR via &lt;a href=&quot;https://www.onlinegar.com/&quot;&gt;OnlineGAR&lt;/a&gt; for the outbound and return.&lt;/li&gt;
&lt;li&gt;NL customs notification via &lt;a href=&quot;https://www.gendec.eu/&quot;&gt;https://www.gendec.eu/&lt;/a&gt; (can also submit a UK GAR)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>notes</category><author>Matthew Brown &lt;me@matthewbrown.io&gt;</author></item><item><title>Sauce</title><link>https://matthewbrown.io/2022/09/23/sauce/</link><guid isPermaLink="true">https://matthewbrown.io/2022/09/23/sauce/</guid><pubDate>Fri, 23 Sep 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The best tomato sauce in the world.&lt;/p&gt;
&lt;p&gt;Add the following to a large pot:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;9.0kg of tomato&lt;/li&gt;
&lt;li&gt;1.5kg onion&lt;/li&gt;
&lt;li&gt;100g garlic&lt;/li&gt;
&lt;li&gt;1 tbsp ground ginger&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Cover with sufficient water, cook for a couple of hours - the longer the better.
Put through a course sieve.&lt;/p&gt;
&lt;p&gt;Mix in and warm through:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1.5kg sugar&lt;/li&gt;
&lt;li&gt;1/2 cup salt&lt;/li&gt;
&lt;li&gt;1.25kg apple puree&lt;/li&gt;
&lt;li&gt;1 cup ezy-sauce&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Bottle.&lt;/p&gt;
</content:encoded><category>recipes</category><author>Matthew Brown &lt;me@matthewbrown.io&gt;</author></item><item><title>Sandwich Loaf Bread Recipe</title><link>https://matthewbrown.io/2021/08/26/sandwich-loaf/</link><guid isPermaLink="true">https://matthewbrown.io/2021/08/26/sandwich-loaf/</guid><pubDate>Thu, 26 Aug 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This is an easy mid-week loaf of bread based on Joshua Weissman&amp;#39;s video.&lt;/p&gt;
&lt;div class=&quot;video-container&quot;&gt;
  &lt;iframe width=&quot;560&quot; height=&quot;315&quot; src=&quot;https://www.youtube.com/embed/lipLAgZkWN0&quot; frameborder=&quot;0&quot; allowfullscreen&gt;&lt;/iframe&gt;
&lt;/div&gt;

&lt;h2&gt;Ingredients&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;125g whole milk&lt;/li&gt;
&lt;li&gt;175g water&lt;/li&gt;
&lt;li&gt;440g of strong white bread flour&lt;/li&gt;
&lt;li&gt;8g of yeast&lt;/li&gt;
&lt;li&gt;8g of salt&lt;/li&gt;
&lt;li&gt;21g of sugar&lt;/li&gt;
&lt;li&gt;30g of softened butter&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Method&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Mix 125g of milk and 175g of water in a saucepan and heat to ~36C.&lt;/li&gt;
&lt;li&gt;Mix 440g of bread flour, 8g of yeast, 8g of salt and 21g of sugar in a mixer bowl.&lt;/li&gt;
&lt;li&gt;Mix the warm milk/water mixture with the dry ingredients in a mixer for ~10mins. At the 2min mark add the 30g of softened butter.&lt;/li&gt;
&lt;li&gt;Take the dough out of the mixing bowl, shape into a ball (as best as you can) and return to the mixing bowl.&lt;/li&gt;
&lt;li&gt;Cover the mixing bowl with a damp cloth and let the dough rise until doubled in size - normally takes about an hour at room temperature.&lt;/li&gt;
&lt;li&gt;Once doubled in size, punch it down to release some of the built up gas.&lt;/li&gt;
&lt;li&gt;Tip out onto a lightly floured surface and roll into a rectangle about 20cm by 30cm. Roll this rectangle up into a cylinder.&lt;/li&gt;
&lt;li&gt;Place the cylinder into a lightly oiled loaf tin, cover with a damp cloth and leave to rise for a second time - this time about 45mins.&lt;/li&gt;
&lt;li&gt;Bake in 180 degree oven for 35 to 40 mins.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Leave to stand before eating - or don&amp;#39;t (it normally smells too good to not eat straight out of the oven).&lt;/p&gt;
</content:encoded><category>recipes</category><author>Matthew Brown &lt;me@matthewbrown.io&gt;</author></item><item><title>Pizza notes</title><link>https://matthewbrown.io/2021/01/23/pizza-dough/</link><guid isPermaLink="true">https://matthewbrown.io/2021/01/23/pizza-dough/</guid><pubDate>Sat, 23 Jan 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In an attempt to match a restaurant quality Neopoliatan pizza, I&amp;#39;ve been experimenting with different pizza dough recipes and toppings. Most the experiments below have been made using an Ooni Koda 12&amp;quot; and use Shipton Mill&amp;#39;s 00.&lt;/p&gt;
&lt;h2&gt;Jan 23 2021&lt;/h2&gt;
&lt;p&gt;For 4 pizzas... using the same ratios as last time (before I started documenting). Experimenting with timings from &lt;a href=&quot;https://github.com/hendricius/pizza-dough&quot;&gt;https://github.com/hendricius/pizza-dough&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Ingredients:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;530g &amp;#39;00&amp;#39; flour&lt;/li&gt;
&lt;li&gt;0.3g yeast&lt;/li&gt;
&lt;li&gt;15g salt&lt;/li&gt;
&lt;li&gt;330g water (64% hydration)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Method:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;20:00: Mix ingredients&lt;/li&gt;
&lt;li&gt;20:30: stretch and fold&lt;/li&gt;
&lt;li&gt;21:00: stretch and fold&lt;/li&gt;
&lt;li&gt;22:00: stretch and fold&lt;/li&gt;
&lt;li&gt;22:30: stretch and fold, leave out overnight&lt;/li&gt;
&lt;li&gt;08:00: stretch and fold&lt;/li&gt;
&lt;li&gt;08:15: divide and shape, to fridge&lt;/li&gt;
&lt;li&gt;18:00: remove from fridge, reshape&lt;/li&gt;
&lt;li&gt;19:00: cook&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Toppings&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Margherita&lt;/li&gt;
&lt;li&gt;Cajun beef mince + caramelised onions&lt;/li&gt;
&lt;li&gt;Salami, mozerella, rocket&lt;/li&gt;
&lt;li&gt;Mushroom, Jalepeno, mozerella&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Eating notes&lt;/h3&gt;
&lt;p&gt;Toppings were great. Bases/dough is still a little &amp;quot;bready&amp;quot;. I think this is maybe caused by either a. oven too hot (hence burning without the interior having time to cook) b. hydration or salt content percentage not quite right.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/pizza/final_23012021.png&quot; alt=&quot;The final product&quot; title=&quot;The final product&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Feb 1 2021&lt;/h2&gt;
&lt;p&gt;For 4 pizzas... This time I used the ratios and timings from &lt;a href=&quot;https://github.com/hendricius/pizza-dough&quot;&gt;https://github.com/hendricius/pizza-dough&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Dough was cold-proofed for 48 hours instead of 24 hours. Also, I accidently covered the pre-proofed dough in oil.. this meant the dough tasted better (longer proofing time) but had a weird crisp and colour (because of the oil). So.. 1 step forward.. 1 step backward.&lt;/p&gt;
&lt;h3&gt;Toppings&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Margherita&lt;/li&gt;
&lt;li&gt;French salami, mozerrella, rocket&lt;/li&gt;
&lt;li&gt;Prawn, Garlic and Chill&lt;/li&gt;
&lt;li&gt;Mushroom, caramelised onion&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>recipes</category><author>Matthew Brown &lt;me@matthewbrown.io&gt;</author></item><item><title>Flour Tortilla Recipe</title><link>https://matthewbrown.io/2021/01/16/flour-tortilla-recipe/</link><guid isPermaLink="true">https://matthewbrown.io/2021/01/16/flour-tortilla-recipe/</guid><pubDate>Sat, 16 Jan 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Inspired by Joshua Weissman&amp;#39;s 5 Ingredient Homemade Flour Tortillas and a bunch of reddit comments about making the perfect mission style flour tortilla.&lt;/p&gt;
&lt;p&gt;You can just mix all the ingredients together, then roll out but I find letting the dough autolyse for 30mins before adding the oil and salt makes the tortilla&amp;#39;s extra soft and tasty.&lt;/p&gt;
&lt;h2&gt;Ingredients&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;350g plain white flour&lt;/li&gt;
&lt;li&gt;190ml warm water&lt;/li&gt;
&lt;li&gt;6g salt&lt;/li&gt;
&lt;li&gt;33g oil&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Method&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Mix 350g of flour and 190ml water a shaggy mess. Doesn&amp;#39;t have to be fully mixed. Just make sure most of the flour is hydrated.&lt;/li&gt;
&lt;li&gt;Leave the flour/water mixture to &lt;a href=&quot;https://www.bakerybits.co.uk/resources/autolyse-what-why-how/&quot;&gt;autolyse&lt;/a&gt; for 30-40 minutes.&lt;/li&gt;
&lt;li&gt;Mix autolysed dough with 33g of oil and 6g of salt.&lt;/li&gt;
&lt;li&gt;Knead until salt is disolved and oil incorporated.&lt;/li&gt;
&lt;li&gt;Cut into 16 approx. 35g balls (use scales - it makes them all nice and even)&lt;/li&gt;
&lt;li&gt;Pre-heat a cast iron skittet or BBQ - you want it as hot as it gets.&lt;/li&gt;
&lt;li&gt;Roll out each ball into a circle. Get it as thin as possible.&lt;/li&gt;
&lt;li&gt;Place the rolled out ball into the pan. Cook until bubles start to form - then flip and repeat. You don&amp;#39;t want to get it too brown (it&amp;#39;s better to be undercooked and soft then overcooked and crispy).&lt;/li&gt;
&lt;li&gt;Place cooked tortilla into a teatowel so it retains some heat.&lt;/li&gt;
&lt;li&gt;Repeat with the remaining balls of dough.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Then fill with whatever ingredients tickle your fancy.&lt;/p&gt;
&lt;h2&gt;Pictures&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;../images/tortilla/IMG_1416.jpg&quot; alt=&quot;Rolling out&quot;&gt;
&lt;img src=&quot;../images/tortilla/IMG_1414.jpg&quot; alt=&quot;Ready to flip&quot;&gt;
&lt;img src=&quot;../images/tortilla/IMG_1415.jpg&quot; alt=&quot;Flipped&quot;&gt;
&lt;img src=&quot;../images/tortilla/IMG_1417.jpg&quot; alt=&quot;All done&quot; title=&quot;All done&quot;&gt;&lt;/p&gt;
</content:encoded><category>recipes</category><author>Matthew Brown &lt;me@matthewbrown.io&gt;</author></item><item><title>Ragu alla Bolognese Recipe</title><link>https://matthewbrown.io/2021/01/16/ragu-alla-bolognese/</link><guid isPermaLink="true">https://matthewbrown.io/2021/01/16/ragu-alla-bolognese/</guid><pubDate>Sat, 16 Jan 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;My go-to recipe for spag bol sauce.&lt;/p&gt;
&lt;h2&gt;Ingredients&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;500g beef mince (not too lean - fat is tasty)&lt;/li&gt;
&lt;li&gt;250g pancetta chopped (bacon will also do)&lt;/li&gt;
&lt;li&gt;85g carrot finely chopped (normally ~1 carrot)&lt;/li&gt;
&lt;li&gt;85g celery finely chopped&lt;/li&gt;
&lt;li&gt;85g onion finely chopped (one medium onion)&lt;/li&gt;
&lt;li&gt;50g tomato paste&lt;/li&gt;
&lt;li&gt;210ml red or white wine&lt;/li&gt;
&lt;li&gt;300ml milk&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Method&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Heat a bit of oil in a large saucepan and add 250g of chopped pancetta. Cook until the pancetta starts to release it&amp;#39;s fat.&lt;/li&gt;
&lt;li&gt;Add all the finely chopped vegetables (85g of carrot, celery and onion). Cook until onion is transparent&lt;/li&gt;
&lt;li&gt;Add 500g of mince and cook until lightly browned.&lt;/li&gt;
&lt;li&gt;Add 50g tomato paste and 210ml of red or white wine and mix through.&lt;/li&gt;
&lt;li&gt;Add 300ml milk and mix through.&lt;/li&gt;
&lt;li&gt;Cook, covered and stiring occationally, for 2 to 3 hours. The longer the better.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Serve with pasta or store - this is one of those dishes that tastes best a couple of days later.&lt;/p&gt;
</content:encoded><category>recipes</category><author>Matthew Brown &lt;me@matthewbrown.io&gt;</author></item><item><title>Connecting Google Cloud Run to Cloud SQL</title><link>https://matthewbrown.io/2020/09/29/deploying-flask-sqlalchemy-to-cloud-run/</link><guid isPermaLink="true">https://matthewbrown.io/2020/09/29/deploying-flask-sqlalchemy-to-cloud-run/</guid><description>Cloud Run is a new GCP service for quickly serving HTTP traffic with containers. This is a guide to connecting your Cloud Run service to Cloud SQL.</description><pubDate>Tue, 29 Sep 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Cloud Run is a relatively new GCP service that means you can get a container up and serving traffic very quickly. This is just a quick guide to connecting your Cloud Run service to Cloud SQL.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This post is part of a Practical GCP for Software Engineers tutorial series I&amp;#39;ve written. If you&amp;#39;d like access to the rest of the content drop me an email on &lt;a href=&quot;mailto:me@matthewbrown.io&quot;&gt;me@matthewbrown.io&lt;/a&gt; :)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Before you start you will need a couple of things before you get started:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A running Cloud Run service (&lt;code&gt;$SERVICE_NAME&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;The service account email you&amp;#39;re using for the Cloud Run Service (&lt;code&gt;$CLOUD_RUN_SERVICE_ACCOUNT_EMAIL&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Your project ID (&lt;code&gt;$GOOGLE_PROJECT_ID&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;br /&gt;

&lt;h3&gt;1. Create a CloudSQL instance&lt;/h3&gt;
&lt;p&gt;This a pretty simple step. For this example I&amp;#39;m using a db-f1-micro instance running PostgreSQL 12 in GCP&amp;#39;s London region (europe-west2).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;gcloud sql instances create cloud-run-example \
  --tier=db-f1-micro \
  --region=europe-west2 \
  --database-version=POSTGRES_12 \
  --root-password=correcthorsebatterystaple \
  --storage-auto-increase
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will take a couple of minutes to run.&lt;/p&gt;
&lt;h3&gt;2. Give your Cloud Run service account the needed role/permissions.&lt;/h3&gt;
&lt;p&gt;Allow your Cloud Run service account access to Cloud SQL.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gcloud projects add-iam-policy-binding $GOOGLE_PROJECT_ID \
  --role roles/cloudsql.client \
  --member serviceAccount:$CLOUD_RUN_SERVICE_ACCOUNT_EMAIL
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By default Cloud Run uses the default compute service account so in that case &lt;code&gt;$CLOUD_RUN_SERVICE_ACCOUNT_EMAIL&lt;/code&gt; would be &lt;code&gt;$GOOGLE_PROJECT_ID-compute@developer.gserviceaccount.com&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;3. Configure the connection from Cloud Run to CloudSQL.&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;gcloud run services update $SERVICE_NAME \
  --add-cloudsql-instances $GOOGLE_PROJECT_ID:europe-west2:cloud-run-example \
  --update-env-vars DATABASE_URL=&amp;#39;postgres://postgres:correcthorsebatterystaple@/postgres?host=/cloudsql/$GOOGLE_PROJECT_ID:europe-west2:cloud-run-example&amp;#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will make a socket available in the Cloud Run container at &lt;code&gt;/cloudsql/$GOOGLE_PROJECT_ID:europe-west2:cloud-run-example/.s.PGSQL.5432&lt;/code&gt;. You can then configure your container to connect via that socket (the example above sets a environment variable called DATABASE_URL)&lt;/p&gt;
&lt;h3&gt;4. (Optional) Connect SQLAlchemy and connection limit gotchas.&lt;/h3&gt;
&lt;p&gt;If you&amp;#39;re using SQLAlchemy you can then configure it like follows:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import os

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import scoped_session, sessionmaker

engine = create_engine(os.getenv(&amp;quot;DATABASE_URL&amp;quot;))
session = scoped_session(sessionmaker(bind=engine))

Base = declarative_base()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;But be careful&lt;/em&gt; - behind the scenes Cloud Run is just containers. By default it will allow 80 concurrent requests to a container before spinning up a 2nd. SQLAlchemy&amp;#39;s connection pool &lt;a href=&quot;https://docs.sqlalchemy.org/en/13/core/pooling.html#sqlalchemy.pool.QueuePool.__init__&quot;&gt;defaults&lt;/a&gt; to a min of 5 and max of 10 connections. If you&amp;#39;re using a small db instance (like the one in this example) and expecting some load you can quickly exhaust connection limits.&lt;/p&gt;
</content:encoded><category>notes</category><author>Matthew Brown &lt;me@matthewbrown.io&gt;</author></item><item><title>Getting started with r and r-studio on MacOS</title><link>https://matthewbrown.io/2018/05/05/getting-started-with-r-and-r-studio-on-macos/</link><guid isPermaLink="true">https://matthewbrown.io/2018/05/05/getting-started-with-r-and-r-studio-on-macos/</guid><pubDate>Sat, 05 May 2018 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I&amp;#39;ve enrolled myself in the Duke University course &lt;a href=&quot;https://www.coursera.org/specializations/statistics&quot;&gt;Statistics with R&lt;/a&gt; on Coursera. This is a quick note around how I setup R and R-studio on MacOS Sierra and above.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1:&lt;/strong&gt; Install &lt;a href=&quot;https://brew.sh/&quot;&gt;Homebrew&lt;/a&gt; and &lt;a href=&quot;https://caskroom.github.io/&quot;&gt;Cask&lt;/a&gt;. Homebrew is a package manager for MacOS and Cask allows you to install Apps using the Homebrew CLI.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;/usr/bin/ruby -e &amp;quot;$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)&amp;quot;
brew tap caskroom/cask
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 2:&lt;/strong&gt; Install R, XQuartz and R Studio. You no longer have to tap the science homebrew repository to install R - it has been included in the standard repository since 2017.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;brew install r
brew cask install xquartz rstudio
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../images/rstudio.png&quot; alt=&quot;RStudio&quot;&gt;&lt;/p&gt;
</content:encoded><category>notes</category><author>Matthew Brown &lt;me@matthewbrown.io&gt;</author></item><item><title>Setting up python, pipenv and jupyter notebook on Ubuntu 18.04</title><link>https://matthewbrown.io/2018/05/05/setting-up-my-python-workspace-2018/</link><guid isPermaLink="true">https://matthewbrown.io/2018/05/05/setting-up-my-python-workspace-2018/</guid><pubDate>Sat, 05 May 2018 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Ubuntu 18.04 Bionic Beaver (the latest LTS version) was released last week. The little script below will get &lt;code&gt;pip3&lt;/code&gt; and &lt;code&gt;pipenv&lt;/code&gt; up and running.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Install pip and pipenv
sudo apt install python3-pip python3-dev
pip3 install --user pipenv

# Add pipenv (and other python scripts) to PATH
echo &amp;quot;PATH=$HOME/.local/bin:$PATH&amp;quot; &amp;gt;&amp;gt; ~/.bashrc
source ~/.bashrc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create a new project and install some dependencies (include jupyter)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mkdir project-name &amp;amp;&amp;amp; cd project-name
pipenv install numpy matplotlib jupyter
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Start a jupyter notebook&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pipenv run jupyter notebook
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>notes</category><author>Matthew Brown &lt;me@matthewbrown.io&gt;</author></item><item><title>The factory method pattern in Go</title><link>https://matthewbrown.io/2016/01/23/factory-pattern-in-golang/</link><guid isPermaLink="true">https://matthewbrown.io/2016/01/23/factory-pattern-in-golang/</guid><pubDate>Sat, 23 Jan 2016 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I have been writing web services in Go for approaching 2 years now. The factory pattern is something I have found particularly useful, especially for writing clean, concise, maintainable and testable code. Originally I used it just for the data access layer so I could swap in and out upon MySQL, PostgreSQL, etc without changing the application layer code but it has proved useful in many other places.&lt;/p&gt;
&lt;p&gt;Starting at the very beginning we define our package and import the required libraries.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// Package definition and import the required stdlib packages.
package main

import (
	&amp;quot;fmt&amp;quot;
	&amp;quot;log&amp;quot;
	&amp;quot;errors&amp;quot;
	&amp;quot;strings&amp;quot;
	&amp;quot;database/sql&amp;quot;
	&amp;quot;sync&amp;quot;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For the purposes of illustration we will create a &lt;code&gt;DataStore&lt;/code&gt; interface that is implemented by &lt;code&gt;PostgreSQLDataStore&lt;/code&gt; and &lt;code&gt;MemoryDataStore&lt;/code&gt;. The DataStore interface will specify 2 methods: &lt;code&gt;Name&lt;/code&gt; and &lt;code&gt;FindUserNameById&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;var UserNotFoundError = errors.New(&amp;quot;User not found&amp;quot;)

type DataStore interface {
	Name() string
	FindUserNameById(id int64) (string, error)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then create 2 structs that implement the the interface.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;//The first implementation.
type PostgreSQLDataStore struct {
	DSN string
	DB sql.DB
}

func (pds *PostgreSQLDataStore) Name() string {
	return &amp;quot;PostgreSQLDataStore&amp;quot;
}

func (pds *PostgreSQLDataStore) FindUserNameById(id int64) (string, error) {
	var username string
	err := pds.DB.Query(&amp;quot;SELECT username FROM users WHERE id=$1&amp;quot;, id).Scan(&amp;amp;username)
	if err != nil {
		if err == sql.ErrNoRows {
			return &amp;quot;&amp;quot;, UserNotFoundError
		}
		return &amp;quot;&amp;quot;, err
	}
	return username, nil
}

//The second implementation.
type MemoryDataStore struct {
	sync.RWMutex
	Users map[int64]string
}

func (mds *MemoryDataStore) Name() string {
	return &amp;quot;MemoryDataStore&amp;quot;
}

func (mds *MemoryDataStore) FindUserNameById(id int64) (string, error) {
	mds.RWMutex.RLock()
	defer mds.RWMutex.RUnlock()
	username, ok := mds.Users[id];
	if !ok {
		return &amp;quot;&amp;quot;, UserNotFoundError
	}
	return username, nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we can start to explore the power of the Factory method pattern.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;...create objects without having to specify the exact &amp;#39;type&amp;#39; of the object that will be created...&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;First we must create &lt;code&gt;factory&lt;/code&gt; methods for all our implementations that return the common interface. These are essentially &lt;code&gt;constructors&lt;/code&gt; that accept a common argument. In our case this common argument is a &lt;code&gt;map[string]string&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;type DataStoreFactory func(conf map[string]string) (DataStore, error)

func NewPostgreSQLDataStore(conf map[string]string) (DataStore, error) {
	dsn, ok := conf.Get(&amp;quot;DATASTORE_POSTGRES_DSN&amp;quot;, &amp;quot;&amp;quot;)
	if !ok {
		return nil, errors.New(fmt.Sprintf(&amp;quot;%s is required for the postgres datastore&amp;quot;, &amp;quot;DATASTORE_POSTGRES_DSN&amp;quot;))
	}

	db, err := sqlx.Connect(&amp;quot;postgres&amp;quot;, dsn)
	if err != nil {
		log.Panicf(&amp;quot;Failed to connect to datastore: %s&amp;quot;, err.Error())
		return nil, datastore.FailedToConnect
	}

	return &amp;amp;PostgresDataStore{
		DSN: dsn,
		DB:  db,
	}, nil
}

func NewMemoryDataStore(conf map[string]string) (DataStore, error) {
	 return &amp;amp;MemoryDataStore{
	 	Users: &amp;amp;map[int64]string{
	 		1: &amp;quot;mnbbrown&amp;quot;,
	 		0: &amp;quot;root&amp;quot;,
	 	},
	 	RWMutex: &amp;amp;sync.RWMutex{},
	 }, nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we must store these factory methods somewhere to be called upon as needed. I&amp;#39;ll create a &lt;code&gt;Register&lt;/code&gt; helper method to add factories to &lt;code&gt;datastoreFactories&lt;/code&gt;. The &lt;code&gt;init&lt;/code&gt; function registers both the factories we created above using easy to remember names.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;var datastoreFactories = make(map[string]DataStoreFactory)

func Register(name string, factory DataStoreFactory) {
	if factory == nil {
		log.Panicf(&amp;quot;Datastore factory %s does not exist.&amp;quot;, name)
	}
	_, registered := datastoreFactories[name]
	if registered {
		log.Errorf(&amp;quot;Datastore factory %s already registered. Ignoring.&amp;quot;, name)
	}
	datastoreFactories[name] = factory
}

func init() {
	Register(&amp;quot;postgres&amp;quot;, NewPostgreSQLDataStore)
	Register(&amp;quot;memory&amp;quot;, NewMemoryDataStore)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the magic happens. Using the &lt;code&gt;Create&lt;/code&gt; function below the appropriate factory method will be called using the the &lt;code&gt;conf&lt;/code&gt; argument to create an instance of the &lt;code&gt;DataStore&lt;/code&gt; interface.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func CreateDatastore(conf map[string]string) (DataStore, error) {

	// Query configuration for datastore defaulting to &amp;quot;memory&amp;quot;.
	engineName := conf.Get(&amp;quot;DATASTORE&amp;quot;, &amp;quot;memory&amp;quot;)

	engineFactory, ok := datastoreFactories[engineName]
	if !ok {
		// Factory has not been registered.
		// Make a list of all available datastore factories for logging.
		availableDatastores := make([]string, len(datastoreFactories))
		for k, _ := range datastoreFactories {
			availableDatastores = append(availableDatastores, k)
		}
		return nil, errors.New(fmt.Sprintf(&amp;quot;Invalid Datastore name. Must be one of: %s&amp;quot;, strings.Join(availableDatastores, &amp;quot;, &amp;quot;)))
	}

	// Run the factory with the configuration.
	return engineFactory(conf)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can use the above CreateDatastore method in your application code as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;datastore, err := CreateDataStore(&amp;amp;map[string]string{
	&amp;quot;DATASTORE&amp;quot;: &amp;quot;postgres&amp;quot;,
	&amp;quot;DATASTORE_POSTGRES_DSN&amp;quot;: &amp;quot;dbname=factoriesareamazing&amp;quot;,
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;of use another datastore:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;datastore, err := CreateDataStore(&amp;amp;map[string]string{
	&amp;quot;DATASTORE&amp;quot;: &amp;quot;memory&amp;quot;,
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This interface driven approach allows you to do injection and makes mocking simple for unit tests. i.e you can test against &lt;code&gt;MockDataStore&lt;/code&gt; which implements the &lt;code&gt;DataStore&lt;/code&gt; interface rather than having to spin up an instances of PostgreSQL for your unit tests. That said, you should still be writing integration tests against a real version of PostgreSQL to account for SQL errors and other quirks you could have missed.&lt;/p&gt;
&lt;p&gt;Like what you read? Or think I&amp;#39;m wrong - let me know via &lt;a href=&quot;https://twitter.com/mnbbrown&quot;&gt;@mnbbrown&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Thanks to &lt;a href=&quot;https://twitter.com/smitec&quot;&gt;@smitec&lt;/a&gt; and &lt;a href=&quot;https://twitter.com/davidghooper&quot;&gt;@davidghooper&lt;/a&gt; for reading my draft.&lt;/p&gt;
</content:encoded><category>notes</category><author>Matthew Brown &lt;me@matthewbrown.io&gt;</author></item><item><title>Ireland</title><link>https://matthewbrown.io/2015/01/18/ireland/</link><guid isPermaLink="true">https://matthewbrown.io/2015/01/18/ireland/</guid><pubDate>Sun, 18 Jan 2015 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;../images/mattandgenvsworld/ireland/Ireland-91.jpg&quot; alt=&quot;Wild Atlantic Way&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Day 1 &amp;amp; 2: Dublin - Kilkenny&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Landing in Dublin was eventful. The wind meant the plane was thrown around like a rag doll with wings all the way down. After surviving the descent and getting our bags, we met up with our friend Bronte, who was on exchange in Glasgow. She was taking a couple of days off before returning to Glasgow for exams. Then it was time for hire car number 2. Our steed for the next 6 days was to be a &lt;del&gt;pink&lt;/del&gt;&amp;nbsp; wine-coloured, Nissan Micra. It was like driving a peanut after the Audi we had in Germany.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/mattandgenvsworld/ireland/Ireland-62.jpg&quot; alt=&quot;The Let Down&quot;&gt;&lt;/p&gt;
&lt;p&gt;We drove straight to our B&amp;amp;B just outside Kilkenny, a 1.5 hour journey that ended up taking 2.5 hours with the poor weather and my need for snacks. After checking in to our B&amp;amp;B we made our way back into the city for dinner with Gen&amp;#39;s Irish relatives. Bronte holed up in a nearby pub nearby to do some study while Gen and I got lost trying to find the restaurant. With an almost-flat phone, no wifi or data, and only a vague (wrong) recollection of where we were going, it took forever to find the restaurant, but we got there in the end! Gen was expecting two, maybe three people tops, but the entire family turned out! We had a fantastic dinner.&lt;/p&gt;
&lt;p&gt;The next morning we awoke to a rare sunny Irish day, enjoyed the first of the incredible Irish breakfasts and headed back into Kilkenny. We toured Kilkenny Castle (with a large group of Russian school students) where the National Trust has done a great restoration job. The tour guide talked us through the history of the Butlers (the main family who lived there).&lt;/p&gt;
&lt;p&gt;After our history-filled morning, we saw Penguins of Madagascar for some comedic relief before eating dinner at a local pub, complete with live folk music.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Day 3: Kilkenny - Killarney&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/mattandgenvsworld/ireland/Ireland-1.jpg&quot; alt=&quot;Stuck behind a tractor&quot;&gt;&lt;/p&gt;
&lt;p&gt;After another amazing breakfast we checked out and started the long drive to Killarney, made only slower by tractors on the winding Irish roads. Our first stop for the day was Cashel, where we spent a couple of hours exploring the Rock of Cashel, an imposing ruined castle and cathedral on top of a hill whose only inhabitants are crows (or jackdaws?). Perched upon a bare hill (or rock?) the Rock of Cashel was commanding. Unfortunately we missed out on learning most of its history as the tour was after we had to be back on the road.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/mattandgenvsworld/ireland/Ireland-16.jpg&quot; alt=&quot;The Team&quot;&gt;&lt;/p&gt;
&lt;p&gt;After exploring the castle, we strolled down to explore the ruins of a wild and rugged looking abbey - Hore Abbey.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/mattandgenvsworld/ireland/Ireland-22.jpg&quot; alt=&quot;Hore Abbey&quot;&gt;&lt;/p&gt;
&lt;p&gt;Bronte then made her way back to Glasgow via Dublin, and we drove on to Killarney! We checked into our AirBnB and found a delicious dinner at a local pub.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/mattandgenvsworld/ireland/Ireland-26.jpg&quot; alt=&quot;Give way in Irish&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Day 4: Killarney - Galway&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Our trip to Galway was a long one. A decent breakfast and postcard hunt was followed by 1.5 hours to Limerick, where we planned to stop for a light lunch. However, the size of the city defeated us and we decided to continue on. Still hungry, we stopped at Ennis and wandered down the quaint streets until we found a great little cafe. Then it was on to the Cliffs of Moher. We decided not to stop at the tourist trap and instead drove down the coastal road - the Wild Atlantic Way - by far my favourite part of the entire Ireland trip! It was an incredible drive, and the Wild Atlantic Way definitely lived up to its name.&lt;/p&gt;
&lt;style&gt;
.video-container {
    position: relative;
    padding-bottom: 56.25%;
    padding-top: 30px; height: 0; overflow: hidden;
}

.video-container iframe,
.video-container object,
.video-container embed {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
}
&lt;/style&gt;

&lt;div class=&quot;video-container&quot;&gt;
         &lt;iframe width=&quot;560&quot; height=&quot;315&quot; src=&quot;https://www.youtube.com/embed/1JrdFaYx8VU&quot; frameborder=&quot;0&quot; allowfullscreen&gt;&lt;/iframe&gt;
&lt;/div&gt;
&lt;br/&gt;
All the houses along the coast are hunkered down against the regular wind and storms that come over the ocean. The weather was rough, making the Atlantic look powerful and savage. We took a detour down to Doolin Pier, where the ferries depart, and got up close (well, not too close) and personal with the ferocity of the waves. The weather was so bad that none of the ferries were running, answering our question for how boats would be able to survive in such harsh weather!

&lt;p&gt;&lt;img src=&quot;../images/mattandgenvsworld/ireland/Ireland-73.jpg&quot; alt=&quot;Storm&quot;&gt;&lt;/p&gt;
&lt;p&gt;We continued down the coastal road, passing a lighthouse perched on a rock, cows on the cliffs, and what passes for an Irish beach. It was windy, cold and miserable, although it might be alright in summer. Not quite up to the Australian standard of beach.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/mattandgenvsworld/ireland/Ireland-81.jpg&quot; alt=&quot;Beach&quot;&gt;&lt;/p&gt;
&lt;p&gt;An hour&amp;#39;s driving (plus many stops) saw us at our next AirBnB, a quirky house and garden owned by an artist. For the first time we had a full kitchen, so we made use of it to make a homemade meal and save some of those euros!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Day 5: Galway - Westport&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/mattandgenvsworld/ireland/Ireland-6-2.jpg&quot; alt=&quot;The View&quot;&gt;&lt;/p&gt;
&lt;p&gt;A leisurely mid-morning checkout was followed by the highlight of the trip: a guided hike into the mountains of Oughtmama. We arrived at the farmhouse early and were the only two people on the hike, making for a very personal tour! Our guide, Darragh, told us about much of the folk lore and history of Ireland. This included the potato famine, the role of Fairies in Irish folklore (and fairy trees), how Ireland shaped the church, why the Irish lost the skill of fishing, and the how central farming was to the Irish way of life.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/mattandgenvsworld/ireland/Ireland-9-2.jpg&quot; alt=&quot;Fairy Tree&quot;&gt;&lt;/p&gt;
&lt;p&gt;We saw a large tribe of goats and learned about the architecture used in the ancient ruined churches. All sorts of weather were had in the 2 hours: sleet, snow, soft hail, wind and sunshine! At the end of the tour we went back to the farmhouse for a hot drink to warm us up, before exploring their boutique chocolate shop. We couldn&amp;#39;t help buying some of the elderberry and hazelnut (both of which are grown and picked on the farm) chocolate.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/mattandgenvsworld/ireland/Ireland-25-2.jpg&quot; alt=&quot;Snow&quot;&gt;&lt;/p&gt;
&lt;p&gt;After our hike it was straight up to Westport, a 2 hour drive that took 3.5 due to Ireland&amp;#39;s inability to deal with snow. There was less than an inch on the ground and some people were doing 30 km/h. Then, to make matters worse, they closed all entrances to Westport meaning we were stuck for nearly an hour.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/mattandgenvsworld/ireland/Ireland-37-2.jpg&quot; alt=&quot;Snow&quot;&gt;&lt;/p&gt;
&lt;p&gt;We finally got to our AirBnB, a room with a friendly sailing couple who greeted us with tea and cake, which was welcome after our long drive!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Day 6: Westport - Athlone&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Another delicious traditional Irish breakfast (including black pudding) was a great start to another day adventuring. We continued to follow the Wild Atlantic Way up to Achill Ireland.  The seas were very rough again, and the wind caused our little car to feel like it was about to picked up and blown away! Many multicoloured sheep were huddled behind rocks and walls away from the wind.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/mattandgenvsworld/ireland/Ireland-16-3.jpg&quot; alt=&quot;The Weather&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/mattandgenvsworld/ireland/Ireland-13-3.jpg&quot; alt=&quot;Life Ring&quot;&gt;&lt;/p&gt;
&lt;p&gt;Not sure the life ring would be much help.&lt;/p&gt;
&lt;p&gt;At the end of the road, we sat on a lookout and had tea and biscuits from our thermos. It was quite a view to have over morning tea! Then it was the long drive to Athlone. Along the way our exhaust pipe started rattling, which was comforting. We hoped the car would hold together long enough for us to get it back. Never getting a Nissan Micra!&lt;/p&gt;
&lt;p&gt;In Athlone we made a quick stop for pizza before arriving at our next AirBnB, a cute self-contained loft. The family had a very friendly dog called Oscar, who kept wanting to join us and would open the door if it wasn&amp;#39;t locked!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Day 7: Athlone - Dublin&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The next day it was leftovers (cold pizza, bread and cheese) for breakfast as we scrambled to get the car back and catch our flight on time. Thankfully they put the rattling exhaust down to general wear and tear. Then it was on a long-haul flight to Canada!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Total kilometres:&lt;/strong&gt; approx. 1,150 km &lt;br/&gt;
&lt;strong&gt;Total time driving:&lt;/strong&gt; approx. 21 hours &lt;br/&gt;
&lt;strong&gt;Weather types:&lt;/strong&gt; wind, rain, snow, sleet, sun &lt;br/&gt;
&lt;strong&gt;Wildlife count:&lt;/strong&gt; lots of sheep, a few woolly cows, a tribe of goats, and the odd horse &lt;br/&gt;&lt;/p&gt;
&lt;p&gt;You can explore some more pictures from our trip on &lt;a href=&quot;https://www.facebook.com/media/set/?set=a.10205492471931730.1073741836.1249162949&amp;type=1&amp;l=f0aa8cadfe&quot;&gt;Facebook&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/mattandgenvsworld/ireland/ireland-2.jpg&quot; alt=&quot;The Map&quot;&gt;&lt;/p&gt;
</content:encoded><category>travel</category><author>Matthew Brown &lt;me@matthewbrown.io&gt;</author></item><item><title>Cold weather, canals and culture.</title><link>https://matthewbrown.io/2015/01/09/amsterdam/</link><guid isPermaLink="true">https://matthewbrown.io/2015/01/09/amsterdam/</guid><pubDate>Fri, 09 Jan 2015 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;After a relatively short bus ride in from Schiphol Amsterdam we arrived at our first AirBnB. AirBnB lets people put their spare bedroom or granny flat to good use, meet people from around the world and make some money at the same time. I love it because it means you&amp;#39;re not in an impersonal hotel or hostel and get to meet the locals!  Amsterdam is famous for its skinny houses and steep staircases as a tax was levied on home owners depending on the width of the property. As you can see in the picture below where we stayed was no different. At the top of this staircase was our little ensuite bedroom - perfect for a couple of nights in Amsterdam.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/mattandgenvsworld/IMG_4056.jpg&quot; alt=&quot;Steep stairs&quot;&gt;&lt;/p&gt;
&lt;p&gt;We spent the first afternoon walking around Amsterdam exploring the canals and shops, and devouring a serving of the Netherland&amp;#39;s famous hot chips (frietjes) covered in Samurai sauce. Later on we met up with one of Gen&amp;#39;s best school friends who is studying in Maastricht on exchange for a drink and some nibbles at one of Amsterdam&amp;#39;s many little restaurants.&lt;/p&gt;
&lt;p&gt;Day two we awoke to cold rainy weather but that didn&amp;#39;t stop us. First stop was Anne Frank house. Anne Frank was persecuted and ultimately killed after the Nazi invasion of The Netherlands. After hearing of the impeding invasion Anne&amp;#39;s father hid the whole family in the back of the family factory. The were eventually found but not before Anne wrote about her experience in her diary, which has now been published. After Anne Frank House we met Caroline for breakfast in a great little cafe and met an Irishman living the ex-pat life in Amsterdam.&lt;/p&gt;
&lt;p&gt;On the basis of a recommendation from Amanda (our AirBnB host) we decided to explore Amsterdam from sea level onboard a canal boat tour. We saw the famous seven bridges, the narrowest house in Amsterdam and much more of the city we wouldn&amp;#39;t have otherwise seen!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/mattandgenvsworld/IMG_4062.jpg&quot; alt=&quot;Seven Bridges&quot;&gt;
&lt;img src=&quot;../images/mattandgenvsworld/IMG_4063.jpg&quot; alt=&quot;Skinny house&quot;&gt;&lt;/p&gt;
&lt;p&gt;Each museum in Amsterdam is open late one evening per week. On the day we were in Amsterdam (a Thursday) it happened to be the gallery of modern art - Stedelijk. We spent a great evening being cultured by exploring one of Amsterdam&amp;#39;s biggest private collections of modern art, as well as the permanent exhibits. Then it was drinks, nachos and goodbyes to Caroline - the next time we will see her is when she returns to Brisvegas in February.&lt;/p&gt;
&lt;p&gt;It was a great quick trip to Amsterdam exploring the canals, bearing the cold (and wet) weather and adding a bit of culture to #mattandgenvsworld. The next morning we woke up early and made our way back to the airport ready to fly to Dublin!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/mattandgenvsworld/IMG_4057.jpg&quot; alt=&quot;Tulips and cheese&quot;&gt;&lt;/p&gt;
</content:encoded><category>travel</category><author>Matthew Brown &lt;me@matthewbrown.io&gt;</author></item><item><title>A Home Away From Home</title><link>https://matthewbrown.io/2015/01/07/dresden/</link><guid isPermaLink="true">https://matthewbrown.io/2015/01/07/dresden/</guid><pubDate>Wed, 07 Jan 2015 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;../images/mattandgenvsworld/dresden_cover.jpg&quot; alt=&quot;Dresden&quot;&gt;&lt;/p&gt;
&lt;p&gt;I love Dresden. It feels like my second home and one of the few places where I feel like a local. Known as the Florence on the Elbe it is a city with a rich and colourful history. It was &lt;a href=&quot;http://en.wikipedia.org/wiki/Bombing_of_Dresden_in_World_War_II&quot;&gt;firebombed&lt;/a&gt; at the end of World War 2 and many buildings remained destroyed until the fall of the Berlin Wall and East Germany in 1989. Since then millions of hours and euros have been spent rebuilding the city to its former glory. The centrepiece of this rebuilding it the Frauenkirche (or Church of our Lady) which was rebuilt stone for stone. It looks like a very &amp;#39;European&amp;#39; city with old buildings and many cobblestone streets - dust it with snow and you have a picture straight from a postcard.&lt;/p&gt;
&lt;p&gt;I spent my GAP year here in 2010 working as a assistant english teacher (think teachers aide) at St. Benno. Gymnasium helping students develop their professional and conversational English skills.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/mattandgenvsworld/benno.jpg&quot; alt=&quot;Benno&quot;&gt;&lt;/p&gt;
&lt;p&gt;On arrival into Dresden this time we made our way down to Schillergarten - a small beer garden in the shadow of the Blue Wonder (Blaues Wunder) bridge (it&amp;#39;s kind of like the Dresden version of Brisbane&amp;#39;s Story Bridge Hotel - only with glühwein and bratwurst instead of XXXX and steak).&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/mattandgenvsworld/IMG_1917.jpg&quot; alt=&quot;Roast Dinner&quot;&gt;&lt;/p&gt;
&lt;p&gt;One of the &amp;#39;conditions&amp;#39; on staying with the Kirchberg&amp;#39;s is that you must cook them a meal. This meant that much of our first full day in Dresden was spent cooking roast beef and vegetables, with pavlova for desert. I invited a couple of my friends over and it was a great evening of food, drink and conversation. One big topic of conversation was the big protests that have been happening in Dresden. 18,000 supporters of the Patriotic Europeans Against the Islamisation of the Occident (PEGIDA) had taken part in protests in Dresden, but in true German style counter-demonstrations took place in basically every major German city including 3,000 in Dresden. You can read more about it here: &lt;a href=&quot;http://www.abc.net.au/news/2015-01-06/anti-immigration-rally-draws-18000-to-protest-in-germany/6001852&quot;&gt;German &amp;#39;anti-Islamisation&amp;#39; group PEGIDA draws 18,000 to protest in Dresden&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;New Year&amp;#39;s Eve or &amp;quot;Silvester&amp;quot; in German is a very crazy affair! In the afternoon we partook in some real german engineering and built a ice bar ready for the party the Kirchberg&amp;#39;s were hosting! We then escaped for the evening to one of my friend&amp;#39;s (Leo Kaßner - founder of fingercoding) flat where we ate all kinds of unhealthy food and spent 3 or so hours catching up. Whenever I come visit Germany it always feels like I never left! It was then back to the Kirchbergs and up to their rooftop terrace to watch the fireworks. Fireworks are freely available for private use in Germany meaning that for New Year&amp;#39;s Eve you feel like you are walking through a war zone. Disappointingly the city was covered in a thick fog and you couldn&amp;#39;t see more than 10 metres - meaning any fireworks further than a couple of meters away were invisible.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/mattandgenvsworld/IMG_4021.jpg&quot; alt=&quot;Jazz&quot;&gt;&lt;/p&gt;
&lt;p&gt;Dresden is a very cultural city. You can enjoy everything from a very serious evening of Opera at the Semperoper in the Altstadt to a local Jazz quartet at the Blue Note bar in the bohemian Neustadt. This time we saw The King&amp;#39;s Son - an opera from the same writer as Hansel and Gretel. This was a tragic fairytale gone wrong, and a great Christmas present from the Ulis! We also spent one evening listening to some very experimental Jazz, and another being blown away by some of Dresden&amp;#39;s top Jazz performers, the Jazzfanatics.&lt;/p&gt;
&lt;p&gt;It may seem like we crammed a lot in, but our time was mainly spent eating great food (especially German breakfast), catching up with friends, long walks on sunny days and doing a lot of nothing. After a busy year of study and work, it was brilliant.&lt;/p&gt;
&lt;p&gt;Much love and thanks to the Kirchbergs, Sedlacks and Heckers for their hospitality!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/mattandgenvsworld/IMG_1934.jpg&quot; alt=&quot;Landscape&quot;&gt;&lt;/p&gt;
</content:encoded><category>travel</category><author>Matthew Brown &lt;me@matthewbrown.io&gt;</author></item><item><title>Let it snow, let it snow, let it snow!</title><link>https://matthewbrown.io/2015/01/01/let-it-snow/</link><guid isPermaLink="true">https://matthewbrown.io/2015/01/01/let-it-snow/</guid><pubDate>Thu, 01 Jan 2015 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Following our graduation a couple of weeks ago my better half (Genevieve) and I decided we should do something to mark the occasion. As we both share a love of travel and will only get 4 weeks of annual leave this year we decided to fill our last couple of months of freedom with one big holiday - a last hurrah!&lt;/p&gt;
&lt;p&gt;Over 52 days we will visit Dresden (via Munich), Amsterdam, Ireland, Toronto, Winnipeg, Yellowknife, Calgary, Big White and Vancouver, before flying back home on the 15th of February.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/mattandgenvsworld/IMG_1820-1.jpg&quot; alt=&quot;EK433 to Dubai&quot;&gt;&lt;/p&gt;
&lt;p&gt;Flying to Europe is always a mixture of excitement, boredom and frustration - it is exciting to go travelling but there are only so many movies and TV shows you can watch! The fact that it signalled the start of our trip only exacerbated these emotions.&lt;/p&gt;
&lt;p&gt;On arriving in Singapore (our first stopover) Gen found a massage chair and that was the last I heard from her for 30 minutes. After this short stopover it was back on the plane for another 7 hours.&lt;/p&gt;
&lt;p&gt;Our second stopover was in Dubai for 2 or so hours - if you ever get the chance to visit Dubai airport I highly recommend visiting Shakeshack near gate A10 for some homemade lemonade and the thickest of milkshakes! Gen noticed that Shakeshack also offered something called a Concrete - it is described as a &amp;quot;dense frozen custard ice cream blended at high speed with mix-ins&amp;quot;. We thought that might be delicious but after getting off a plane such a dense drink is just not appealing.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/mattandgenvsworld/IMG_1827-1.jpg&quot; alt=&quot;Shakeshack&quot;&gt;&lt;/p&gt;
&lt;p&gt;After downing a croissant from Paul (a French bakery that apparently has the best coffee in Dubai Airport) it was onto an A380 for the last leg to Munich! Emirates has never disappointed me - the planes are new, the food is nice, the flight attendants friendly and there&amp;#39;s free Wifi on their fleet of A380s.&lt;/p&gt;
&lt;p&gt;Landing onto a snow lined runway, we finally arrived in Munich. We deplaned, went through (a surprisingly friendly and funny) immigration, collected our bags, and made our way to the Europcar counter. Now came the first big test of my planning aptitude. Hiring cars as a &amp;#39;young driver&amp;#39; (under 25) is difficult and expensive. Add to this the strange QLD/Australian graduated driving licence system and you have a recipe for uncertainty and noncommittal emails from car rental companies. Would they accept my QLD provisional licence as a full licence (I had been informed they would) and would they be happy to rent the car to 22 year old me?&lt;/p&gt;
&lt;p&gt;Well luckily the answer happened to be yes and Gen and I were presented with the keys to an Audi A3! What. A. Car. It looked fantastic and had more buttons and gadgets on it then you could poke a stick at! That said, driving a manual on the opposite side of the road, through two roundabouts, in the snow and dark, following instructions from a very german GPS could have been a recipe for disaster but it was non-eventful, helped by the bucketloads of adrenaline that was surging through my body.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/mattandgenvsworld/audi.jpeg&quot; alt=&quot;Our Audi A3&quot;&gt;&lt;/p&gt;
&lt;p&gt;We found our hotel (Hotel Hallbergerhof) and were pleased to find out our room had been upgraded from a twin room with a shared bathroom to a double room with our own bathroom! We were early to bed and woke again early the next morning ready for the big drive to Dresden. Before we got on our way we found a small local bakery and bought breakfast - it cost us €4.50 for a day&amp;#39;s worth of bread and an excellent cup of coffee.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/mattandgenvsworld/IMG_1863-1.jpg&quot; alt=&quot;Gen Looking Excited&quot;&gt;&lt;/p&gt;
&lt;p&gt;The drive to Dresden was great! Cruising along German Autobahns at an average of 140km/h through snow covered mountains is an surreal experience. It was a team effort with Gen letting me know when I was drifting too far in one direction and watching out for cars when changing lanes - essentially a beautiful lane assist. It is invaluable when you are driving on the opposite side of the road.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/mattandgenvsworld/IMG_1875-1.jpg&quot; alt=&quot;Its snowing cats and dogs&quot;&gt;&lt;/p&gt;
&lt;p&gt;It was a reasonably uneventful trip minus getting stuck behind a couple of snowploughs and occasionally being overtaken by someone going way too fast given the snowy conditions!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/mattandgenvsworld/IMG_1886.jpg&quot; alt=&quot;Snow Ploughs&quot;&gt;&lt;/p&gt;
&lt;p&gt;Finally, after hours of travel we made it to Dresden where I spent my GAP year in 2010 as a teaching assistant and lived with, what is now my German family, the Kirchbergs. Catching up with the Kirchbergs, and all my other German friends, is always great! They are some of the friendliest, open-minded and interesting people I have ever met.&lt;/p&gt;
&lt;p&gt;After dropping the car back off to the airport it was back to the Kirchbergs and early to bed. We were buggered!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/mattandgenvsworld/IMG_1896-1.jpg&quot; alt=&quot;Dresden&quot;&gt;&lt;/p&gt;
</content:encoded><category>travel</category><author>Matthew Brown &lt;me@matthewbrown.io&gt;</author></item><item><title>Why I&apos;m In Silicon Valley</title><link>https://matthewbrown.io/2014/11/17/why-i-m-in-silicon-valley/</link><guid isPermaLink="true">https://matthewbrown.io/2014/11/17/why-i-m-in-silicon-valley/</guid><pubDate>Mon, 17 Nov 2014 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Steve Baxter is an entrepreneur, investor, founder of River City Labs and board member for StartupAUS. He will appear as a ‘Shark’ on Channel Ten’s Shark Tank Australia when it airs in 2015. He is also the man behind the &lt;a href=&quot;http://www.startupcatalyst.com.au/&quot;&gt;Startup Catalyst&lt;/a&gt; program that has brought me and 19 other young potential entreprenuers to Silicon Valley for 12 days!
He recently wrote an article for &lt;a href=&quot;http://www.businessspectator.com.au/&quot;&gt;Business Spectator&lt;/a&gt; titled &amp;#39;Why I’m sending 20 young Queenslanders to Silicon Valley&amp;#39;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Our economy is still hugely reliant on stuff we dig out of the ground,
yet we live in an age when next generation technology is
creating opportunities to break into every industry sector.
Australia needs technologically capable and entrepreneurially inspired young people.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;You can read the rest here: &lt;a href=&quot;https://www.businessspectator.com.au/article/2014/11/17/technology/why-im-sending-20-young-queenslanders-silicon-valley&quot;&gt;Why I&amp;#39;m sending 20 young Queenslanders to Silicon Valley&lt;/a&gt; and follow all the action from Silicon Valley on our blog &lt;a href=&quot;http://blog.startupcatalyst.com.au/&quot;&gt;http://blog.startupcatalyst.com.au/&lt;/a&gt;!&lt;/p&gt;
</content:encoded><category>travel</category><author>Matthew Brown &lt;me@matthewbrown.io&gt;</author></item></channel></rss>