<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Dan&apos;s Blog</title>
    <description>A blog about software engineering
</description>
    <link>http://stefaniuk.co.uk/</link>
    <atom:link href="http://stefaniuk.co.uk/feed.xml" rel="self" type="application/rss+xml"/>
    <pubDate>Tue, 30 Dec 2025 09:20:29 +0000</pubDate>
    <lastBuildDate>Tue, 30 Dec 2025 09:20:29 +0000</lastBuildDate>
    <generator>Jekyll v4.3.4</generator>
    
      <item>
        <title>Why I&apos;m Betting on uv: The Python Package Manager I Wish Existed Years Ago</title>
        <description>&lt;p&gt;I started my career working with Java, where tools like Maven made managing dependencies simple and reliable. Everything just worked (more or less). You didn’t have to worry about setting up different tools or whether your project would build the same way every time.&lt;/p&gt;

&lt;p&gt;When I started using Python, things weren’t quite as smooth. Python is powerful and flexible, but managing environments and dependencies always felt messy compared to Java and Maven. I often wished for something more predictable and organised.&lt;/p&gt;

&lt;p&gt;That’s why &lt;a href=&quot;https://github.com/astral-sh/uv&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uv&lt;/code&gt;&lt;/a&gt; caught my eye. It’s not entirely new, it’s been around for a while, but it’s really starting to mature. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uv&lt;/code&gt; brings together everything needed for Python project management into one modern, fast tool. Here’s why I think it’s worth serious consideration.&lt;/p&gt;

&lt;h2 id=&quot;what-is-uv-and-why-should-you-care&quot;&gt;What is uv and why should you care?&lt;/h2&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uv&lt;/code&gt; is designed to replace &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pip&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pip-tools&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;virtualenv&lt;/code&gt; with a single command-line tool. If you’ve ever thought Python’s packaging story felt clunky compared to Node’s npm or yarn, uv is what you’ve been waiting for.&lt;/p&gt;

&lt;p&gt;You can install it on macOS with:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;brew &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;uv
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;or follow other &lt;a href=&quot;https://github.com/astral-sh/uv?tab=readme-ov-file#installation&quot;&gt;installation instructions&lt;/a&gt; from the uv GitHub page.&lt;/p&gt;

&lt;p&gt;If you’ve used Node.js before, uv will feel familiar, it’s like npm for Python - dependency management, environment isolation, and builds in one.&lt;/p&gt;

&lt;h2 id=&quot;quick-start-and-how-i-got-going&quot;&gt;Quick start and how I got going&lt;/h2&gt;

&lt;p&gt;The biggest compliment I can give uv is that it just works.
Here are a few examples of how I got started:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Download and manage multiple Python versions&lt;/span&gt;
uv python &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;3.12 3.13 3.14

&lt;span class=&quot;c&quot;&gt;# Pin a global default Python version&lt;/span&gt;
uv python pin &lt;span class=&quot;nt&quot;&gt;--global&lt;/span&gt; 3.14

&lt;span class=&quot;c&quot;&gt;# See where Python versions are stored&lt;/span&gt;
uv python &lt;span class=&quot;nb&quot;&gt;dir&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Create a virtual environment in the project folder&lt;/span&gt;
uv venv .venv

&lt;span class=&quot;c&quot;&gt;# Run Python inside the environment (no manual activation)&lt;/span&gt;
uv run python &lt;span class=&quot;nt&quot;&gt;--version&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Bootstrap a new app quickly&lt;/span&gt;
uv init &lt;span class=&quot;nt&quot;&gt;--app&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--name&lt;/span&gt; my-app &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--python&lt;/span&gt; 3.14 &lt;span class=&quot;nt&quot;&gt;--managed-python&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--no-readme&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--vcs&lt;/span&gt; none
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;uv keeps things tidy. You can use different Python versions in different projects, and switching between them is easy.&lt;/p&gt;

&lt;h2 id=&quot;my-initial-observations&quot;&gt;My initial observations&lt;/h2&gt;

&lt;h3 id=&quot;what-i-liked&quot;&gt;What I liked&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;Speed, installing dependencies and syncing environments is much faster than pip and virtualenv.&lt;/li&gt;
  &lt;li&gt;One tool for all tasks, no juggling between pip, pip-tools, and virtualenv.&lt;/li&gt;
  &lt;li&gt;Modern by default, everything is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pyproject.toml&lt;/code&gt;-based.&lt;/li&gt;
  &lt;li&gt;Built-in Python management, like pyenv, but integrated.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;what-could-improve&quot;&gt;What could improve&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;There’s still no single command to bump all dependencies to their latest versions (like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;yarn upgrade&lt;/code&gt;). For now, you have to update version numbers manually.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;should-you-switch-to-uv&quot;&gt;Should you switch to uv?&lt;/h3&gt;

&lt;p&gt;If you want a fast, simple, and unified Python development workflow, uv is a great choice. It’s already production-ready and has an active community of maintainers and contributors.&lt;/p&gt;

&lt;p&gt;If you rely heavily on advanced Poetry-style features, it’s worth testing first. But uv feels like the direction Python tooling is heading. For my projects, the switch has already paid off.&lt;/p&gt;

