<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>emre şahin's digital garden 🍃 - Python</title>
    <link>https://emresahin.net/tags/python/</link>
    <description>Posts in the Python tag</description>
    <language>en</language>
    <managingEditor>contact@emresahin.net (Emre Şahin)</managingEditor>
    <lastBuildDate>Tue, 15 Sep 2026 19:46:32 +0000</lastBuildDate>
    <atom:link href="https://emresahin.net/tags/python/rss.xml" rel="self" type="application/rss+xml"/>
    <item>
      <title>devlog 32</title>
      <published>2025-07-12T16:10:36+00:00</published>
      <updated>2025-07-12T16:10:36+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Sat, 12 Jul 2025 16:10:36 +0000</pubDate>
      <link>https://emresahin.net/devlog-32/</link>
      <guid isPermaLink="true">https://emresahin.net/devlog-32/</guid>
      <description>This dialog is about converting ColQwen2 to the ONNX format. 🐢 Now, I have two classes. This one is a copy of the ONNX Patcher that’s used to convert ColQwen2 to ONNX format. But this one is valid only for the underlying model, that is Qwen2VLForConditionalGeneration , and ColQwen2 also has some ...</description>
      <category>devlog</category>
      <category>ONNX</category>
      <category>conversions</category>
      <category>ColQwen2</category>
      <category>machine-learning</category>
      <category>python</category>
      <category>torch</category>
      <content:encoded><![CDATA[<p><em>This dialog is about converting ColQwen2 to the ONNX format.</em></p>
<p>🐢 Now, I have two classes. This one is a copy of the ONNX Patcher that’s used to convert ColQwen2 to ONNX format. But this one is valid only for the underlying model, that is <code>Qwen2VLForConditionalGeneration</code>, and <code>ColQwen2</code> also has some modifications to call it.</p>
<p>🐇 What are these modifications?</p>
<p>🐢 The <code>forward</code> method looks something like this:</p>
<h2 id="colqwen2">ColQwen2</h2>
<pre><code class="language-python">    def forward(self, *args, **kwargs) -&gt; torch.Tensor:
        kwargs.pop("output_hidden_states", None)

        # Handle the custom "pixel_values" input obtained with `ColQwen2Processor` through unpadding
        if "pixel_values" in kwargs:
            offsets = kwargs["image_grid_thw"][:, 1] * kwargs["image_grid_thw"][:, 2]  # (batch_size,)
            kwargs["pixel_values"] = torch.cat(
                [pixel_sequence[:offset] for pixel_sequence, offset in zip(kwargs["pixel_values"], offsets)],
                dim=0,
            )

        position_ids, rope_deltas = self.get_rope_index(
            input_ids=kwargs["input_ids"],
            image_grid_thw=kwargs.get("image_grid_thw", None),
            video_grid_thw=None,
            attention_mask=kwargs.get("attention_mask", None),
        )
        last_hidden_states = self.inner_forward(
            *args, **kwargs, position_ids=position_ids, use_cache=False, output_hidden_states=True
        )  # (batch_size, sequence_length, hidden_size)

        proj = self.custom_text_proj(last_hidden_states)  # (batch_size, sequence_length, dim)

        # L2 normalization
        proj = proj / proj.norm(dim=-1, keepdim=True)  # (batch_size, sequence_length, dim)
        proj = proj * kwargs["attention_mask"].unsqueeze(-1)  # (batch_size, sequence_length, dim)

        if "pixel_values" in kwargs and self.mask_non_image_embeddings:
            # Pools only the image embeddings
            image_mask = (kwargs["input_ids"] == self.config.image_token_id).unsqueeze(-1)
            proj = proj * image_mask
        return proj
</code></pre>
<p>🐇 How about the <code>inner_forward</code> method? Does it just directly call <code>Qwen2VLForConditionalGeneration</code>’s <code>forward</code>?</p>
<p>🐢 No, it has some conditionals:</p>
<pre><code class="language-python">        if inputs_embeds is None:
            inputs_embeds = self.model.embed_tokens(input_ids)
            if pixel_values is not None:
                pixel_values = pixel_values.type(self.visual.get_dtype())
                image_embeds = self.visual(pixel_values, grid_thw=image_grid_thw)
                image_mask = (input_ids == self.config.image_token_id).unsqueeze(-1).expand_as(inputs_embeds)
                image_embeds = image_embeds.to(inputs_embeds.device, inputs_embeds.dtype)
                inputs_embeds = inputs_embeds.masked_scatter(image_mask, image_embeds)

            if pixel_values_videos is not None:
                pixel_values_videos = pixel_values_videos.type(self.visual.get_dtype())
                video_embeds = self.visual(pixel_values_videos, grid_thw=video_grid_thw)
                video_mask = (input_ids == self.config.video_token_id).unsqueeze(-1).expand_as(inputs_embeds)
                video_embeds = video_embeds.to(inputs_embeds.device, inputs_embeds.dtype)
                inputs_embeds = inputs_embeds.masked_scatter(video_mask, video_embeds)

            if attention_mask is not None:
                attention_mask = attention_mask.to(inputs_embeds.device)

        outputs = self.model(
            input_ids=None,
            position_ids=position_ids,
            attention_mask=attention_mask,
            past_key_values=past_key_values,
            inputs_embeds=inputs_embeds,
            use_cache=use_cache,
            output_attentions=output_attentions,
            output_hidden_states=output_hidden_states,
            return_dict=return_dict,
        )

        hidden_states = outputs[0]
        return hidden_states
</code></pre>
<p>🦊 The patcher only modifies the <code>past_key_values_args</code> and moves them to a <code>DynamicCache</code>. There are no other changes for patching.</p>
<p>🐢 But the return types are also different. <code>Qwen2VLForConditionalGeneration.forward</code> returns a <code>Union[Tuple, Qwen2VLCausalLMOutputWithPast]</code>, but <code>ColQwen2.forward</code> returns a <code>torch.Tensor</code>.</p>
<p>🐇 How is that <code>torch.Tensor</code> calculated from the return type of <code>Qwen2VLForConditionalGeneration</code>? ❓</p>
<p>🦊 There are multiple return types in <code>Qwen2VLForConditionalGeneration</code>. It’s determined by the <code>return_dict</code> argument.</p>
<pre><code class="language-python">
        if not return_dict:
            output = (logits,) + outputs[1:]
            return (loss,) + output if loss is not None else output

        return Qwen2VLCausalLMOutputWithPast(
            loss=loss,
            logits=logits,
            past_key_values=outputs.past_key_values,
            hidden_states=outputs.hidden_states,
            attentions=outputs.attentions,
            rope_deltas=self.rope_deltas,
        )
</code></pre>
<p>🐇 What’s that argument in <code>ColQwen2</code>?</p>
<p>🐢 It’s passed from the caller; it’s a <code>kwarg</code>.</p>
<p>🐇 Is there any modification to this in the ONNX patcher?</p>
<p>🐢 Nope.</p>
<p>🐇 Then we can consider it as the default. What’s the default?</p>
<p>🐢 The default is <code>None</code>. Hence it returns <code>(logits,) + outputs[1:]</code>.</p>
<p>🐇 Then, <code>logits</code> are the first parameter by default.</p>
<p>🐢 Yes, we can assume this.</p>
<p>🐇 What does <code>ColQwen2</code> do with these logits?</p>
<p>🦊 By the way, it calls <code>inner_forward</code> with <code>use_cache=False</code> and <code>output_hidden_states=True</code>. What do these change in <code>Qwen2VLForConditionalGeneration</code>?</p>
<p>🐢 These hidden states are actually output. In <code>ColQwen2.forward</code>, the <code>inner_forward</code> call is actually:</p>
<pre><code class="language-python">        last_hidden_states = self.inner_forward(
            *args, **kwargs, position_ids=position_ids, use_cache=False, output_hidden_states=True
        )  # (batch_size, sequence_length, hidden_size)
</code></pre>
<p>and the <code>output</code> is the hidden states.</p>
<p>🦊 It then runs:</p>
<pre><code class="language-py">        proj = self.custom_text_proj(last_hidden_states)  # (batch_size, sequence_length, dim)
</code></pre>
<p>and calculates a set of projections.</p>
<p>🐢 <code>custom_set_proj</code> is defined as:</p>
<pre><code class="language-py">        self.custom_text_proj = nn.Linear(self.model.config.hidden_size, self.dim)
</code></pre>
<p>hence these projections are fully connected layer calculations.</p>
<p>🦊 It returns these after L2 normalization and checks if there are <code>pixel_values</code> to consider.</p>
<p>🐢 In this case, the output from <code>ColQwen2</code> is this single tensor.</p>
<p>🐇 Ok. Then it’s actually simpler than wrapping up the whole <code>Qwen2VLForConditionalGeneration</code>.</p>
<p>🐢 It looks so, yes. But the example for <code>ColQwen2</code> also has a post-processing step:</p>
<pre><code class="language-py">scores = processor.score_multi_vector(query_embeddings, image_embeddings)
</code></pre>
<p>🐇 I think we can leave that post-processing for the time being. We only need multi-vector embeddings for FastEmbed.</p>
<p>🐢 Maybe it’s convertible to a <code>forward</code> method that we can use with ONNX.</p>
<p>🐇 Let’s see how this scoring works, then.</p>
<p>🐢 <a href="https://github.com/illuin-tech/colpali/blob/main/colpali_engine/utils/processing_utils.py#L68"><code>score_multi_vector</code></a> receives two tensors or tensor lists and compares query vectors with passage vectors. It’s a rather straightforward implementation that requires Torch, but not Transformers.</p>
<p>🐇 In theory, we can also convert this to an ONNX model.</p>
<p>🦊 We can also write a custom model for this.</p>
<p>🐢 It has two <code>for</code> loops for comparisons. Can we convert all of these?</p>
<p>🦊 The loop indices can be seen as <code>dynamic_axes</code>, and it’s possible to convert the whole thing as a Torch model, then use <code>torch.onnx.export</code> just as we do for the model itself.</p>
<p>🐢 I see, but I don’t think that’s what we must do now.</p>
<p>🐇 Yes, let’s skip that for the time being and convert the model itself. We’ll have multivectors for patches and queries at the end.</p>
<p>🐢 So, we’ll keep the processing part, send <code>BatchFeature</code> objects that are output from the processor, and send this to two models: one for images and one for text.</p>
<p>🐇 Yep, that’s the plan. In the end, we’ll have two ONNX models that require <code>BatchFeature</code>s.</p>
<p>🐢 Then, we’ll modify <code>past_key_values</code> in this argument to use <code>DynamicCache</code>.</p>
<p>🐇 Yes, that’s alright. We can start by moving <code>past_key_values_converter</code> to a method in <code>PatchedColQwen2</code>.</p>
<p>🐢 <code>past_key_values</code> wasn’t used much, so I completely removed it. I also began to use processor outputs as dummy input in the exporter. However, when using <code>BatchFeature</code>, we get:</p>
<blockquote>
<p>RuntimeError: Only tuples, lists and Variables are supported as JIT inputs/outputs. Dictionaries and strings are also accepted, but their usage is not recommended. Here, received an input of unsupported type: BatchFeature</p>
</blockquote>
<p>🐇 So, we can try Dynamo first, I think. If that doesn’t work, we can just collect items as tensors and build up a <code>BatchFeature</code> inside the patcher.</p>
<p>🐢 Let’s try <code>dynamo=True</code> for this first.</p>
<p>🦊 This time, it’s about <code>BatchFeature</code> again, but the error is different: <code>KeyError: 'Indexing with integers is not available when using Python based feature extractors'</code></p>
<p>🐇 In this case, we can just create <code>BatchFeature</code> inside the patcher.</p>
<h2 id="patchedcolqwen2">PatchedColQwen2</h2>
<pre><code class="language-python">
class PatchedColQwen2(ColQwen2):
    def forward(self, *args):
        (
            input_ids,
            inputs_embeds,
            attention_mask,
            position_ids,
            *past_key_values_args,
        ) = args
        # Convert past_key_values list to DynamicCache
        if len(past_key_values_args) == 0:
            past_key_values = None
        else:
            past_key_values = DynamicCache()
            for i in range(self.config.num_hidden_layers):
                key = past_key_values_args.pop(0)
                value = past_key_values_args.pop(0)
                past_key_values.update(key_states=key, value_states=value, layer_idx=i)

        breakpoint()
        o = super().forward(
            input_ids=input_ids,
            inputs_embeds=inputs_embeds,
            attention_mask=attention_mask,
            # position_ids=position_ids,
            past_key_values=past_key_values,
        )

        flattened_past_key_values_outputs = {
            "logits": o.logits,
        }
        output_past_key_values: DynamicCache = o.past_key_values
        for i, (key, value) in enumerate(
            zip(output_past_key_values.key_cache, output_past_key_values.value_cache)
        ):
            flattened_past_key_values_outputs[f"present.{i}.key"] = key
            flattened_past_key_values_outputs[f"present.{i}.value"] = value

        return flattened_past_key_values_outputs

</code></pre>]]></content:encoded>
    </item>
    <item>
      <title>devlog 25</title>
      <published>2025-04-24T02:57:26+00:00</published>
      <updated>2025-04-24T02:57:26+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Thu, 24 Apr 2025 02:57:26 +0000</pubDate>
      <link>https://emresahin.net/devlog-25/</link>
      <guid isPermaLink="true">https://emresahin.net/devlog-25/</guid>
      <description>🐢 I want to update xvc.py to the latest version. 🐇 It should only be needed to update the dependency versions in Cargo.toml , right? 🐢 Let’s start with that. 🐇 We have interface changes regarding aliases; let’s start using uv for building. 🦊 Added requirements to pyproject.toml by running: uv add...</description>
      <category>XVC</category>
      <category>Python</category>
      <category>Development</category>
      <category>XVC</category>
      <category>Python</category>
      <category>uv</category>
      <category>Package Management</category>
      <category>Requirements</category>
      <content:encoded><![CDATA[<p>🐢 I want to update <code>xvc.py</code> to the latest version.</p>
<p>🐇 It should only be needed to update the dependency versions in <code>Cargo.toml</code>, right?</p>
<p>🐢 Let’s start with that.</p>
<p>🐇 We have interface changes regarding aliases; let’s start using <code>uv</code> for building.</p>
<p>🦊 Added requirements to <code>pyproject.toml</code> by running:</p>
<pre><code class="language-bash">uv add -r requirements.txt
</code></pre>]]></content:encoded>
    </item>
    <item>
      <title>bits 15</title>
      <published>2025-03-27T09:52:06+00:00</published>
      <updated>2025-03-27T09:52:06+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Thu, 27 Mar 2025 09:52:06 +0000</pubDate>
      <link>https://emresahin.net/bits-15/</link>
      <guid isPermaLink="true">https://emresahin.net/bits-15/</guid>
      <description>How to use a Python virtual environment with Nushell: virtualenv .venv overlay use .venv/bin/activate.nu</description>
      <category>bits</category>
      <category>shell</category>
      <category>nushell</category>
      <category>Python</category>
      <category>venv</category>
      <category>virtualenv</category>
      <category>cli</category>
      <content:encoded><![CDATA[<p>How to use a Python virtual environment with Nushell:</p>
<pre><code class="language-nu">virtualenv .venv
overlay use .venv/bin/activate.nu
</code></pre>]]></content:encoded>
    </item>
    <item>
      <title>devlog 12</title>
      <published>2025-01-19T09:46:29+00:00</published>
      <updated>2025-01-19T09:46:29+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Sun, 19 Jan 2025 09:46:29 +0000</pubDate>
      <link>https://emresahin.net/devlog-12/</link>
      <guid isPermaLink="true">https://emresahin.net/devlog-12/</guid>
      <description>🐢 What are today’s plans? 🐇 I think we can start by improving xvc.py. I mean, releasing. Yesterday we finished our work with the Rust library. 🐢 Then we can start to look at the GUI, I believe. We can replace that 3-column view with a table and preview. It will be much easier that way. 🐇 Yep. Let...</description>
      <category>xvc</category>
      <category>xvc.py</category>
      <category>pypi</category>
      <category>maturin</category>
      <category>pytest</category>
      <category>xvc-test-helper</category>
      <category>codecov</category>
      <category>coverage</category>
      <category>GitHub Actions</category>
      <category>Python</category>
      <category>Rust</category>
      <category>PyO3</category>
      <content:encoded><![CDATA[<p>🐢 What are today’s plans?</p>
<p>🐇 I think we can start by improving xvc.py. I mean, releasing. Yesterday we finished our work with the Rust library.</p>
<p>🐢 Then we can start to look at the GUI, I believe. We can replace that 3-column view with a table and preview. It will be much easier that way.</p>
<p>🐇 Yep. Let’s finish and release the Python version first. Then we’ll go on to the GUI.</p>
<p>🐢 Let’s take a look at the PRs first.</p>
<pre><code>ghpl 
36	Bump pyo3 from 0.22.2 to 0.23.3	dependabot/cargo/pyo3-0.23.3	OPEN	2024-12-04T03:52:03Z

gh pr close 36
✓ Closed pull request iesahin/xvc.py#36 (Bump pyo3 from 0.22.2 to 0.23.3)
</code></pre>
<p>🐢 We have already upgraded to pyo3 0.23. No need for this. Let’s create a PR for the current branch.</p>
<pre><code>git push --set-upstream origin v0.6.13

ghpC --fill
https://github.com/iesahin/xvc.py/pull/37
</code></pre>
<p>🐇 We can also tag and push the tags.</p>
<p>🐢 I’d like to have some more coverage for certain parts of the code. Let’s add some tests.</p>
<p>🐇 It looks like we mainly lack the storage tests. They need configuration to add keys to GitHub, and we already skip some of these even in the Rust code.</p>
<p>🐢 Umm, I see. We also need a way to measure coverage. Could we do this with Codecov, I wonder?</p>
<p>🐇 I found an example here: https://github.com/codecov/example-python/blob/main/.github/workflows/ci.yml. It needs <code>coverage</code> and <code>pytest-cov</code> in the requirements.</p>
<p>🐢 Let’s try this then.</p>
<p>🐇 Added <code>coverage.yml</code> file. It’s simpler than the other GitHub action. Need to update the token now.</p>
<p>🐢 There is an issue installing the requirements.</p>
<p>🐇 I forgot to add <code>sudo</code> to <code>apt-get</code>. Will take care of it now.</p>
<p>🐢 Let’s check the run.</p>
<pre><code>ghrl
in_progress		Release v0.6.13	coverage	v0.6.13	pull_request	12556590550	2m31s	2024-12-31T06:59:58Z
in_progress		Release v0.6.13	publish-to-pypi	v0.6.13	pull_request	12556590548	2m31s	2024-12-31T06:59:58Z
...
</code></pre>
<p>🐇 It takes a while and we still didn’t add the <code>CODECOV_TOKEN</code>.</p>
<p>🐢 Let’s add it, and after that, we need to take a look at this <code>blink</code> configuration. It adds letters after the selection.</p>
<p>🐇 Added the secret and configured <code>xvc.py</code> for coverage. There is an error with the build, though. Maybe the command we should be using is <code>maturin develop</code> instead of <code>build</code> to make Xvc available for the environment.</p>
<p>🐢 Let’s update and try it then.</p>
<p>🐇 <code>maturin develop</code> requires a virtual environment.</p>
<p>🐢 I checked the options for <code>build</code>, and I think there is an option, but let’s search first.</p>
<p>🐇 I searched, but it looks like we can just pass an <code>--out</code> directory or install <code>xvc</code> from the <code>target/wheels/</code> directory. The second option requires less maintenance.</p>
<p>🐢 Okay. Let’s add a step to the action then.</p>
<p>🐇 Now, let’s wait for the run to finish with <code>gh run watch</code>.</p>
<p>🐢 It failed. Let’s take a look at the logs:</p>
<pre><code>ghrl
completed	failure	Release v0.6.13	coverage	v0.6.13	pull_request	12556939836	3m26s	2024-12-31T07:38:54Z

gh run view 12556939836 --log-failed
</code></pre>
<p>🐇 It looks like some of the tests are failing. Let’s run the tests locally.</p>
<p>🐢 We should run with the <code>--forked</code> option, and it looks like we have a test to update with the xvc file list.</p>
<p>🐇 Updated the test, and I noticed we forgot to supply the new <code>--show-directories</code> option in the Python interface.</p>
<p>🐢 Yep. Passing these options as command-line options in strings is not robust. It’s very easy to forget things. I think we should start using CLI structs directly, but it’s not time yet.</p>
<p>🐇 I agree. It’s one of the goals for building a GUI, actually.</p>
<p>🐢 The tests failed again.</p>
<pre><code>ghrl | rg failure
completed	failure	Release v0.6.13	coverage	v0.6.13	pull_request	12557031091	3m49s	2024-12-31T07:51:19Z

gh run view 12557031091 --log-failed
</code></pre>
<p>🐇 There is a Git error now. We need to add a Git user and email to the action.</p>
<p>🐢 Added those and watching the results again now.</p>
<p>🐇 Why do you think the publish action always works? It can only run with the main branch, I think. No need to run it with other pushes.</p>
<p>🐢 Yes, let’s configure it now.</p>
<pre><code>ghrl | rg failure
completed	failure	Release v0.6.13	coverage	v0.6.13	pull_request	12557083393	3m21s	2024-12-31T07:59:18Z

gh run view 12557083393 --log-failed
</code></pre>
<p>🐇 It looks like we also need <code>xvc-test-helper</code> in the path. Let’s <code>cargo install</code> it and add it to the path.</p>
<p>🐢 Added <code>.cargo/bin</code> to the path like:</p>
<pre><code class="language-yaml">- name: Add cargo bin to PATH
  run: echo "$HOME/.cargo/bin" &gt;&gt; $GITHUB_PATH
</code></pre>
<p>and installed the helper with <code>cargo install xvc-test-helper</code>.</p>
<p>🐇 By the way, GitHub Copilot is hallucinating about a method to update the path.</p>
<p>🐢 I searched and it may not be hallucinating. We can use the <code>::add-path::</code> command with <code>echo</code>, it looks like. This is new to me.</p>
<p>🐇 The tests failed again.</p>
<pre><code>ghrl | rg failure
completed	failure	Release v0.6.13	coverage	v0.6.13	pull_request	12557208723	3m34s	2024-12-31T08:12:09Z

gh run view 12557208723 --log-failed
</code></pre>
<p>🐢 <code>file().list()</code> had a mistake, and we need to install <code>rg</code> for the tests.</p>
<p>🐇 Watching the test run. In the meantime, maybe we can review…</p>
<p>🐢 Failed again.</p>
<pre><code>ghrl | rg failure
completed	failure	Release v0.6.13	coverage	v0.6.13	pull_request	12557294105	3m46s	2024-12-31T08:21:17Z

gh run view 12557294105 --log-failed
</code></pre>
<p>🐇 Added <code>db.commit()</code> to two places in the code. This should pass now.</p>
<p>🐢 Yeah!</p>
<pre><code>ghrl | head -n 1
completed	success	Release v0.6.13	coverage	v0.6.13	pull_request	12557509836	3m46s	2024-12-31T08:44:33Z

gh run view 12557509836

✓ v0.6.13 coverage iesahin/xvc.py#37 · 12557509836
Triggered via pull_request about 4 minutes ago

JOBS
✓ linux in 3m36s (ID 35010253915)

ANNOTATIONS
! ubuntu-latest pipelines will use ubuntu-24.04 soon. For more details, see https://github.com/actions/runner-images/issues/10636
linux: .github#1


For more information about the job, try: gh run view --job=35010253915
View this run on GitHub: https://github.com/iesahin/xvc.py/actions/runs/12557509836
</code></pre>
<p>🐇 I can’t see a coverage report on codecov.io, though.</p>
<p>🐢 It says no coverage report is generated. Let’s test to generate XML files locally.</p>
<p>🐇 The option in the docs seems incorrect. <code>--cov-branch</code> doesn’t produce anything.</p>
<p>🐢 Let’s wait for the run again.</p>
<p>🐇 Should we add a badge to the README?</p>
<p>🐢 It won’t show much, but yeah, let’s make it.</p>
<p>🐇 The results are in and it shows 100% coverage. This means it doesn’t actually test anything.</p>
<p>🐢 We need Rust coverage for this. Let’s search for it.</p>
<p>🐇 I found this: https://github.com/cjermain/rust-python-coverage. It runs <code>cargo llvm-cov</code> with the project and measures test coverage. But we don’t have any Rust tests.</p>
<p>🐢 It looks like we don’t need Rust tests. <code>cargo llvm-cov</code> can check coverage with the Python as well.</p>
<pre><code class="language-bash">$ cargo llvm-cov show-env --export-prefix
export RUSTFLAGS=" -C instrument-coverage --cfg coverage --cfg trybuild_no_target"
export LLVM_PROFILE_FILE="/home/.../rust-python-coverage/target/rust-python-coverage-%m.profraw"
export CARGO_INCREMENTAL="0"
export CARGO_LLVM_COV_TARGET_DIR="/home/.../rust-python-coverage/target"
</code></pre>
<hr>
<p>🐢 Let’s take a look at what remained for 0.6.13.</p>
<p>🐇 I think there is nothing left. Python must be published when we merged the PR.</p>
<p>🐢 Let’s take a look by searching xvc python.</p>
<p>🐇 The PyPI page is https://pypi.org/project/xvc/ and it still reports the version as 0.6.11. There must be something.</p>
<p>🐢 Now let’s take a look at</p>
<p>https://github.com/iesahin/xvc.py</p>
<p>🐇 The run seems to be OK though.</p>
<p>https://github.com/iesahin/xvc.py/actions/runs/12557780141/job/35010938462</p>
<p>🐢 Maybe the version in <code>pyproject.toml</code> is still 0.6.11 and we forgot to update it?</p>
<p>tmux new-window -c $HOME/github.com/iesahin/xvc.py/ nvim</p>
<p>🐇 There is no version string in <code>~/github.com/iesahin/xvc.py/pyproject.toml</code>. It’s <em>dynamic</em>.</p>
<p>🐢 Then, let’s try to publish from local now.</p>
<p>🐇 The <code>iex</code> username requires email verification.</p>
<p>🐢 It looks like I publish xvc through the <code>iesahin</code> account, not <code>iex</code>. Maybe I can add both accounts to the project.</p>
<p>🐇 There seem to be no errors on the PyPI site.</p>
<p>🐢 Updating the token. Let’s add the token to pass to run <code>maturin publish</code>.</p>
<p>🐇 We’re receiving invalid or non-existent authentication information. Upgraded <code>maturin</code> to see if it fixes the issue.</p>
<p>🐢 We can also double-check the key.</p>
<p>🐇 Uploaded successfully from local. Let’s check the job again.</p>
<p>🐢 The release job was skipped because we didn’t tag after the merge. https://github.com/iesahin/xvc.py/actions/runs/12557780141/job/35011303045 That looks like the reason.</p>
<p>🐇 I should be more careful which is run and which is skipped.</p>
<p>🐢 Maybe we can relax the condition. We do this rarely. Maybe republishing is alright?</p>
<p>🐇 Yep, removed that. The jobs are running now. Let’s watch them to see what happens when we publish some of the packages.</p>
<p>🐢 In the meantime, let’s experiment with searching commands file with <code>fzf-lua</code>.</p>
<p>🐇 It requires more experimentation, but we can start from https://github.com/ibhagwan/fzf-lua/wiki/Advanced#interactive-shell-command.</p>
<p>🐢 The xvc publish jobs failed, btw.</p>
<pre><code>ghrl
completed	failure	Remove if condition from release	publish-to-pypi	v0.6.13	push	12568863716	15m26s	2025-01-01T08:30:03Z

gh run view 12568863716 --log-failed
...
Release	Run actions/download-artifact@v4.1.7	        Please ensure that your artifact is not expired and the artifact was uploaded using a compatible version of toolkit/upload-artifact.
...
</code></pre>
<p>🐇 It was using an older version of <code>upload-artifact</code>.</p>
<p>🐢 Rerunning the job and it looks like it runs for both push to <code>main</code> and tag with <code>v0.6.13</code>. We can turn off push to <code>main</code>, I believe.</p>
<p>🐇 There was a missing <code>upload-artifact</code> again. Fixed and updated the tags.</p>
<p>ghrl
completed	failure	update upload-artifacts	publish-to-pypi	main	push	12569060915	14m39s	2025-01-01T08:57:22Z</p>
<p>ghrf 12569060915</p>
<p>🐢 There are conflicts with uploaded artifacts now. We may need to clean up the artifacts manually.</p>
<p>🐇 The conflicts were not about inter-workflow names. The names were conflicting because all files were named <code>wheel</code>. I added the platform and target to the names to avoid conflicts.</p>
<p>🐢 Let’s wait then. Maybe it will work this time. Could you search for a Lua console for Neovim?</p>
<p>🐇 Let’s try this one: return {
“yarospace/lua-console.nvim”,
lazy = true, keys = “`”, opts = {},
}</p>
<p>🐢 I couldn’t make it run but won’t spend much time ATM. How about the jobs?</p>
<pre><code>ghrl
completed	failure	update upload artifact names	publish-to-pypi	v0.6.13	push	12569298414	13m57s	2025-01-01T09:27:00Z

ghrf 12569298414
Release	Run actions/download-artifact@v4	2025-01-01T09:40:52.9180510Z ##[group]Run actions/download-artifact@v4
Release	Run actions/download-artifact@v4	2025-01-01T09:40:52.9182098Z with:
Release	Run actions/download-artifact@v4	2025-01-01T09:40:52.9182859Z   name: wheels
Release	Run actions/download-artifact@v4	2025-01-01T09:40:52.9183843Z   merge-multiple: false
Release	Run actions/download-artifact@v4	2025-01-01T09:40:52.9184817Z   repository: iesahin/xvc.py
Release	Run actions/download-artifact@v4	2025-01-01T09:40:52.9185820Z   run-id: 12569298414
Release	Run actions/download-artifact@v4	2025-01-01T09:40:52.9186903Z ##[endgroup]
Release	Run actions/download-artifact@v4	2025-01-01T09:40:53.1937617Z Downloading single artifact
Release	Run actions/download-artifact@v4	2025-01-01T09:40:53.4694769Z ##[error]Unable to download artifact(s): Artifact not found for name: wheels
Release	Run actions/download-artifact@v4	        Please ensure that your artifact is not expired and the artifact was uploaded using a compatible version of toolkit/upload-artifact.
Release	Run actions/download-artifact@v4	        For more information, visit the GitHub Artifacts FAQ: https://github.com/actions/toolkit/blob/main/packages/artifact/docs/faq.md
</code></pre>
<p>🐇 Now the download doesn’t work.</p>
<p>🐢 Update it to download using patterns.</p>
<p>🐇 Did so and let’s take a look at the jobs again.</p>
<pre><code>ghrl
completed	failure	added pattern to download	.github/workflows/publish.yml	main	push	12569456859	0s	2025-01-01T09:49:19Z

ghrv 12569456859

X main .github/workflows/publish.yml · 12569456859
Triggered via push about 3 minutes ago

X This run likely failed because of a workflow file issue.

For more information, see: https://github.com/iesahin/xvc.py/actions/runs/12569456859
</code></pre>
<p>🐢 The line with the pattern was reported as broken.</p>
<p>🐇  Removed it and recommitted.</p>
<pre><code>ghrl
✅ completed	success	remove pattern to download all	publish-to-pypi	v0.6.13	push	12569509435	14m41s	2025-01-01T09:58:49Z
</code></pre>
<p>🐢 And now this completes the <code>v0.6.13</code> release.</p>
<p>🐇 We’ll see how it will work next time.</p>
<p>🐢 Yep. Let’s move on to the GUI for now. Is that okay with you?</p>]]></content:encoded>
    </item>
    <item>
      <title>devlog 6</title>
      <published>2024-07-12T08:53:14+00:00</published>
      <updated>2024-07-12T08:53:14+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Fri, 12 Jul 2024 08:53:14 +0000</pubDate>
      <link>https://emresahin.net/devlog-6/</link>
      <guid isPermaLink="true">https://emresahin.net/devlog-6/</guid>
      <description>I have a habit of testing against the CLI’s help string output. It allows me to keep the documentation up to date and makes me aware of any undocumented options. When new features are added, the help text changes and the test fails, which prompts me to add those options to the documentation. I tr...</description>
      <category>devlog</category>
      <category>Software Development</category>
      <category>xvc</category>
      <category>Python</category>
      <category>pytest</category>
      <category>clap</category>
      <category>CLI</category>
      <category>Testing</category>
      <content:encoded><![CDATA[<p>I have a habit of testing against the CLI’s help string output. It allows me to
keep the documentation up to date and makes me aware of any undocumented options.
When new features are added, the help text changes and the test fails, which
prompts me to add those options to the documentation.</p>
<p>I tried the same approach when testing Python bindings with Pytest:</p>
<pre><code class="language-python">def test_pipeline_step_dependency(empty_xvc_repo):
    dep_help = empty_xvc_repo.pipeline().step().dependency(help=True)
    expected = """
Usage: xvc pipeline step dependency [OPTIONS] --step-name &lt;STEP_NAME&gt;

Options:
  -s, --step-name &lt;STEP_NAME&gt;
          Name of the step to add the dependency to
"""
    assert dep_help == expected
</code></pre>
<p>This doesn’t work because the help text is generated by <a href="https://docs.rs/clap/latest/clap/">clap</a> and skips the usual
thread-based output handler. All command output and errors in Xvc are returned
as strings from the command, except for the help text that’s generated by <a href="https://docs.rs/clap/latest/clap/">clap</a>
automatically.</p>
<p>There are probably workarounds for this, but I won’t pursue them further as I don’t
test the actual functionality in the Python bindings anyway.</p>]]></content:encoded>
    </item>
    <item>
      <title>devlog 5</title>
      <published>2024-07-08T20:35:24+00:00</published>
      <updated>2024-07-08T20:35:24+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Mon, 08 Jul 2024 20:35:24 +0000</pubDate>
      <link>https://emresahin.net/devlog-5/</link>
      <guid isPermaLink="true">https://emresahin.net/devlog-5/</guid>
      <description>While writing Xvc tests for Python, I hit an error caused by the ECS single-load protection. The single-loader allows only one instance of Xvc to be run in a single process. This is no problem for the shell, but it looks like it won’t be possible to use multiple Xvc instances in a single Python p...</description>
      <category>devlog</category>
      <category>Xvc</category>
      <category>devlog</category>
      <category>multiprocessing</category>
      <category>Jupyter</category>
      <category>python</category>
      <category>ecs</category>
      <category>testing</category>
      <content:encoded><![CDATA[<p>While writing Xvc tests for Python, I hit an error caused by the ECS single-load protection.</p>
<p>The single-loader allows only one instance of Xvc to be run in a single process. This is no problem for the shell, but it looks like it won’t be possible to use multiple Xvc instances in a single Python process.</p>
<p>It’s possible to overcome this with an elaborate multiprocessing setup in the wrapper, but I won’t bother with it for now.</p>]]></content:encoded>
    </item>
    <item>
      <title>devlog 4</title>
      <published>2024-06-05T10:04:54+00:00</published>
      <updated>2024-06-05T10:04:54+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Wed, 05 Jun 2024 10:04:54 +0000</pubDate>
      <link>https://emresahin.net/devlog-4/</link>
      <guid isPermaLink="true">https://emresahin.net/devlog-4/</guid>
      <description>Let’s start by looking at the debug output issue. We can start by replacing the eprintln! macros with println! , perhaps. I replaced the eprintln! s with println! , but it didn’t make any difference. Maybe we should remove those statements completely. I can’t really find the place that kills the ...</description>
      <category>devlog</category>
      <category>xvc</category>
      <category>xvc storage</category>
      <category>python</category>
      <category>clap</category>
      <category>debug</category>
      <category>rust</category>
      <category>s3</category>
      <category>cli</category>
      <content:encoded><![CDATA[<p>Let’s start by looking at the debug output issue. We can start by replacing the <code>eprintln!</code> macros with <code>println!</code>, perhaps.</p>
<p>I replaced the <code>eprintln!</code>s with <code>println!</code>, but it didn’t make any difference. Maybe we should remove those statements completely.</p>
<p>I can’t really find the place that kills the kernel. The last command to run is <code>xvc file list</code>:</p>
<pre><code class="language-python">print(xvc_test_data.file().list("test-data/dir-0002"))
</code></pre>
<p>and the output it produces is:</p>
<pre><code>[src/output.rs:144:13] &amp;output_str = "SS         131 2024-06-05 08:57:11 41e16be7          test-data/dir-0002/file-0003.bin\nSS         131 2024-06-05 08:57:11 27f0
efd0          test-data/dir-0002/file-0002.bin\nSS         131 2024-06-05 08:57:11 66de5084          test-data/dir-0002/file-0001.bin\nTotal #: 3 Workspace Size:
      393 Cached Size:        6006\n"
</code></pre>
<p><code>print</code> may be causing the crash, but the more likely cause is the command that comes after this:</p>
<pre><code>!ls -l test-data/dir-0001/
</code></pre>
<p>I replaced this with <code>lsd</code>, which also failed. Maybe it’s actually a Python crash or bug.</p>
<p>The way to understand is to create a notebook file with only that cell and try to run it.</p>
<p>The <code>ls</code> line runs fine with a new notebook. It even runs on the <code>README</code> file when run at the beginning. The line that makes the kernel crash is:</p>
<pre><code class="language-python">xvc_test_data.storage().new_s3(name="backup", bucket_name="xvc-test", region="eu-central-1", storage_prefix="xvc-storage")
</code></pre>
<p>We can start by removing the <code>new_s3</code> part.</p>
<p>The <code>storage()</code> method runs fine. It returns an <code>XvcStorage()</code> object, as it should.</p>
<p>When I run <code>storage().list()</code>, it takes a very long time. The bug is likely related to <code>storage()</code>.</p>
<p>It looks like the <code>storage</code> object was adding <code>file</code> instead of <code>storage</code> as a subcommand. I’ve fixed it now.</p>
<p>That was the bug. The <code>README</code> notebook now creates the S3 storage.</p>
<p>What was the reason behind this?</p>
<p>Parsing the CLI to the <code>XvcCLI</code> object was perhaps the culprit. Let’s look at it more clearly.</p>
<p>Let’s try <code>xvc file new s3</code> as a command to see how it behaves.</p>
<p>It says <em>unrecognized subcommand</em> for <code>new</code>.</p>
<p>This is how it should be, but I wonder why it doesn’t work for the <code>XvcCLI</code> parser.</p>
<p>Anyway, it’s already 13:00, so let’s stop here for today.</p>]]></content:encoded>
    </item>
    <item>
      <title>devlog 3</title>
      <published>2024-06-04T10:07:01+00:00</published>
      <updated>2024-06-04T10:07:01+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Tue, 04 Jun 2024 10:07:01 +0000</pubDate>
      <link>https://emresahin.net/devlog-3/</link>
      <guid isPermaLink="true">https://emresahin.net/devlog-3/</guid>
      <description>Let’s begin this session by removing debug statements from both the Xvc library and the Python bindings. Another issue is the restart script. The if in that script that restarts the server doesn’t work; it always restarts the notebook server. I removed some println! statements from xvc.py . There...</description>
      <category>XVC</category>
      <category>Development</category>
      <category>Rust</category>
      <category>Python</category>
      <category>clippy</category>
      <category>Jupyter</category>
      <category>bug-fix</category>
      <category>notebook</category>
      <content:encoded><![CDATA[<p>Let’s begin this session by removing debug statements from both the Xvc library and the Python bindings.</p>
<p>Another issue is the restart script. The <code>if</code> in that script that restarts the server doesn’t work; it always restarts the notebook server.</p>
<p>I removed some <code>println!</code> statements from <code>xvc.py</code>. There doesn’t seem to be anything in the library related to outputs.</p>
<p>Let’s search for how to check if a <code>jupyter-lab</code> command with the port 7979 runs in the background.</p>
<p>It looks like the bug is in the condition; it’s not <code>-s</code>, it’s <code>-z</code>.</p>
<p>Oops, yeah.</p>
<p>Let’s do a bit of tidying and check if the script is fixed.</p>
<p>I have a <code>clippy</code> warning with a <code>new</code> function that says these usually don’t take <code>self</code> as a parameter. This is for the <code>pipeline new</code> command, and it receives a <code>self</code> as an <code>XvcPipeline</code> object. It seems best to turn off the <code>clippy</code> warning for this.</p>
<p>I allowed two <code>clippy</code> warnings, and the script seems to work fine.</p>
<p>Let’s go on to copying the content from the Xvc <code>README</code> to the notebook.</p>
<p>There is an issue with the <code>run-after-commit</code> script. When Xvc commits the changes, the command we give is run again.</p>
<p>The issue is that <code>git</code> initializes the directory in the <code>test-data/</code> directory, while <code>xvc</code> works in the current directory. I think we can either <code>git init</code> in the current directory or <code>xvc init</code> in <code>test-data</code>.</p>
<p>We’re already deleting <code>.git</code> and <code>.xvc</code> directories in the <code>start-readme</code> script. I think it may be easier to update <code>git init</code> to just initialize in the current directory.</p>
<p>Yep, let’s do it that way.</p>
<p>I see there are still extra outputs from the commands. We need to deal with this first.</p>
<p>I removed them, but there are still pink outputs. These are from <code>dbg!</code> statements, it looks like, or we’re initializing the output thread incorrectly.</p>
<p>Fixed those as well. I wrote up the <code>xvc file list</code> command examples as well. Now we have issues with <code>xvc storage new s3</code> not running, and not even showing any debug output. We’ll deal with it in the next devlog, though.</p>]]></content:encoded>
    </item>
    <item>
      <title>Perl Error Building rust-openssl with Maturin, Ubuntu, and Manylinux</title>
      <published>2023-11-02T09:19:26+00:00</published>
      <updated>2023-11-02T09:19:26+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Thu, 02 Nov 2023 09:19:26 +0000</pubDate>
      <link>https://emresahin.net/perl-error-building-rust-openssl-with-maturin--ubuntu-and-manylinux/</link>
      <guid isPermaLink="true">https://emresahin.net/perl-error-building-rust-openssl-with-maturin--ubuntu-and-manylinux/</guid>
      <description>While building Python packages for Xvc with Maturin, I was receiving an error in GitHub Actions CI for Linux packages. Can't locate IPC/Cmd.pm in @INC (@INC contains: /home/runner/work/xvc.py/xvc.py/target/x86_64-unknown-linux-gnu/release/build/openssl-sys-844a96d66ae533b1/out/openssl-build/build...</description>
      <category>Xvc</category>
      <category>CI/CD</category>
      <category>Rust</category>
      <category>Xvc</category>
      <category>Maturin</category>
      <category>GitHub Actions</category>
      <category>Perl</category>
      <category>Python</category>
      <category>OpenSSL</category>
      <category>CentOS</category>
      <category>Debian</category>
      <content:encoded><![CDATA[<p>While building <a href="https://github.com/iesahin/xvc.py">Python packages for Xvc</a> with Maturin, I was receiving an error in GitHub Actions CI for Linux packages.</p>
<pre><code class="language-text">Can't locate IPC/Cmd.pm in @INC (@INC contains: /home/runner/work/xvc.py/xvc.py/target/x86_64-unknown-linux-gnu/release/build/openssl-sys-844a96d66ae533b1/out/openssl-build/build/src/util/perl /usr/local/lib64/perl5 /usr/local/share/perl5 /usr/lib64/perl5/vendor_perl /usr/share/perl5/vendor_perl /usr/lib64/perl5 /usr/share/perl5 . /home/runner/work/xvc.py/xvc.py/target/x86_64-unknown-linux-gnu/release/build/openssl-sys-844a96d66ae533b1/out/openssl-build/build/src/external/perl/Text-Template-1.56/lib)
</code></pre>
<p>The issue seemed to be a missing Perl package. Building <code>rust-openssl</code> now appears to require the <code>perl-core</code> package.</p>
<p>I added <code>apt-get update &amp;&amp; apt-get install perl-core</code> to the CI configuration, but that didn’t work.</p>
<p>However, as <a href="https://github.com/sfackler/rust-openssl/issues/2036#issuecomment-1724324145">this GitHub issue comment suggests</a>, Maturin with its <code>manylinux</code> support uses different kinds of Docker containers to build the packages. We must detect whether it is a CentOS or Debian-based container and install the missing packages accordingly.</p>
<p>The <em>Building wheels</em> step in the configuration should look similar to:</p>
<pre><code class="language-yaml">- name: Build wheels
  uses: PyO3/maturin-action@v1
  with:
    target: ${{ matrix.target }}
    manylinux: auto
    args: --release --out dist
    before-script-linux: |
      # If we're running on RHEL/CentOS, install needed packages.
      if command -v yum &amp;&gt; /dev/null; then
          yum update -y &amp;&amp; yum install -y perl-core openssl openssl-devel pkgconfig libatomic

          # If we're running on i686, we need to symlink libatomic
          # in order to build openssl with the -latomic flag.
          if [[ ! -d "/usr/lib64" ]]; then
              ln -s /usr/lib/libatomic.so.1 /usr/lib/libatomic.so
          fi
      else
          # If we're running on a Debian-based system.
          apt update -y &amp;&amp; apt-get install -y libssl-dev openssl pkg-config
      fi
</code></pre>
<p>This will run the specified script before executing the Maturin build command in the container, ensuring all missing packages are installed.</p>]]></content:encoded>
    </item>
    <item>
      <title>Object-Oriented Brain Damage</title>
      <published>2022-04-05T06:10:16+00:00</published>
      <updated>2022-04-05T06:10:16+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Tue, 05 Apr 2022 06:10:16 +0000</pubDate>
      <link>https://emresahin.net/Object-Oriented-Brain-Damage/</link>
      <guid isPermaLink="true">https://emresahin.net/Object-Oriented-Brain-Damage/</guid>
      <description>So, this is the post that I’ve been thinking about for some time. I’m still surprised that it feels like swearing in church ; it shouldn’t be this hard (and this rare) to criticize object-oriented programming . What do I mean by OOP? Mainly the classical style, where you define classes with membe...</description>
      <category>Software Architecture</category>
      <category>Programming Paradigms</category>
      <category>OOP</category>
      <category>Object-Oriented Programming</category>
      <category>ORM</category>
      <category>Principle of Locality</category>
      <category>Python</category>
      <category>Performance</category>
      <category>Software Engineering</category>
      <content:encoded><![CDATA[<p>So, this is the post that I’ve been thinking about for some time. I’m still surprised that
it feels like <em>swearing in church</em>; it shouldn’t be this hard (and this rare)
to criticize <em>object-oriented programming</em>.</p>
<p>What do I mean by OOP? Mainly the <em>classical</em> style, where you define classes
with members and methods and use them to model the solution you’re working on.
It was supposed that this way was superior to the <em>procedural style</em>, where you
write procedures that modify global state. I believe the rage
against <em>procedural programming</em> was because of this <em>global state</em>.
Somehow, in all domains, we have to maintain state, and having a global
state makes the reusability of procedures/functions almost impossible.</p>
<p>I can understand how the reaction against <em>global state</em> led to something like
Java’s “everything is a class” creed. In order to contain global state, you
need classes that contain parts of it and interact through methods. The idea is
simple: if we forbid global state and keep partial state within <em>structs</em> (or classes),
and mutate it with methods, we get reusable software. Once upon a time, I believed
in this, too.</p>
<p>In an ideal world where we could write software in Smalltalk instead of
C++, I’d probably not write this post. Actually, this post isn’t about C++ or
Java, either. They have their places; they solve real problems and
were probably necessary steps to arrive at Go or Rust. We now
have better ideas thanks to the “everything should be a class” worldview.</p>
<p>The problem, in my experience, is applying these <em>classical</em> classes to
interpreted languages. In C++, although it became like a Leviathan with
arms for many different paradigms, there is some care for the <em>cost of
abstraction.</em> If you know the tool, you can get away with classes. Java also
seems to try to make <em>abstractions</em> cost zero. So if you use these languages,
it’s a matter of modeling, habit, or domain suitability. I still think, over
the long term, OOP increases the maintenance burden, but like most ideas in
our profession, this is not rigorously tested.</p>
<p>However, when it comes to <em>multi-paradigm</em> interpreted languages like Python,
<em>objects</em> begin to hurt. The problem, in my opinion, is that <em>object-oriented</em> design
is in conflict with the Principle of Locality.</p>
<p>The Principle of Locality (PoL) is probably the most important
<em>empirical</em> idea in software design. It’s directly related to the Pareto
Principle, Zipf’s Law, and other well-known notions. Our CPUs, disks, search
engines, and content delivery networks depend on it to cache artifacts for
reuse. We know it works because a CPU with a larger L1 cache is faster; we add
more caches to CPUs and disks to increase their speed.</p>
<p>The conflict between the PoL and OOP arises when classes include data
that is not directly related to the <em>problem at hand.</em> The problem at hand, for
example, might be finding the maximum of one million numbers or performing transformations
on some variables. But if these are members of a class, they bring unusable data
to the <em>locality.</em> If the variable I’m transforming is a member of a class, when
I access it through the object, all other members—be they 3, 30, or 300—are
also referenced. Thus, looping over objects ends up polluting the <em>locality</em>
with all other members of the class.</p>
<p>In compiled languages, the advantage of iterators over plain loops is to
overcome this. However, in interpreted languages, no one writes specific
iterators for their member variables. This means when I have:</p>
<pre><code class="language-python">
for obj in my_objects:
    obj.do_something(a)

</code></pre>
<p>All the members of <code>obj</code> are now in the loop. This is against the PoL.</p>
<p>Another problem I see with OOP is its <em>alienation</em> from the basic
elements of software. When you begin to work with objects that do not directly
correspond to anything in computer/software architecture, it becomes more or
less a castle in the clouds. You have to tie this castle—i.e., the <em>class
hierarchy</em>—somehow to the ground. The ground is the CPU, memory, disk, cache, etc.
Compilers may do a good job of this, or they may not, but this
detachment from the basic elements of computing machinery is the cause of what
I call <em>Object-Oriented Brain Damage.</em></p>
<p>When you begin to believe that the <em>objects</em> in your program are <em>real</em>, you
try to express your problem using more objects. Classes are like kipple. They
proliferate all the time. You add classes, then you add classes to create
classes, then you add some base class to derive classes, then you try to fix
these problems with patterns, and so on.</p>
<p>But this whole enterprise doesn’t have anything to do with the <em>real tools</em> we
have. Our tools are processors, memory, disks, screens, printers, etc. When
we detach the problem from these basics, it doesn’t become <em>more solvable.</em> We
only create an ideal version of our understanding that requires <em>additional</em>
attention to teach others, to document, etc.</p>
<p>It’s true that we need abstractions over the tools—we cannot just <em>read
bytes from disk</em>, <em>process in CPU</em>, and <em>print to screen</em>. These all
must be abstracted. But in my experience, OOP is not a good way to abstract
these tools. Instead, it tries to abstract the problem at hand in an arbitrary
way that is <em>supposed</em> to solve the problem. Yet this solution itself becomes a
problem that must be fit onto the <em>ground.</em></p>
<p>Databases and ORMs are good examples of this. Databases and
Entity-Relationship theory are well-understood abstractions. We know how to use
them across multiple CPUs, multiple disks, and multiple machines across multiple
continents. ORMs are not like this; they are <em>supposed</em> to correspond to
databases and provide that <em>cozy object-oriented feeling</em>. However, any system
that depends on ORMs learns that these are not <em>identical</em> to databases. They
don’t have the same capabilities and performance, and over the long run,
depending on an ORM causes more problems than just writing SQL
queries. Because an ORM does not consider the tools we have and their
limitations, it tries to fit an <em>idealistic</em> world view onto databases. When it
doesn’t scale, we think this <em>performance degradation</em> is <em>natural.</em></p>
<p>No, it’s not natural. When your models load whole rows from billions of
records just to access a single field for a calculation, you deplete the cache
space quickly, and it doesn’t scale. OOP might just be an <em>educational</em> tool,
but even in this regard, I believe it causes most of the brain damage we see in
enterprise software.</p>]]></content:encoded>
    </item>
    <item>
      <title>telegram-send</title>
      <published>2020-05-12T18:47:24+00:00</published>
      <updated>2020-05-12T21:47:34+03:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Tue, 12 May 2020 18:47:24 +0000</pubDate>
      <link>https://emresahin.net/telegram-send-6932/</link>
      <guid isPermaLink="true">https://emresahin.net/telegram-send-6932/</guid>
      <description>A little Python CLI app for Telegram messages</description>
      <category>CLI</category>
      <category>Telegram</category>
      <category>Tools</category>
      <category>Python</category>
      <category>Automation</category>
      <category>Messaging</category>
      <category>Bot</category>
      <content:encoded><![CDATA[<p>There is a small Python command-line program called <code>telegram-send</code> that allows you to send messages to your
Telegram account.</p>
<p>First, you need to register a new bot with <a href="https://t.me/BotFather">@BotFather</a> and get an API token. Then,
run <code>pip3 install --user telegram-send</code> and prepare a config file at <code>~/.config/telegram-send.conf</code>:</p>
<pre><code class="language-ini">[telegram]
token = &lt;TOKEN_YOU_GET_FROM_BOT_FATHER&gt;
chat_id = &lt;CHAT_OR_USER_ID&gt;
</code></pre>
<p>You’ll need to start a conversation with the bot and find your user ID (which is identical to the chat
ID for the conversation you start with the bot).</p>
<p>After that, you can send yourself messages from the CLI like this:</p>
<pre><code class="language-bash">telegram-send "Hello Telegram."
</code></pre>
<p>You can also send Markdown-formatted messages, audio, stickers, and more using various command-line options:</p>
<pre><code class="language-text">usage: telegram-send [-h] [--format {text,markdown,html}] [--stdin] [--pre]
                     [--disable-web-page-preview] [--silent] [-c]
                     [--configure-channel] [--configure-group]
                     [-f FILE [FILE ...]] [-i IMAGE [IMAGE ...]]
                     [-s STICKER [STICKER ...]]
                     [--animation ANIMATION [ANIMATION ...]]
                     [--video VIDEO [VIDEO ...]] [--audio AUDIO [AUDIO ...]]
                     [-l LOCATION [LOCATION ...]]
                     [--caption CAPTION [CAPTION ...]] [--config CONF] [-g]
                     [--file-manager] [--clean] [--timeout TIMEOUT]
                     [--version]
                     [message [message ...]]
</code></pre>]]></content:encoded>
    </item>
    <item>
      <title>Using SSH Private Keys in Dockerfile aimed for Google Cloud Run</title>
      <published>2020-05-12T18:46:51+00:00</published>
      <updated>2020-05-12T18:46:51+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Tue, 12 May 2020 18:46:51 +0000</pubDate>
      <link>https://emresahin.net/using-ssh-private-keys-in-dockerfile-aimed-for-google-cloud-run--24390/</link>
      <guid isPermaLink="true">https://emresahin.net/using-ssh-private-keys-in-dockerfile-aimed-for-google-cloud-run--24390/</guid>
      <description>I had a one-user software application that I had wanted to deploy to Google Cloud Run for some time. It was on Python 3.5, and when I updated the system it lives on, the virtual environment stopped working. It also depended on lxml-3.7 , and that particular version didn’t compile on my new stable...</description>
      <category>DevOps</category>
      <category>Docker</category>
      <category>Cloud Computing</category>
      <category>Docker</category>
      <category>Google Cloud Run</category>
      <category>SSH</category>
      <category>Security</category>
      <category>Python</category>
      <category>Bitbucket</category>
      <content:encoded><![CDATA[<p>I had a one-user software application that I had wanted to deploy to Google Cloud Run for some time. It was on
Python 3.5, and when I updated the system it lives on, the virtual environment stopped working. It
also depended on <code>lxml-3.7</code>, and that particular version didn’t compile on my new stable Debian
installation.</p>
<p>This motivated me to learn Docker and gcloud rather quickly.</p>
<p>I was able to create a new Docker container in a short time. However, as I don’t want to share my Git
SSH key publicly, I needed a way to put an SSH private key (<code>~/.ssh/id_rsa</code>) into this new container
securely, without leaving any trace.</p>
<p>I tried a few things, but in the end, I decided to do something like:</p>
<pre><code class="language-Dockerfile">FROM python:3.5-stretch as intermediate

# add credentials on build
RUN mkdir /root/.ssh/
# To use docker --build-arg, you can uncomment the following two lines and comment out the COPY line below.
# ARG SSH_PRIVATE_KEY
# RUN echo "${SSH_PRIVATE_KEY}" &gt; /root/.ssh/id_rsa
COPY application_sshkey /root/.ssh/id_rsa
RUN chmod 0600 /root/.ssh/id_rsa

# make sure your domain is accepted
RUN touch /root/.ssh/known_hosts
RUN ssh-keyscan bitbucket.org &gt;&gt; /root/.ssh/known_hosts

RUN git clone git@bitbucket.org:username/application /root/application

FROM python:3.5-stretch
COPY --from=intermediate /root/application /root/application

RUN pip3 install -r /root/application/requirements.txt

EXPOSE 9090/tcp

WORKDIR /root/application/

CMD python3 manage.py runserver 0.0.0.0:9090
</code></pre>
<p>Here, <code>application_sshkey</code> is a file I created using <code>ssh-keygen</code> and granted read-only access to on
Bitbucket.</p>
<p>As you can see, the Dockerfile has two <code>FROM</code> statements. It creates a container to clone the
application repository. Then it starts again with a new container and copies the repository to this new
container. This way, it is not possible to peek into the private key using the <code>docker history</code> command.</p>
<p>By the way, I included two methods in the Dockerfile because <code>gcloud builds</code> does not accept a
<code>--build-arg</code> parameter similar to <code>docker build</code>. I’m sure there are other workarounds for passing
secrets to <code>gcloud</code> builds, but instead of digging for them, I found a solution that works for both
Docker and Google Cloud Run.</p>]]></content:encoded>
    </item>
    <item>
      <title>TIL April 28</title>
      <published>2020-04-28T21:40:02+00:00</published>
      <updated>2020-04-28T21:40:02+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Tue, 28 Apr 2020 21:40:02 +0000</pubDate>
      <link>https://emresahin.net/til-april-28--4248/</link>
      <guid isPermaLink="true">https://emresahin.net/til-april-28--4248/</guid>
      <description>In yesterday’s post , I presented a Python script to convert Pelican preamble files to YAML for Hugo. For some UTF-8 files, there is a BOM marker at the beginning of the file. The script (as a true quick and dirty solution) doesn’t check for the presence of such a marker and cannot detect the Tit...</description>
      <category>TIL</category>
      <category>Python</category>
      <category>BOM</category>
      <category>Encoding</category>
      <category>UTF-8</category>
      <category>bvi</category>
      <category>Hex-editor</category>
      <content:encoded><![CDATA[<p>In <a href="https://emresahin.net/post/til-27-april-12091/">yesterday’s post</a>, I presented a Python script to convert Pelican preamble files to YAML for Hugo.</p>
<p>For some UTF-8 files, there is a BOM marker at the beginning of the file. The script (as a true quick and dirty solution) doesn’t check for the presence of such a marker and cannot detect the <code>Title</code> element if it exists.</p>
<p>I added an <code>fm = fm.strip('\ufeff')</code> line to clear the BOM marker from a line if it exists.</p>
<hr>
<p>There is an editor called <code>bvi</code> to edit binary files in Hex format, similar to the <code>vi</code> editor.</p>]]></content:encoded>
    </item>
    <item>
      <title>Numpy ValueError while using dlib's face detector</title>
      <published>2018-10-24T21:41:18+00:00</published>
      <updated>2018-10-24T21:41:18+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Wed, 24 Oct 2018 21:41:18 +0000</pubDate>
      <link>https://emresahin.net/value-error-dlib-14346-27722/</link>
      <guid isPermaLink="true">https://emresahin.net/value-error-dlib-14346-27722/</guid>
      <description>For two days, I was trying to find a bug in my code because an assertion in the code that uses numpy.max was throwing an error: ValueError: zero-size array to reduction operation maximum which has no identity , which didn’t seem reasonable. I’m building a face recognizer with dlib ’s frontal face...</description>
      <category>Machine Learning</category>
      <category>Python</category>
      <category>Debugging</category>
      <category>dlib</category>
      <category>NumPy</category>
      <category>Face Detection</category>
      <category>Python</category>
      <category>Computer Vision</category>
      <category>ValueError</category>
      <content:encoded><![CDATA[<p>For two days, I was trying to find a bug in my code because an assertion in the
code that uses <code>numpy.max</code> was throwing an error: <code>ValueError: zero-size array to reduction operation maximum which has no identity</code>, which didn’t seem
reasonable.</p>
<p>I’m building a face recognizer with <a href="https://github.com/davisking/dlib/">dlib</a>’s
frontal face detector, and today, I noticed that some of the results return
<em>negative</em> coordinates in face detection. This means the detected face is
partial, although it’s a bit of a stretch to use negative coordinates for this.</p>
<p>My code wasn’t checking for negative coordinates and was building the NumPy
array incorrectly. However, after reading <a href="https://github.com/davisking/dlib/issues/767">the
issue</a>, although I’m still not convinced
that it’s a good approach, I added a few <code>if</code> statements and the problem was solved.</p>]]></content:encoded>
    </item>
    <item>
      <title>My Emacs Packages</title>
      <published>2014-05-13T14:00:00+00:00</published>
      <updated>2014-05-13T14:00:00+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Tue, 13 May 2014 14:00:00 +0000</pubDate>
      <link>https://emresahin.net/my-emacs-packages/</link>
      <guid isPermaLink="true">https://emresahin.net/my-emacs-packages/</guid>
      <description>I’ve been using Emacs for about 7 or 8 years now—maybe a bit less, maybe more. I tried to quit several times for other editors and different workflows, and every time I returned with more enthusiasm. It’s hard to tell for those who use their editors with mouse clicks on pretty icons, but once you...</description>
      <category>Software</category>
      <category>Emacs</category>
      <category>emacs</category>
      <category>org-mode</category>
      <category>software</category>
      <category>python</category>
      <category>latex</category>
      <category>w3m</category>
      <content:encoded><![CDATA[<p>I’ve been using Emacs for about 7 or 8 years now—maybe a bit less,
maybe more. I tried to quit several times for other editors and different
workflows, and every time I returned with more enthusiasm.</p>
<p>It’s hard to tell for those who <em>use</em> their editors with mouse clicks
on pretty icons, but once you catch this virus called <em>doing everything
from the keyboard</em>, it becomes attached to your digital (from <em>digitus</em>,
finger) psyche, making it impossible to leave behind. I even purchased an
Android keyboard for my phablet just to <code>ssh</code> to my home and write with
Emacs. Consider that!</p>
<p>Today I stumbled upon several posts regarding favorite Emacs packages
and wanted to list mine.</p>
<h1 id="org-mode">org-mode</h1>
<p>I’m not writing this in <code>org-mode</code> because I observed that I can
detail, micro-revise, and lose focus while writing because of its
extensive functionality. Instead, I’m using Zsh’s <code>vared</code> to enter lines to
an org-file when I write first drafts. But editing, keeping, following,
creating plots, literate programming—doing everything that can be done
with text—is possible in <code>org-mode</code>.</p>
<p>I have <code>F11</code> bound to <code>org-capture</code> and <code>F12</code> to <code>org-agenda-list</code>.
<code>F9 F9</code> stores links wherever I like, and I can use <code>C-c C-l</code> in org
buffers to paste them.</p>
<h1 id="w3m">w3m</h1>
<p>When I want to search Google but don’t like the idea of running a huge
browser session just to look up an answer on StackOverflow, I use this.</p>
<h1 id="undo-tree">undo-tree</h1>
<p>It shows the versions created by undo/redo of a buffer in a tree format.
Emacs’s default undo may be cumbersome at times, but this package is
terrific.</p>
<h1 id="wc-mode">wc-mode</h1>
<p>To count words in a file. When I forget this package, I use
<code>C-x h M-| wc</code> as a substitute.</p>
<h1 id="auctex">auctex</h1>
<p>Once upon a time, this mode led me to migrate from LyX to LaTeX itself.</p>
<h1 id="ess">ESS</h1>
<p>Emacs Speaks Statistics is an R frontend. I’m not using it daily, but
it’s much better than the R command-line client.</p>
<h1 id="uniquify">Uniquify</h1>
<p>Makes buffer names unique. If you have files with identical names, this
is much better than having names like <code>myfile.txt&lt;1&gt;</code>, <code>myfile.txt&lt;2&gt;</code>,
etc.</p>
<h1 id="recentf-mode">recentf-mode</h1>
<p>A list of recent files.</p>
<h1 id="smex">smex</h1>
<p><code>ido</code> for <code>M-x</code>. Standard <code>M-x</code> selects commands by their prefix; with
<code>smex</code>, you can filter them by any substring. This makes a difference for
those with poor memory.</p>
<h1 id="ahg">ahg</h1>
<p>I use Mercurial more than Git, and <code>ahg-mode</code> seems like a good
alternative to standard <code>vc-</code> commands.</p>
<h1 id="projectile">projectile</h1>
<p>It considers directories under version control as projects and defines
functions like <code>find-file</code>, <code>multi-occur</code>, and <code>recentf</code> for them. It’s a
very simple and non-intrusive way to have project management.</p>
<h1 id="elpy">elpy</h1>
<p>For Python programming, <code>elpy</code> provides completion and much more
functionality.</p>
<h1 id="ack-and-a-half">ack-and-a-half</h1>
<p>An <code>ack</code> mode, a hybrid version of the other two.</p>
<h1 id="xgtags">xgtags</h1>
<p>A GNU Global mode for C++ code tagging.</p>]]></content:encoded>
    </item>
    <item>
      <title>Patch Histogram Feature</title>
      <published>2014-04-10T14:00:00+00:00</published>
      <updated>2014-04-10T14:00:00+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Thu, 10 Apr 2014 14:00:00 +0000</pubDate>
      <link>https://emresahin.net/patch-histogram/</link>
      <guid isPermaLink="true">https://emresahin.net/patch-histogram/</guid>
      <description>This post introduces a new feature for binary blobs, such as connected components in text. The feature is called patch histogram , and it represents the histogram of 3x3 patches of black and white pixels. We collect all 3x3 patches and count their frequencies. A 3x3 patch for a binary image conta...</description>
      <category>Computer Vision</category>
      <category>Image Processing</category>
      <category>Histogram</category>
      <category>Python</category>
      <category>Feature Extraction</category>
      <content:encoded><![CDATA[<p>This post introduces a new feature for binary blobs, such as connected components in text.</p>
<p>The feature is called <em>patch histogram</em>, and it represents the histogram of 3x3 patches of black and white pixels. We collect all 3x3 patches and count their frequencies.</p>
<p>A 3x3 patch for a binary image contains $2^9 = 512$ different combinations. For each of these combinations, we assign a unique ID. I wrote the implementation in Python; here is a lookup table that converts all possible 3x3 patches to their IDs.</p>
<pre><code class="language-python">patch_histogram_dict = {}
for f in range(0, 512):
    i22 = f &amp; 1
    i21 = (f &gt;&gt; 1) &amp; 1
    i20 = (f &gt;&gt; 2) &amp; 1
    i12 = (f &gt;&gt; 3) &amp; 1
    i11 = (f &gt;&gt; 4) &amp; 1
    i10 = (f &gt;&gt; 5) &amp; 1
    i02 = (f &gt;&gt; 6) &amp; 1
    i01 = (f &gt;&gt; 7) &amp; 1
    i00 = (f &gt;&gt; 8) &amp; 1
    patch_histogram_dict[ma(i00, i01, i02, i10, i11, i12, i20, i21, i22)] = f
</code></pre>
<p>I was looking for a <em>skeleton feature</em> that can be applied to components after a medial axis transform. However, in this version, we will use binarized connected component images directly and explore the skeletal version later.</p>
<p>Above, we used a function <code>ma</code> to convert a set of numbers to a tuple of tuples. It’s as simple as:</p>
<pre><code class="language-python">def ma(i00, i01, i02, i10, i11, i12, i20, i21, i22):
    return ((i00, i01, i02),
            (i10, i11, i12),
            (i20, i21, i22))
</code></pre>
<p>The first step in feature generation is marking each pixel of the image with its ID. Then, a simple <code>np.histogram</code> call generates the histogram.</p>
<pre><code class="language-python">for i in range(1, rows - 1):
    for j in range(1, cols - 1):
        s = framed[(i-1):(i+2), (j-1):(j+2)]
        marks[i-1, j-1] = patch_dict[ma(s[0, 0], s[0, 1], s[0, 2],
                                        s[1, 0], s[1, 1], s[1, 2],
                                        s[2, 0], s[2, 1], s[2, 2])]
</code></pre>
<p><code>framed</code> is a copy of the original image with a 1-pixel frame, so that boundary pixels are also counted without much boundary checking. <code>patch_dict</code> is a parameter that the function receives. Its default value is the dictionary that we created above, but any kind of patch dictionary can be used.</p>
<p>The feature is generated using a call to <code>numpy.histogram</code> as follows:</p>
<pre><code class="language-python">histogram = numpy.histogram(marked_points,
                            hist_range,
                            density=True,
                            bins=bins)
</code></pre>
<p><code>marked_points</code> are the results of the pixel counting loop above. <code>hist_range</code> is the minimum and maximum value for the patch dictionary. The <code>density</code> parameter is set to <code>True</code> so that the histogram is normalized to 1, making the feature size-invariant. <code>bins</code> is a parameter we can set freely, but by default, it’s equal to the range of elements.</p>
<p>Comparisons can be performed using standard histogram comparison methods, such as EMD (Earth Mover’s Distance) or Chi-Square distance metrics.</p>]]></content:encoded>
    </item>
    <item>
      <title>This Site's RSS Generator</title>
      <published>2013-04-22T21:13:06+00:00</published>
      <updated>2013-04-22T21:13:06+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Mon, 22 Apr 2013 21:13:06 +0000</pubDate>
      <link>https://emresahin.net/this-sites-rss-generator/</link>
      <guid isPermaLink="true">https://emresahin.net/this-sites-rss-generator/</guid>
      <description>This is an ancient post from 2013. I’m not using any of these now. Previously with Pandoc , I was using a simple setup to create RSS feeds. Markdown files were converted to plain, headerless HTML, and they were collected together to build an XML file. The obvious drawback is that all HTML files s...</description>
      <category>Software Development</category>
      <category>Python</category>
      <category>RSS</category>
      <category>automation</category>
      <category>static site generator</category>
      <category>web development</category>
      <content:encoded><![CDATA[<p><strong>This is an ancient post from 2013. I’m not using any of these now.</strong></p>
<p>Previously with <a href="http://johnmacfarlane.net/pandoc/">Pandoc</a>, I was using a simple setup to create RSS feeds. <code>Markdown</code> files were converted to plain, headerless HTML, and they were collected together to build an XML file. The obvious drawback is that all HTML files should be generated by Pandoc; anything that doesn’t fit that route does not appear in the feeds.</p>
<p>However, when I began to use <a href="http://orgmode.org">Org Mode</a> for data analysis and other tasks, I stopped using Pandoc. Org Mode has extensive facilities for exporting into HTML and other document formats, so I would not mess with Pandoc for this.</p>
<p>I thought RSS could be produced by parsing HTML files after they are produced. This requires parsing the HTML file, but it’s simple, and there are parsers for all programming languages out there. My previous RSS generator was in Python, and I decided to modify it to fit my needs. I think producing RSS for a static HTML site is a common need, and I tried to solve this problem as simply as possible.</p>
<p>Let’s begin with the ubiquitous shebang line. This tells the system that the script is in Python.</p>
<pre><code class="language-python">#!/usr/bin/env python
</code></pre>
<p>The following are the imports for this script. Apart from <a href="http://www.dalkescientific.com/Python/PyRSS2Gen.html">PyRSS2Gen</a>, all modules are present in Python 2.7.</p>
<pre><code class="language-python">import argparse
import codecs
import os
import datetime
from HTMLParser import HTMLParser
import PyRSS2Gen as rssgen
import operator as op
import re
import subprocess as proc
</code></pre>
<p>I use <a href="https://mercurial.selenic.com">Mercurial</a> to track the site’s files. I once thought about using the Mercurial public API to check the status of files, but it proved to be overkill because only the modification time of files is necessary, and retrieving them using a standard command-line call is much simpler. Hence, I removed the following imports for the time being.</p>
<pre><code class="language-python"># from mercurial import commands as cmd
# from mercurial import hg
# from mercurial import ui as hgui
</code></pre>
<p>The following function returns a valid HTML tag string, given the tag and its attributes in a list. <code>HTMLParser</code> sends the tags in a list form, and I use this function to reconvert them to usual HTML tags.</p>
<pre><code class="language-python">def make_tag(tag, attrs):
    content_list = [ tag ]
    content_list += [ "%s=\"%s\"" % (k, v) for (k, v) in attrs]
    return "&lt;" + " ".join(content_list) + "&gt;"
</code></pre>
<p><code>TitleBodyExtractor</code> is an <code>HTMLParser</code> subclass. It collects the body of a page in a string and also keeps the title. These two are the only requirements. It might be possible to parse meta tags to get publish date and author information as well, but I prefer to keep simple things simple.</p>
<pre><code class="language-python">class TitleBodyExtractor(HTMLParser):

    def __init__(self):
        HTMLParser.__init__(self)
        self.in_body = False
        self.in_title = False
        self.body = ""
        self.title = ""


    def handle_data(self, data):
        if self.in_body:
            self.body += data
        if self.in_title:
            self.title += data

    def handle_starttag(self, tag, attrs):
        if self.in_body:
            self.body += make_tag(tag, attrs)

        if tag == "body":
            self.in_body = True
        if tag == "title":
            self.in_title = True

    def handle_endtag(self, tag):
        if tag == "body":
            self.in_body = False

        if tag == "title":
            self.in_title = False

        if self.in_body:
            self.body += "&lt;/%s&gt;" % (tag)
</code></pre>
<p>Getting contents of a file in <em>UTF-8</em> encoding is a common task. The following two functions retrieve and store the contents in UTF-8 using the <code>codecs</code> module.</p>
<pre><code class="language-python">def get_content(filename):
    f = codecs.open(filename, "r", "utf-8")
    cont = f.read()
    f.close()
    return cont

def write_content(filename, content):
    f = codecs.open(filename, "w", "utf-8")
    f.write(content)
    f.close()
</code></pre>
<p>Mercurial allows running commands for a repository outside of that repository with the <code>-R</code> command-line switch. However, it requires the exact path of the repository and does not accept a child path. The following function finds the repository path of a file by recursively checking whether parent paths contain an <code>.hg/</code> directory.</p>
<pre><code class="language-python">def get_repo_path(dir):
    if dir == "/" or dir == "":
        return ""
    if os.path.exists(os.path.join(dir, ".hg")):
        return dir
    else:
        return get_repo_path(os.path.dirname(dir))
</code></pre>
<p>The <code>FileObject</code> class keeps the required data of an HTML file. It stores the path, modification time, body, and title.</p>
<pre><code class="language-python">class FileObject:
    def __init__(self, path, mtime):
        self.path = path
        self.mtime = int(mtime)
        self._body = ""
        self._title = ""

    def parse(self):
        content = get_content(self.path)
        tbe = TitleBodyExtractor()
        tbe.feed(content)
        self._body = tbe.body
        self._title = tbe.title

    def body(self):
        if self._body == "":
            self.parse()
        return self._body

    def title(self):
        if self._title == "":
            self.parse()
        return self._title

    def __str__(self):
        return str(self.path) + " " + str(self.mtime)
</code></pre>
<p>The modification time of a file should be retrieved from the Mercurial repository. The following function calls <code>hg log</code> with a specific template, then parses the date to get the last commit time of a file. If the file is not registered to a repository, it simply returns the filesystem modification time.</p>
<pre><code class="language-python">def get_mtime(full_path):
    if os.path.exists(full_path):
        repo_path = get_repo_path(full_path)
        if repo_path != "":
            logcmd = "/usr/bin/hg log -R %s --template='{date|hgdate}' -l 1 %s " % (repo_path, full_path)
            # print logcmd

            proc_res = proc.check_output(logcmd, shell=True).split()
            if len(proc_res) &gt; 0:
                filetime = int(proc_res[0])
            else:
                filetime = os.path.getmtime(full_path)
        else:
            filetime = os.path.getmtime(full_path)
        return filetime
    else:
        return 0
</code></pre>
<p>We need a list of files as <code>FileObject</code> objects, given the directory name and extension. The function also takes a repository path and excludes filenames that match a given regex.</p>
<pre><code class="language-python">
    def file_list(dirname, extension, repo_path, exclude_regex=None):
        results = []
        for root, dirs, files in os.walk(dirname):
            # print "Dirs:", dirs
            for d in dirs:
                if exclude_regex == None or (not re.match(exclude_regex, d)):
                    results += file_list(os.path.join(root, d), extension, repo_path)
                else:
                    print "Skipping", d
            # print "Files:", files
            for f in files:
                if (exclude_regex == None or (not re.match(exclude_regex, f))) and f.endswith(extension):
                    fullname = os.path.join(root, f)
                    filetime = get_mtime(fullname)
                    results.append(FileObject(fullname, filetime))
        return results
</code></pre>
<p>Given a local file in the site, we need to create a link that shows the URL of that file relative to the site’s URL. The following function finds the relative path with respect to an input directory and returns the complete URL by concatenating it to the site’s URL.</p>
<pre><code class="language-python">
    def make_link(site_url, input_dir, file_path):
        rel_path = os.path.relpath(file_path, input_dir)
        return site_url + rel_path
</code></pre>
<p>Given a <code>FileObject</code> that points to an HTML file, we need a function that builds an RSS item from it. It obtains the URL of the file and fills the rest using the attributes of the <code>FileObject</code>.</p>
<pre><code class="language-python">
    def get_rss_item(file_object, input_dir, site_url):
        the_link = make_link(site_url, input_dir, file_object.path)
        item = rssgen.RSSItem(title=file_object.title(),
                              link=the_link,
                              description=file_object.body(),
                              guid=rssgen.Guid(the_link),
                              pubDate=datetime.datetime.fromtimestamp(file_object.mtime))
        return item
</code></pre>
<p>The function that creates an RSS file from the files in a given directory is the topmost function. It takes all variables that are set from the command line and returns the RSS object.</p>
<p>It first lists all files with the given extension (default being <code>.html</code>) in the input directory. Then it compares the modification time of the RSS file with the modification times of these listed files. If there is no previous RSS file or there are newer HTML files, the RSS is generated again.</p>
<p>Note that a certain amount of <em>edit time</em> can be set, so that the script doesn’t consider files as <em>new</em> if they are modified within <em>edit time</em> minutes. This can be set to prevent too frequent generation of files during an edit session.</p>
<p>To generate the RSS, each file is supplied to the previous function and an RSS item is obtained; then these are fed into the <code>RSS2</code> function of <code>PyRSS2Gen</code> to get the resulting object.</p>
<pre><code class="language-python">def generate_rss(input_dir, extension = ".html", output = "rss/rss.xml", site_title = "Title", site_description = "Description", max_items = 20, site_url = "http://example.com", edit_time=0, exclude_regex = None):
        if not site_url.endswith("/"):
            site_url += "/"
        files = file_list(input_dir, extension, get_repo_path(input_dir), exclude_regex)
        rssmtime = get_mtime(output)
        files_up = [f for f in files if f.mtime &gt;= (rssmtime + edit_time)]
        if len(files_up) &gt; 0:
            print "Before", files_up
            files_up.sort(key=lambda x: x.mtime, reverse=True)
            print "After", files_up
            rss_items = [get_rss_item(fo, input_dir, site_url) for fo in files_up[:max_items]]
            rssobj = rssgen.RSS2(title = site_title,
                                 link = site_url,
                                 description = site_description,
                                 lastBuildDate = datetime.datetime.now(),
                                 items = rss_items)
            return rssobj
        return None
</code></pre>
<p>The main function uses <code>argparse</code> to handle options. The input directory, site title, site URL, and number of items are mandatory; other options have sensible default values.</p>
<p>The function also builds the <code>exclude_regex</code> object to supply to file listings. The regex is built here from the supplied string, and all other functions use this compiled regex.</p>
<p>After generating the RSS, it writes the file with the <code>write_xml</code> function.</p>
<pre><code class="language-python">
    def main():

        parser = argparse.ArgumentParser(description='Generate RSS feed from a set of HTML files')

        parser.add_argument('--input-dir', help="input directory", required=True)
        parser.add_argument("--extension", help="file extension to collect", default=".html")
        parser.add_argument("--output", help="output filename to write the results", default="rss/rss.xml")
        parser.add_argument('--title', help="title of the RSS feed", required=True)
        parser.add_argument("--description", help="site description", default="")
        parser.add_argument("--items", help="max items included in the feed", required=True, type=int)
        parser.add_argument("--site-url", help="site url of items", required=True)
        parser.add_argument("--exclude-regex", help="regex to set skipped files", default="")
        parser.add_argument("--edit-time", help="minutes to wait before putting an item into rss", default=0, type=int)

        args = vars(parser.parse_args())

        if args["exclude_regex"] == "":
            exclude_regex = None
        else:
            exclude_regex = re.compile(args["exclude_regex"])

        rssresults = generate_rss(args["input_dir"],
                                  args["extension"],
                                  args["output"],
                                  args["title"],
                                  args["description"],
                                  args["items"],
                                  args["site_url"],
                                  args["edit_time"],
                                  exclude_regex)

        if rssresults != None:
            rssresults.write_xml(open(args["output"], "w"))



    if __name__ == "__main__":
        main()
</code></pre>
<p>You can get the resulting Python script from <code>rss-generator.py</code>.</p>]]></content:encoded>
    </item>
  </channel>
</rss>