&lt;h2 id=&quot;my-everyday-usage-flow&quot;&gt;My everyday usage flow&lt;/h2&gt;

&lt;p&gt;I now use uv daily and settled into a repeatable flow that keeps things clean and reproducible.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Start fresh, remove old environment and lock files&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;rm&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-rf&lt;/span&gt; .venv uv.lock requirements.txt

&lt;span class=&quot;c&quot;&gt;# Sync dependencies based on pyproject.toml (creates a new .venv and uv.lock)&lt;/span&gt;
uv &lt;span class=&quot;nb&quot;&gt;sync&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Inspect dependency tree&lt;/span&gt;
uv tree

&lt;span class=&quot;c&quot;&gt;# Keep .toml and .txt files in sync if you need the legacy requirements files&lt;/span&gt;
uv pip compile pyproject.toml &lt;span class=&quot;nt&quot;&gt;--output-file&lt;/span&gt; requirements.txt

&lt;span class=&quot;c&quot;&gt;# Run a script directly&lt;/span&gt;
uv run python ./path/to/script.py

&lt;span class=&quot;c&quot;&gt;# Run tests with dev dependencies&lt;/span&gt;
uv &lt;span class=&quot;nb&quot;&gt;sync&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--extra&lt;/span&gt; dev
uv run pytest

&lt;span class=&quot;c&quot;&gt;# In Visual Studio Code go to:&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Command Palette (⇧⌘P) → &quot;Python: Select Interpreter&quot;&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# and choose `./.venv/bin/python` located in your project&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;A minimal working &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pyproject.toml&lt;/code&gt; example:&lt;/p&gt;

&lt;div class=&quot;language-toml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nn&quot;&gt;[project]&lt;/span&gt;
&lt;span class=&quot;py&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;my-app&quot;&lt;/span&gt;
&lt;span class=&quot;py&quot;&gt;version&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;0.1.0&quot;&lt;/span&gt;
&lt;span class=&quot;py&quot;&gt;description&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;Add your description here&quot;&lt;/span&gt;
&lt;span class=&quot;py&quot;&gt;requires-python&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;&quot;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;3.14&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;py&quot;&gt;dependencies&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;[&quot;loguru&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;0.7&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;]&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;
&lt;/span&gt;
&lt;span class=&quot;nn&quot;&gt;[dependency-groups]&lt;/span&gt;
&lt;span class=&quot;py&quot;&gt;dev&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;pytest&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This setup, one clear &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pyproject.toml&lt;/code&gt; and a consistent uv flow, has made my Python work much smoother. I no longer worry about drifting environments or mismatched dependencies.&lt;/p&gt;

&lt;h2 id=&quot;working-with-multi-package-projects-in-uv&quot;&gt;Working with multi-package projects in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uv&lt;/code&gt;&lt;/h2&gt;

&lt;p&gt;As my projects grow, I split them into separate components such as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;core&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;api&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cli&lt;/code&gt;. This is where I discovered one of uv’s best features, &lt;a href=&quot;https://docs.astral.sh/uv/concepts/projects/workspaces/&quot;&gt;workspaces&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;Workspaces let you manage several packages inside a single repository, with one lockfile, shared tooling, and clear internal dependencies. Here’s how I’ve been using them.&lt;/p&gt;

&lt;h3 id=&quot;1-create-a-top-level-workspace&quot;&gt;1. Create a top-level workspace&lt;/h3&gt;

&lt;p&gt;At the repository root, define a workspace in your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pyproject.toml&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-toml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nn&quot;&gt;[tool.uv.workspace]&lt;/span&gt;
&lt;span class=&quot;py&quot;&gt;members&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;core&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;cli&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;api&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Each folder (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;core&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cli&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;api&lt;/code&gt;) will have its own &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pyproject.toml&lt;/code&gt;, but they’ll share one virtual environment and one &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uv.lock&lt;/code&gt;. This keeps the setup unified and avoids dependency duplication.&lt;/p&gt;

&lt;p&gt;You can initialise these packages easily:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;uv init core
uv init cli
uv init api
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then create the rest of the folder structure:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;mkdir&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  core/src/core core/tests &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  cli/src/cli cli/tests &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  api/src/api api/tests
&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;core/src/core/__init__.py core/tests/test_core.py
&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;cli/src/cli/__init__.py cli/tests/test_cli.py
&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;api/src/api/__init__.py api/tests/test_api.py
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;2-link-internal-dependencies-explicitly&quot;&gt;2. Link internal dependencies explicitly&lt;/h3&gt;

&lt;p&gt;If one package depends on another, for example, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cli&lt;/code&gt; uses functions from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;core&lt;/code&gt; tell uv to link them locally at the top-level &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pyproject.toml&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-toml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nn&quot;&gt;[tool.uv.sources]&lt;/span&gt;
&lt;span class=&quot;py&quot;&gt;core&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;py&quot;&gt;workspace&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This tells uv to use your workspace version of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;core&lt;/code&gt; instead of fetching it from PyPI. Without this mapping, uv would assume &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;core&lt;/code&gt; is an external dependency and try to install it from the registry.&lt;/p&gt;

&lt;h3 id=&quot;3-keep-configuration-dry-dont-repeat-yourself&quot;&gt;3. Keep configuration DRY (Don’t Repeat Yourself)&lt;/h3&gt;

&lt;p&gt;For shared tools such as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pytest&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ruff&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mypy&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;black&lt;/code&gt;, define them only once in the root &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pyproject.toml&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-toml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nn&quot;&gt;[dependency-groups]&lt;/span&gt;
&lt;span class=&quot;py&quot;&gt;dev&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;pytest&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;ruff&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;black&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;mypy&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;nn&quot;&gt;[tool.pytest.ini_options]&lt;/span&gt;
&lt;span class=&quot;py&quot;&gt;testpaths&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;core/tests&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;cli/tests&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;api/tests&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;tests&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;py&quot;&gt;addopts&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;-ra -q&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This setup keeps your test and lint configuration consistent across all packages. Each sub-package automatically uses these tools when you run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uv sync --extra dev&lt;/code&gt;. Keep in mind that uv commands, especially &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uv sync&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uv run&lt;/code&gt;, should be executed from the root workspace directory, not from within an individual package. That’s how uv resolves shared dependencies and applies your top-level configuration correctly.&lt;/p&gt;

&lt;h3 id=&quot;4-handle-imports-cleanly-in-src-layouts&quot;&gt;4. Handle imports cleanly in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;src/&lt;/code&gt; layouts&lt;/h3&gt;

&lt;p&gt;If you’re following the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;src/&lt;/code&gt; layout (e.g. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;core/src/core&lt;/code&gt;), Python needs to know where to find those modules. You can fix this by adding to the root pytest config:&lt;/p&gt;

&lt;div class=&quot;language-toml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nn&quot;&gt;[tool.pytest.ini_options]&lt;/span&gt;
&lt;span class=&quot;py&quot;&gt;pythonpath&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;core/src&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;api/src&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;cli/src&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This makes imports like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;from core import something&lt;/code&gt; work during test discovery.&lt;/p&gt;

&lt;h3 id=&quot;5-versioning-without-duplication&quot;&gt;5. Versioning without duplication&lt;/h3&gt;

&lt;p&gt;You don’t need to define &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;__version__&lt;/code&gt; manually in every package. Instead, read it directly from your project metadata:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;importlib.metadata&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;version&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;PackageNotFoundError&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;try&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;__version__&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;version&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;__name__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;except&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;PackageNotFoundError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;__version__&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;0.0.0&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This small snippet keeps the version in sync with your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pyproject.toml&lt;/code&gt; automatically. It’s one of those “set it once and forget it” improvements that make the workspace cleaner.&lt;/p&gt;

&lt;h3 id=&quot;6-organise-integration-and-cross-package-tests-at-the-root&quot;&gt;6. Organise integration and cross-package tests at the root&lt;/h3&gt;

&lt;p&gt;Place integration tests that touch multiple packages in a shared folder at the repository root:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;tests/
├── test_integration_api_core.py
└── test_integration_cli_core.py
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Since your root pytest config already points to all test folders, running tests from the top-level works out of the box:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;uv run pytest
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This makes it easy to test your system as a whole without leaving the workspace root.&lt;/p&gt;

&lt;h2 id=&quot;effective-project-structure&quot;&gt;Effective project structure&lt;/h2&gt;

&lt;p&gt;The layout below shows a clean, maintainable multi-package setup using uv workspaces. This pattern scales well for modular back-end systems, internal libraries, and CLI utilities that need to evolve together but remain logically separate.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;.
├── api/                             # package: API
│   ├── src/api/
│   │   └── __init__.py              # API package root
│   ├── tests/
│   │   └── test_api.py              # unit tests for API
│   └── pyproject.toml               # package-level metadata
│
├── cli/                             # package: CLI
│   ├── src/cli/
│   │   └── __init__.py              # CLI package root
│   ├── tests/
│   │   └── test_cli.py              # unit tests for CLI
│   └── pyproject.toml               # package-level metadata
│
├── core/                            # package: Core business logic
│   ├── src/core/
│   │   └── __init__.py              # Core package root
│   ├── tests/
│   │   └── test_core.py             # unit tests for Core
│   └── pyproject.toml               # package-level metadata
│
├── tests/                           # root-level integration tests
│   ├── test_integration_api_core.py # cross-package integration test
│   └── test_integration_cli_core.py # cross-package integration test
│
├── .env.default                     # example environment variables
├── .gitignore                       # git ignore rules
├── .python-version                  # pinned Python version
├── Makefile                         # developer &amp;amp; CI/CD shortcuts (build, test, clean, etc.)
├── pyproject.toml                   # workspace root config (defines uv workspace)
└── README.md                        # project overview and documentation
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;final-takeaway&quot;&gt;Final takeaway&lt;/h2&gt;

&lt;p&gt;uv’s workspace model has become one of its most powerful (and underrated) features. It keeps multi-package repositories clean, removes duplication, and makes local development feel seamless.&lt;/p&gt;

&lt;p&gt;For me, it’s the first time Python tooling has felt coherent across a full stack of packages, the kind of “batteries included” experience I wish we’d always had.&lt;/p&gt;
</description>
        <pubDate>Mon, 28 Jul 2025 07:22:00 +0100</pubDate>
        <link>http://stefaniuk.co.uk/why-im-betting-on-uv</link>
        <guid isPermaLink="true">http://stefaniuk.co.uk/why-im-betting-on-uv</guid>
        
        <category>uv</category>
        
        <category>python</category>
        
        <category>tools</category>
        
        <category>learning</category>
        
        
      </item>
    
      <item>
        <title>A Revised Way of Generating SSH Keys</title>
        <description>&lt;p&gt;I’ve been using 4096-bit RSA SSH keys for quite a few years. The RSA keys are very compatible. They’ve been working on various operating systems as well as on mobile devices. They are also known as being slow and potentially insecure if created with a small amount of bits, especially after the year 2013. Today, I decided to do some research to find an alternative configuration. Things have moved on since my last check. Large keys are still considered secure, however the &lt;a href=&quot;https://en.wikipedia.org/wiki/Elliptic_curve_cryptography&quot;&gt;&lt;strong&gt;elliptic curve cryptography&lt;/strong&gt;&lt;/a&gt; has become much more popular in the recent decade.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“The primary benefit promised by elliptic curve cryptography is a smaller key size, reducing storage and transmission requirements, i.e. that an elliptic curve group could provide the same level of security afforded by an RSA-based system with a large modulus and correspondingly larger key: for example, a 256-bit elliptic curve public key should provide comparable security to a 3072-bit RSA public key.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;My take on it is that if this new signature algorithm can give us similar or even &lt;strong&gt;better level of security&lt;/strong&gt; (yes, its implementation is more secure than RSA) with a &lt;strong&gt;comparable flexibility&lt;/strong&gt; using &lt;strong&gt;less resources&lt;/strong&gt;, it is definitely time to adopt it. For anyone who is interested in a detailed explanation how exactly it works a lot of papers have been published providing mathematical rationale behind it.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ed25519.cr.yp.to/&quot;&gt;&lt;strong&gt;Ed25519&lt;/strong&gt;&lt;/a&gt; is a digital signature scheme based on Twisted Edwards curves. Its has been &lt;a href=&quot;https://github.com/openssh/openssh-portable/commit/5be9d9e3cbd9c66f24745d25bf2e809c1d158ee0#diff-e71776f50c4432cb9cd999367424de20&quot;&gt;added to the OpenSSH codebase&lt;/a&gt; on the 7th of December 2013. Since then no issue was reported against it.&lt;/p&gt;

&lt;p&gt;The easies way to generate such an SSH key is by using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ssh-keygen&lt;/code&gt; command of the OpenSSH package:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ssh-keygen &lt;span class=&quot;nt&quot;&gt;-t&lt;/span&gt; ed25519 &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-a&lt;/span&gt; 100
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-t ed25519&lt;/code&gt; parameter is used to generate two asymmetrical keys based on the elliptic curve cryptography. The output is produced in the new &lt;a href=&quot;https://tools.ietf.org/html/rfc4716&quot;&gt;RFC4716&lt;/a&gt; format rather than the well known PEM and the flag &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-o&lt;/code&gt; to support it is implied by default so it can be omitted. It enforces use of a modern key derivation function (KDF) powered by combination of &lt;a href=&quot;https://github.com/openssh/openssh-portable/blob/f104da263de995f66b6861b4f3368264ee483d7f/openbsd-compat/bcrypt_pbkdf.c&quot;&gt;PBKDF2 and bcrypt&lt;/a&gt;. In practice this means that our keypair is more resistant to brute-force password cracking. This is especially true when combined with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-a &amp;lt;rounds&amp;gt;&lt;/code&gt; parameter which specifies the number of key derivation function rounds to be used. Higher the number more time it takes to unlock the key.&lt;/p&gt;

&lt;p&gt;Time to see the result of the above command. Here is an example of my revised way of generating SSH keys along with content of the files.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;ssh-keygen &lt;span class=&quot;nt&quot;&gt;-t&lt;/span&gt; ed25519 &lt;span class=&quot;nt&quot;&gt;-a&lt;/span&gt; 100 &lt;span class=&quot;nt&quot;&gt;-C&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;my-key-comment&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; my-key-name
Generating public/private ed25519 key pair.
Enter passphrase &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;empty &lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;no passphrase&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;:
Enter same passphrase again:
Your identification has been saved &lt;span class=&quot;k&quot;&gt;in &lt;/span&gt;my-key-name.
Your public key has been saved &lt;span class=&quot;k&quot;&gt;in &lt;/span&gt;my-key-name.pub.
The key fingerprint is:
SHA256:GIh0NDZ6WhZLyFJxq8dpbJ7aAwfEpJdgBsgxs/5NziM my-key-comment
The key&lt;span class=&quot;s1&quot;&gt;&apos;s randomart image is:
+--[ED25519 256]--+
|*@*+X            |
|==OO B           |
|.+= B .          |
|...O . o         |
| .o.B.. S        |
|  o=*.           |
|   Eo=           |
|   oo .          |
|  . ..           |
+----[SHA256]-----+
$ puttygen my-key-name -C &quot;my-key-comment&quot; -o my-key-name.ppk
Enter passphrase to load key:
$ ls -la my-key-name*
-rw-------  1 dan  staff  464  3 Sep 21:27 my-key-name
-rw-------  1 dan  staff  304  3 Sep 21:27 my-key-name.ppk
-rw-r--r--  1 dan  staff   96  3 Sep 21:27 my-key-name.pub
$ cat my-key-name
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jYmMAAAAGYmNyeXB0AAAAGAAAABA+HlsP7o
3Jzj8b+N+WhbEuAAAAZAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIErbaYC9ZHLEtCY3
uDDl0LqRwlxLSGoHO1IBrvhzikyVAAAAoD632B3FjYbZmkLco+r7BVIvK3r1Cn2dJE8Q4N
l9IES8bieUYPxDi3uu3gaIcWwygdHTM5zFMqWePJgNP2M0jvfLkQLbaJd816D/BYwUGcTR
3Mgo7Rnf1qqoen70rwl67bbTaN8D0M5dNL5EkYdKvOoiRhoyQEIdptNOWF6rjwMyfloWw9
JMKCOfYrSUB4pDf6mgbWw7+60IUrIc+BeAQgc=
-----END OPENSSH PRIVATE KEY-----
$ cat my-key-name.pub
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIErbaYC9ZHLEtCY3uDDl0LqRwlxLSGoHO1IBrvhzikyV my-key-comment
$ cat my-key-name.ppk
PuTTY-User-Key-File-2: ssh-ed25519
Encryption: aes256-cbc
Comment: my-key-comment
Public-Lines: 2
AAAAC3NzaC1lZDI1NTE5AAAAIErbaYC9ZHLEtCY3uDDl0LqRwlxLSGoHO1IBrvhz
ikyV
Private-Lines: 1
63bsFlto1i20tXwFu3xveXsGtDD7hEmsM0FrIGqviHn4O2xXG9Pwjmhmwf+z8rJe
Private-MAC: 72102d9a0f158f653577df276fcf329b482df329
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
</description>
        <pubDate>Sat, 02 Sep 2017 22:16:00 +0100</pubDate>
        <link>http://stefaniuk.co.uk/a-revised-way-of-generating-ssh-keys</link>
        <guid isPermaLink="true">http://stefaniuk.co.uk/a-revised-way-of-generating-ssh-keys</guid>
        
        <category>ssh</category>
        
        <category>security</category>
        
        <category>cryptography</category>
        
        <category>ed25519</category>
        
        <category>linux</category>
        
        <category>devops</category>
        
        
      </item>
    
      <item>
        <title>Create Consistent SSH User Accounts Across Multiple Linux Servers</title>
        <description>&lt;p&gt;Due to a specific design requirements on a project I’ve been working on recently I needed to create a consistent system account on all the Linux servers across all the environments. This account supposed to have the following characteristics:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Same GID and UID&lt;/li&gt;
  &lt;li&gt;Both password and public SSH key are set&lt;/li&gt;
  &lt;li&gt;Only the SSH key-based authentication is allowed&lt;/li&gt;
  &lt;li&gt;Privileged operations are executed without having to provide password (optional)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As you can tell this is an admin type account that is usually created just after the installation of an operating system.&lt;/p&gt;

&lt;p&gt;Servers are up and running. Setting it up by hand could be an option. However, this would be a very old-fashioned approach though. I already have an automation tool in place. My favorite one is Ansible due to its simple architecture. Whether or not our configuration management framework is really advanced and facilitates tweaking of every aspect of an OS or a VM in my opinion the best language to script it is Bash. The change will be expressed in a most readable way that every sysadmin or platform engineer as they are called these days will understand. I’m going to use Ansible only as an orchestration tool to ship the script across and execute it without any user interaction.&lt;/p&gt;

&lt;p&gt;Here is our sample input:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;&lt;span class=&quot;nb&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;1000
&lt;span class=&quot;nv&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;dan&quot;&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;s3cr3t&quot;&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;ssh_pk&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDGA++RFH4MfQ+697dSNS+p2Hx0a4X56pnDrMIbDozYUPcbY6rictsJvsakaF2yMlR9xW0nEWichdwB5dFxoVQ9E6n6ygRB5Q88j+SOsvVA+wjCJ0+Ittfrb9/dTrnuSWT58p1HKaCDOJdx402P41DZGw4Fq+JKTEvP+H0AbOdUifNMrZBPZ0kOv/0bNT839ytI6rKGi+3w42TN37k7H02TmAFPnip3YpLrUMNxMHulxyzaQ2ueAILGmPac2pz6hU6OflSCkptiV7yDYv/LnjFDSzagFP0dIr5jkT+XenpJOXgdXA/g3/4aOUHfo9tEQngC9ANKbaB3pRwAfGM35V35 my-key&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;But before we create such a user let’s check if the ID that we intent to use is available. If it is already taken we need to find the current user, terminate all his processes, change the UID and GID, then fix file ownership accordingly.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;&lt;span class=&quot;nb&quot;&gt;uname&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$id&lt;/span&gt; /etc/passwd | &lt;span class=&quot;nb&quot;&gt;cut&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-f1&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt;:&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;gname&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$id&lt;/span&gt; /etc/group | &lt;span class=&quot;nb&quot;&gt;cut&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-f1&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt;:&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;
ps &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; pid &lt;span class=&quot;nt&quot;&gt;-u&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$uname&lt;/span&gt; | xargs &lt;span class=&quot;nb&quot;&gt;kill&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-9&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$uname&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
    &lt;/span&gt;usermod &lt;span class=&quot;nt&quot;&gt;-u&lt;/span&gt; 1111 &lt;span class=&quot;nv&quot;&gt;$uname&lt;/span&gt;
    find / &lt;span class=&quot;nt&quot;&gt;-user&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$id&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-exec&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;chown&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-h&lt;/span&gt; 1111 &lt;span class=&quot;o&quot;&gt;{}&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;fi
if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$gname&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
    &lt;/span&gt;groupmod &lt;span class=&quot;nt&quot;&gt;-g&lt;/span&gt; 1111 &lt;span class=&quot;nv&quot;&gt;$gname&lt;/span&gt;
    find / &lt;span class=&quot;nt&quot;&gt;-group&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$id&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-exec&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;chgrp&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-h&lt;/span&gt; 1111 &lt;span class=&quot;o&quot;&gt;{}&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;fi&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Now, we are ready to create a new system user.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;groupadd &lt;span class=&quot;nt&quot;&gt;-g&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$id&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$username&lt;/span&gt;
useradd &lt;span class=&quot;nt&quot;&gt;-m&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-s&lt;/span&gt; /bin/bash &lt;span class=&quot;nt&quot;&gt;-u&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$id&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-g&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$username&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-G&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;sudo&lt;/span&gt;,docker &lt;span class=&quot;nv&quot;&gt;$username&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$username&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$password&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; | chpasswd&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;SSH key-based authentication provides cryptographic strength that even extremely long passwords cannot offer. So, this will be our preferred method of granting access to a system and …&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;&lt;span class=&quot;nb&quot;&gt;mkdir&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; /home/&lt;span class=&quot;nv&quot;&gt;$username&lt;/span&gt;/.ssh
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$ssh_pk&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; /home/&lt;span class=&quot;nv&quot;&gt;$username&lt;/span&gt;/.ssh/authorized_keys
&lt;span class=&quot;nb&quot;&gt;chmod &lt;/span&gt;600 /home/&lt;span class=&quot;nv&quot;&gt;$username&lt;/span&gt;/.ssh/authorized_keys
&lt;span class=&quot;nb&quot;&gt;chown&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-R&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$username&lt;/span&gt;:&lt;span class=&quot;nv&quot;&gt;$username&lt;/span&gt; /home/&lt;span class=&quot;nv&quot;&gt;$username&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;… we make sure the above is the only method of authentication by changing the OpenSSL daemon configuration.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;&lt;span class=&quot;nb&quot;&gt;sed&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-i&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;s/^PasswordAuthentication yes/#PasswordAuthentication yes/g&quot;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    /etc/ssh/sshd_config&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;The last thing we may want to do is to allow privileged operations without password. This may come handy while the same system account is used to perform other automated tasks that normally would prompt for a user password.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$username&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; ALL=(ALL) NOPASSWD: ALL&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; /etc/sudoers.d/&lt;span class=&quot;nv&quot;&gt;$username&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;There we have it. A simple way to test it could be by loading the private key locally using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ssh-add&lt;/code&gt; command and trying to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ssh&lt;/code&gt; into the server.&lt;/p&gt;

&lt;p&gt;In my next blog post I will show how to create a minimal Ansible orchestration script.&lt;/p&gt;
</description>
        <pubDate>Tue, 22 Aug 2017 22:29:00 +0100</pubDate>
        <link>http://stefaniuk.co.uk/create-consistent-ssh-user-across-linux-servers</link>
        <guid isPermaLink="true">http://stefaniuk.co.uk/create-consistent-ssh-user-across-linux-servers</guid>
        
        <category>linux</category>
        
        <category>ssh</category>
        
        <category>ansible</category>
        
        <category>devops</category>
        
        <category>automation</category>
        
        <category>security</category>
        
        <category>bash</category>
        
        
      </item>
    
      <item>
        <title>An Alternative Approach to Estimates</title>
        <description>&lt;h2 id=&quot;problem&quot;&gt;Problem&lt;/h2&gt;

&lt;p&gt;A while ago I was asked to plan one of the last phases of development on a project that required re-architecture of the data access layer. One of my tasks was to estimate an effort to replace code that communicates with a legacy back-end. The new code, instead of directly connecting to a database should consume JSON-based REST API endpoints. Due to time constraints and to mitigate the risk to bring the service down this change supposed to be done in as non-invasive way as possible.&lt;/p&gt;

&lt;p&gt;Objectives&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Functionality of the application must remain unchanged for the end user&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Facts&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;It is a simple MVC application built upon a reach domain model&lt;/li&gt;
  &lt;li&gt;Books like &lt;em&gt;&lt;a href=&quot;https://www.amazon.co.uk/Design-patterns-elements-reusable-object-oriented/dp/0201633612&quot;&gt;Design Patterns&lt;/a&gt;&lt;/em&gt; and &lt;em&gt;&lt;a href=&quot;https://www.amazon.co.uk/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882&quot;&gt;Clean Code&lt;/a&gt;&lt;/em&gt; were probably not that popular at the time of writing the software&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Issues&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Developers’ skills in our team differ much&lt;/li&gt;
  &lt;li&gt;Complexity of the code that needs to be refactored varies from module to module&lt;/li&gt;
  &lt;li&gt;We have never done that before&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Assumptions&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;We know the product already&lt;/li&gt;
  &lt;li&gt;Most of the code logic and flow remain the same&lt;/li&gt;
  &lt;li&gt;It should take no longer to refactor the code than implementing that functionality from scratch&lt;/li&gt;
  &lt;li&gt;A lot of work is repetitive and follows a pattern&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;How to take the above factors into account and produce accurate estimates for the management?&lt;/p&gt;

&lt;h2 id=&quot;solution&quot;&gt;Solution&lt;/h2&gt;

&lt;p&gt;I was not able to find anyone who faced similar technical problems with the platform we use. This tells me something about our design, anyway… Yes, I heard about &lt;a href=&quot;http://csse.usc.edu/tools/COCOMOII.php&quot;&gt;COCOMO II - Constructive Cost Model&lt;/a&gt;. However, I still decided to come up with my simplistic formula.&lt;/p&gt;

&lt;p&gt;  
\(\scriptsize
{
    \sum_{i=0}^n componentManDays\_i = totalManDays
}\)
\(\scriptsize
{
    \frac{linesOfCode\_i}{avgLinesOfCodePerHour \times 6h} \times complexityFactor\_i \times contingency = componentManDays\_i
}\)
  &lt;/p&gt;

&lt;p&gt;Why? Because whichever method is used the outcome is always a guesstimate. I found out that &lt;strong&gt;it is so much dependent on motivation of individual team members&lt;/strong&gt; as well as their willingness to learn and contribute. This can not be expressed by any mathematical equation.&lt;/p&gt;

&lt;p&gt;I usually add 20% contingency to my calculations and set complexity factor to &lt;em&gt;cf=1&lt;/em&gt; for a simple code in higher layers of an application architecture, &lt;em&gt;cf=2&lt;/em&gt; for components that require more analytical and problem solving skills and up to &lt;em&gt;cf=3&lt;/em&gt; for tasks that require attention of a polyglot programmer.&lt;/p&gt;

&lt;h2 id=&quot;automation&quot;&gt;Automation&lt;/h2&gt;

&lt;p&gt;After considering a manual approach of producing my estimates for each module and component I decided to automate that process. Quick, and in my opinion relatively reliable solution was to base the calculations on an ability to change number of lines of code by an average developer in our team in a selected period of time. There are number of issues with this approach. For example, &lt;strong&gt;data for the empirical analysis must come from a project with a comparable architecture, technology stack, complexity and codebase&lt;/strong&gt;. Fortunately, we have been working on this software already for over a year delivering new functionality. So, all the data I needed were in the version control system… The only thing I needed to do is write a script that does the rest of the work for me.&lt;/p&gt;

&lt;p&gt;Here is the usage example followed by some of the Bash functions to help to extract relevant information from Git. I used an open source project to produce the output.&lt;/p&gt;

&lt;p&gt; &lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;git_print_stats 1y &lt;span class=&quot;c&quot;&gt;# for Linux use &quot;1 year&quot;&lt;/span&gt;
Developer  1       1                     46 +23:-23
Developer  2       1                     79 +44:-35
Developer  3       0                        0 +0:-0
Developer  4       5                      46 +40:-6
Developer  5       0                        0 +0:-0
Developer  6       1                        2 +1:-1
Developer  7       0                        0 +0:-0
Developer  8       0                        0 +0:-0
Developer  9       0                        0 +0:-0
Developer 10       1                        2 +1:-1
Developer 11       3                       17 +9:-8
Developer 12       2                      61 +57:-4
Developer 13      30                   545 +455:-90
Developer 14      30                   545 +455:-90
Developer 15       4                   117 +101:-16
Developer 16       1                        4 +2:-2
Developer 17       0                        0 +0:-0
Developer 18       1                      15 +13:-2
Developer 19       6                     79 +63:-16
Developer 20       3                       16 +8:-8
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;git_calc_avg_changes_per_dev 1y
78
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;source_count_lines ./module/A &lt;span class=&quot;k&quot;&gt;*&lt;/span&gt;.ext
3595
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;  &lt;/p&gt;

&lt;p&gt;Calculate average line changes per developer in a given period of time.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;&lt;span class=&quot;k&quot;&gt;function &lt;/span&gt;git_calc_avg_changes_per_dev&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;git_print_stats &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--csv&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;awk&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-F&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;,&apos;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;{ print $3 }&apos;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;sum&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;0&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;0
    &lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;n &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do
        &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sum&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;$((&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$sum&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$n&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;))&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;((&lt;/span&gt;i++&lt;span class=&quot;o&quot;&gt;))&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;done
    &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;$((&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$sum&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$i&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;))&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Print number of commits and line changes for each individual contributor.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;&lt;span class=&quot;k&quot;&gt;function &lt;/span&gt;git_print_stats&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;OLDIFS&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$IFS&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;IFS&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;$&apos;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;#&apos;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;committer &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;git_list_committers&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do
        &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;cc&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;git_count_commits &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$committer&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;cl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;git_count_line_changes &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$committer&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$*&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; _&lt;span class=&quot;s2&quot;&gt;&quot;--csv&quot;&lt;/span&gt;_ &lt;span class=&quot;o&quot;&gt;]]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
            &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;al&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$cl&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;awk&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;{ print $1 }&apos;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;nv&quot;&gt;dl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$cl&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;awk&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;{ print $2 }&apos;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;nb&quot;&gt;printf&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;%s&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;,%s,%s,%s&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$committer&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$cc&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$al&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$dl&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;else
            &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;printf&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;%40s %7s %30s&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$committer&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$cc&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$cl&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;fi
    done
    &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;IFS&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$OLDIFS&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;List all contributors.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;&lt;span class=&quot;k&quot;&gt;function &lt;/span&gt;git_list_committers&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    git log &lt;span class=&quot;nt&quot;&gt;--all&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--format&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;%cN&apos;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;sort&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-u&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Return number of commits for a given author.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;&lt;span class=&quot;k&quot;&gt;function &lt;/span&gt;git_count_commits&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    git log &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;--author&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;--since&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;date_ago &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$2&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;--until&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;date&lt;/span&gt; +%Y/%m/%d&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;--no-merges&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;--pretty&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;%h [%s] &amp;lt;%an&amp;gt;&apos;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;--abbrev-commit&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    | &lt;span class=&quot;nb&quot;&gt;wc&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-l&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;tr&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;[[:space:]]&apos;&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Return number of line changes for a given author.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;&lt;span class=&quot;k&quot;&gt;function &lt;/span&gt;git_count_line_changes&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    git log &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;--author&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;--since&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;date_ago &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$2&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;--until&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;date&lt;/span&gt; +%Y/%m/%d&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;--no-merges&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;--pretty&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;%h [%s] &amp;lt;%an&amp;gt;&apos;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;--abbrev-commit&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;--numstat&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    | &lt;span class=&quot;nb&quot;&gt;awk&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;NF==3 \
        { plus+=$1; minus+=$2; total+=$1+$2 } END \
        { printf(&quot;%d +%d:-%d\n&quot;, total, plus, minus) }&apos;&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Calculate date in the past from now.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;&lt;span class=&quot;k&quot;&gt;function &lt;/span&gt;date_ago&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$OSTYPE&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;darwin&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then&lt;/span&gt;
        &lt;span class=&quot;c&quot;&gt;# 1y, 12m, 52w, 365d, etc.&lt;/span&gt;
        &lt;span class=&quot;nb&quot;&gt;date&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-v-&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt; +%Y/%m/%d
    &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;
        &lt;span class=&quot;c&quot;&gt;# 1 year, 12 months, 52 weeks, 365 days, etc.&lt;/span&gt;
        &lt;span class=&quot;nb&quot;&gt;date&lt;/span&gt; +%Y/%m/%d &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;-&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fi&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Count recursively all lines in files for a give file type.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;&lt;span class=&quot;k&quot;&gt;function &lt;/span&gt;source_count_lines&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    find &lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-name&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$2&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-print0&lt;/span&gt; | xargs &lt;span class=&quot;nt&quot;&gt;-0&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;cat&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;wc&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-l&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt; &lt;/p&gt;
</description>
        <pubDate>Sun, 03 Jul 2016 21:18:13 +0100</pubDate>
        <link>http://stefaniuk.co.uk/an-alternative-approach-to-estimates</link>
        <guid isPermaLink="true">http://stefaniuk.co.uk/an-alternative-approach-to-estimates</guid>
        
        <category>project-management</category>
        
        <category>estimation</category>
        
        <category>software-development</category>
        
        <category>methodology</category>
        
        <category>git</category>
        
        
      </item>
    
      <item>
        <title>Hello, World!</title>
        <description>&lt;p&gt;I decided to blog. This is long overdue. I should have started this years ago. This begs the question why didn’t I? I code at work I code at home. Time is very precious for me… and yes, there are more important things in live than a hobby. But I’m going to try anyway!&lt;/p&gt;

&lt;p&gt;There is an objective, self-improvement. Continuous learning is part of being a good programmer. The world around us changes quickly. Coping with change is no longer sufficient. We need to embrace it.&lt;/p&gt;

&lt;p&gt;I also believe that keeping notes of what I do will help me gain better understanding of the problems I solve. Maintaining good reading habits and writing a lot of code is one thing. But sharing the knowledge and being able to explain all of that to others requires deep insight - &lt;strong&gt;here is my challenge&lt;/strong&gt;!&lt;/p&gt;
</description>
        <pubDate>Fri, 04 Mar 2016 07:21:52 +0000</pubDate>
        <link>http://stefaniuk.co.uk/hello-world</link>
        <guid isPermaLink="true">http://stefaniuk.co.uk/hello-world</guid>
        
        <category>blogging</category>
        
        <category>personal</category>
        
        <category>professional-development</category>
        
        
      </item>
    
  </channel>
</rss>
