From 34ba9869d1aaed66f6c0431d53e90509128636a0 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 12 Feb 2026 00:06:12 +0000 Subject: [PATCH 01/20] fix(deps): update dependency typer to v0.23.0 --- pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a58de8c..7c117be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ dependencies = [ "pydantic==2.12.5", "pydantic-settings[yaml]==2.12.0", "rich==14.3.2", - "typer==0.21.2", + "typer==0.23.0", "xdg-base-dirs==6.0.2", ] diff --git a/uv.lock b/uv.lock index 4be435e..6b0c03b 100644 --- a/uv.lock +++ b/uv.lock @@ -393,7 +393,7 @@ requires-dist = [ { name = "pydantic", specifier = "==2.12.5" }, { name = "pydantic-settings", extras = ["yaml"], specifier = "==2.12.0" }, { name = "rich", specifier = "==14.3.2" }, - { name = "typer", specifier = "==0.21.2" }, + { name = "typer", specifier = "==0.23.0" }, { name = "xdg-base-dirs", specifier = "==6.0.2" }, ] @@ -407,7 +407,7 @@ dev = [ [[package]] name = "typer" -version = "0.21.2" +version = "0.23.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, @@ -415,9 +415,9 @@ dependencies = [ { name = "rich" }, { name = "shellingham" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f2/1e/a27cc02a0cd715118c71fa2aef2c687fdefc3c28d90fd0dd789c5118154c/typer-0.21.2.tar.gz", hash = "sha256:1abd95a3b675e17ff61b0838ac637fe9478d446d62ad17fa4bb81ea57cc54028", size = 120426, upload-time = "2026-02-10T19:33:46.182Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/e6/44e073787aa57cd71c151f44855232feb0f748428fd5242d7366e3c4ae8b/typer-0.23.0.tar.gz", hash = "sha256:d8378833e47ada5d3d093fa20c4c63427cc4e27127f6b349a6c359463087d8cc", size = 120181, upload-time = "2026-02-11T15:22:18.637Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/cc/d59f893fbdfb5f58770c05febfc4086a46875f1084453621c35605cec946/typer-0.21.2-py3-none-any.whl", hash = "sha256:c3d8de54d00347ef90b82131ca946274f017cffb46683ae3883c360fa958f55c", size = 56728, upload-time = "2026-02-10T19:33:48.01Z" }, + { url = "https://files.pythonhosted.org/packages/7a/ed/d6fca788b51d0d4640c4bc82d0e85bad4b49809bca36bf4af01b4dcb66a7/typer-0.23.0-py3-none-any.whl", hash = "sha256:79f4bc262b6c37872091072a3cb7cb6d7d79ee98c0c658b4364bdcde3c42c913", size = 56668, upload-time = "2026-02-11T15:22:21.075Z" }, ] [[package]] From c0911307fdddd4571238eae5de533321b4072106 Mon Sep 17 00:00:00 2001 From: Konstantin Fickel Date: Sat, 7 Feb 2026 22:03:45 +0100 Subject: [PATCH 02/20] docs: expand README with project overview, concepts, and usage --- README.md | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2fcf603..fca374d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,34 @@ # streamer -Searching for `@tags` in time-based [streams](https://www.cs.yale.edu/homes/freeman/lifestreams.html). +Streamer is a personal knowledge management and time-tracking CLI tool. It organizes time-ordered markdown files using `@tag` annotations, letting you manage tasks, track time, and query your notes from the terminal. -# Usage +## Core Concepts -Running `streamer` finds all lines with @Task \ No newline at end of file +- **Shards** — Sections of markdown files, organized hierarchically by headings. Each shard can contain markers, tags, and nested child shards. +- **Markers** — Special `@tags` like `@Task`, `@Done`, `@Waiting`, or `@Timesheet` that give shards semantic meaning and place them into dimensions. +- **Dimensions** — Classification axes (e.g. task state, project, timesheet) that categorize shards. Some dimensions propagate to child shards. + +## File Format + +Markdown files are named with a timestamp: `YYYYMMDD-HHMMSS [markers].md` + +For example: `20260131-210000 Task Streamer.md` + +Within files, `@`-prefixed markers at the beginning of paragraphs or headings define how a shard is categorized. + +## Commands + +- `streamer` / `streamer new` — Create a new timestamped markdown entry, opening your editor +- `streamer todo` — Show all open tasks (shards with `@Task` markers) +- `streamer edit [number]` — Edit a stream file by index (most recent first) +- `streamer timesheet` — Generate time reports from `@Timesheet` markers + +## Configuration + +Streamer reads its configuration from `~/.config/streamer/config.yaml` (XDG standard). The main setting is `base_folder`, which points to the directory containing your stream files (defaults to the current working directory). + +## Usage + +Running `streamer` opens your editor to create a new entry. After saving, the file is renamed based on its timestamp and any markers found in the content. + +Running `streamer todo` finds all shards marked as open tasks and displays them as rich-formatted panels in your terminal. From 646241f355a1587eecf26649cbe3aefa3889e169 Mon Sep 17 00:00:00 2001 From: Konstantin Fickel Date: Sat, 14 Feb 2026 17:59:43 +0100 Subject: [PATCH 03/20] feat: add home-manager module output --- flake.lock | 12 ++++++------ flake.nix | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 0f2727f..4fa0a20 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1769789167, - "narHash": "sha256-kKB3bqYJU5nzYeIROI82Ef9VtTbu4uA3YydSk/Bioa8=", + "lastModified": 1770197578, + "narHash": "sha256-AYqlWrX09+HvGs8zM6ebZ1pwUqjkfpnv8mewYwAo+iM=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "62c8382960464ceb98ea593cb8321a2cf8f9e3e5", + "rev": "00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", "type": "github" }, "original": { @@ -80,11 +80,11 @@ ] }, "locked": { - "lastModified": 1769957392, - "narHash": "sha256-6PkqwwYf5K2CHi2V+faI/9pqjfz/HxUkI/MVid6hlOY=", + "lastModified": 1770331927, + "narHash": "sha256-jlOvO++uvne/lTgWqdI4VhTV5OpVWi70ZDVBlT6vGSs=", "owner": "pyproject-nix", "repo": "uv2nix", - "rev": "d18bc50ae1c3d4be9c41c2d94ea765524400af75", + "rev": "5b43a934e15b23bfba6c408cba1c570eccf80080", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 669701a..d112b66 100644 --- a/flake.nix +++ b/flake.nix @@ -85,6 +85,50 @@ in { + homeManagerModules.default = + { + lib, + config, + pkgs, + ... + }: + let + cfg = config.programs.streamer; + in + { + options.programs.streamer = { + enable = lib.mkEnableOption "streamer"; + + base-folder = lib.mkOption { + type = lib.types.str; + description = "Base Folder of Streamer"; + }; + + package = lib.mkOption { + type = lib.types.package; + default = self.packages.${pkgs.system}.streamer; + defaultText = lib.literalExpression "inputs.streamer.packages.\${pkgs.system}.streamer"; + description = "The package to use for the streamer binary."; + }; + }; + + config = lib.mkIf cfg.enable { + assertions = [ + (lib.hm.assertions.assertPlatform "programs.streamer" pkgs lib.platforms.linux) + ]; + + home.packages = [ cfg.package ]; + + xdg.configFile."streamer/config.yaml".source = + (pkgs.formats.yaml { }).generate "streamer-configuration" + { + base_folder = cfg.base-folder; + }; + + home.shellAliases.s = "streamer"; + }; + }; + # Package a virtual environment as our main application. # # Enable no optional dependencies for production build. From 2298bdaa8f28d5b0e5fb2a8b4bb4a055b4c00503 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sun, 15 Feb 2026 00:06:41 +0000 Subject: [PATCH 04/20] chore(deps): update dependency ruff to v0.15.1 --- pyproject.toml | 2 +- uv.lock | 40 ++++++++++++++++++++-------------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7c117be..f526dda 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,5 +26,5 @@ dev = [ "faker==40.4.0", "pyright==1.1.408", "pytest==9.0.2", - "ruff==0.15.0", + "ruff==0.15.1", ] diff --git a/uv.lock b/uv.lock index 6b0c03b..bdacd98 100644 --- a/uv.lock +++ b/uv.lock @@ -332,27 +332,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.0" +version = "0.15.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c8/39/5cee96809fbca590abea6b46c6d1c586b49663d1d2830a751cc8fc42c666/ruff-0.15.0.tar.gz", hash = "sha256:6bdea47cdbea30d40f8f8d7d69c0854ba7c15420ec75a26f463290949d7f7e9a", size = 4524893, upload-time = "2026-02-03T17:53:35.357Z" } +sdist = { url = "https://files.pythonhosted.org/packages/04/dc/4e6ac71b511b141cf626357a3946679abeba4cf67bc7cc5a17920f31e10d/ruff-0.15.1.tar.gz", hash = "sha256:c590fe13fb57c97141ae975c03a1aedb3d3156030cabd740d6ff0b0d601e203f", size = 4540855, upload-time = "2026-02-12T23:09:09.998Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/88/3fd1b0aa4b6330d6aaa63a285bc96c9f71970351579152d231ed90914586/ruff-0.15.0-py3-none-linux_armv6l.whl", hash = "sha256:aac4ebaa612a82b23d45964586f24ae9bc23ca101919f5590bdb368d74ad5455", size = 10354332, upload-time = "2026-02-03T17:52:54.892Z" }, - { url = "https://files.pythonhosted.org/packages/72/f6/62e173fbb7eb75cc29fe2576a1e20f0a46f671a2587b5f604bfb0eaf5f6f/ruff-0.15.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dcd4be7cc75cfbbca24a98d04d0b9b36a270d0833241f776b788d59f4142b14d", size = 10767189, upload-time = "2026-02-03T17:53:19.778Z" }, - { url = "https://files.pythonhosted.org/packages/99/e4/968ae17b676d1d2ff101d56dc69cf333e3a4c985e1ec23803df84fc7bf9e/ruff-0.15.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d747e3319b2bce179c7c1eaad3d884dc0a199b5f4d5187620530adf9105268ce", size = 10075384, upload-time = "2026-02-03T17:53:29.241Z" }, - { url = "https://files.pythonhosted.org/packages/a2/bf/9843c6044ab9e20af879c751487e61333ca79a2c8c3058b15722386b8cae/ruff-0.15.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:650bd9c56ae03102c51a5e4b554d74d825ff3abe4db22b90fd32d816c2e90621", size = 10481363, upload-time = "2026-02-03T17:52:43.332Z" }, - { url = "https://files.pythonhosted.org/packages/55/d9/4ada5ccf4cd1f532db1c8d44b6f664f2208d3d93acbeec18f82315e15193/ruff-0.15.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6664b7eac559e3048223a2da77769c2f92b43a6dfd4720cef42654299a599c9", size = 10187736, upload-time = "2026-02-03T17:53:00.522Z" }, - { url = "https://files.pythonhosted.org/packages/86/e2/f25eaecd446af7bb132af0a1d5b135a62971a41f5366ff41d06d25e77a91/ruff-0.15.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f811f97b0f092b35320d1556f3353bf238763420ade5d9e62ebd2b73f2ff179", size = 10968415, upload-time = "2026-02-03T17:53:15.705Z" }, - { url = "https://files.pythonhosted.org/packages/e7/dc/f06a8558d06333bf79b497d29a50c3a673d9251214e0d7ec78f90b30aa79/ruff-0.15.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:761ec0a66680fab6454236635a39abaf14198818c8cdf691e036f4bc0f406b2d", size = 11809643, upload-time = "2026-02-03T17:53:23.031Z" }, - { url = "https://files.pythonhosted.org/packages/dd/45/0ece8db2c474ad7df13af3a6d50f76e22a09d078af63078f005057ca59eb/ruff-0.15.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:940f11c2604d317e797b289f4f9f3fa5555ffe4fb574b55ed006c3d9b6f0eb78", size = 11234787, upload-time = "2026-02-03T17:52:46.432Z" }, - { url = "https://files.pythonhosted.org/packages/8a/d9/0e3a81467a120fd265658d127db648e4d3acfe3e4f6f5d4ea79fac47e587/ruff-0.15.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcbca3d40558789126da91d7ef9a7c87772ee107033db7191edefa34e2c7f1b4", size = 11112797, upload-time = "2026-02-03T17:52:49.274Z" }, - { url = "https://files.pythonhosted.org/packages/b2/cb/8c0b3b0c692683f8ff31351dfb6241047fa873a4481a76df4335a8bff716/ruff-0.15.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9a121a96db1d75fa3eb39c4539e607f628920dd72ff1f7c5ee4f1b768ac62d6e", size = 11033133, upload-time = "2026-02-03T17:53:33.105Z" }, - { url = "https://files.pythonhosted.org/packages/f8/5e/23b87370cf0f9081a8c89a753e69a4e8778805b8802ccfe175cc410e50b9/ruff-0.15.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5298d518e493061f2eabd4abd067c7e4fb89e2f63291c94332e35631c07c3662", size = 10442646, upload-time = "2026-02-03T17:53:06.278Z" }, - { url = "https://files.pythonhosted.org/packages/e1/9a/3c94de5ce642830167e6d00b5c75aacd73e6347b4c7fc6828699b150a5ee/ruff-0.15.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afb6e603d6375ff0d6b0cee563fa21ab570fd15e65c852cb24922cef25050cf1", size = 10195750, upload-time = "2026-02-03T17:53:26.084Z" }, - { url = "https://files.pythonhosted.org/packages/30/15/e396325080d600b436acc970848d69df9c13977942fb62bb8722d729bee8/ruff-0.15.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:77e515f6b15f828b94dc17d2b4ace334c9ddb7d9468c54b2f9ed2b9c1593ef16", size = 10676120, upload-time = "2026-02-03T17:53:09.363Z" }, - { url = "https://files.pythonhosted.org/packages/8d/c9/229a23d52a2983de1ad0fb0ee37d36e0257e6f28bfd6b498ee2c76361874/ruff-0.15.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6f6e80850a01eb13b3e42ee0ebdf6e4497151b48c35051aab51c101266d187a3", size = 11201636, upload-time = "2026-02-03T17:52:57.281Z" }, - { url = "https://files.pythonhosted.org/packages/6f/b0/69adf22f4e24f3677208adb715c578266842e6e6a3cc77483f48dd999ede/ruff-0.15.0-py3-none-win32.whl", hash = "sha256:238a717ef803e501b6d51e0bdd0d2c6e8513fe9eec14002445134d3907cd46c3", size = 10465945, upload-time = "2026-02-03T17:53:12.591Z" }, - { url = "https://files.pythonhosted.org/packages/51/ad/f813b6e2c97e9b4598be25e94a9147b9af7e60523b0cb5d94d307c15229d/ruff-0.15.0-py3-none-win_amd64.whl", hash = "sha256:dd5e4d3301dc01de614da3cdffc33d4b1b96fb89e45721f1598e5532ccf78b18", size = 11564657, upload-time = "2026-02-03T17:52:51.893Z" }, - { url = "https://files.pythonhosted.org/packages/f6/b0/2d823f6e77ebe560f4e397d078487e8d52c1516b331e3521bc75db4272ca/ruff-0.15.0-py3-none-win_arm64.whl", hash = "sha256:c480d632cc0ca3f0727acac8b7d053542d9e114a462a145d0b00e7cd658c515a", size = 10865753, upload-time = "2026-02-03T17:53:03.014Z" }, + { url = "https://files.pythonhosted.org/packages/23/bf/e6e4324238c17f9d9120a9d60aa99a7daaa21204c07fcd84e2ef03bb5fd1/ruff-0.15.1-py3-none-linux_armv6l.whl", hash = "sha256:b101ed7cf4615bda6ffe65bdb59f964e9f4a0d3f85cbf0e54f0ab76d7b90228a", size = 10367819, upload-time = "2026-02-12T23:09:03.598Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ea/c8f89d32e7912269d38c58f3649e453ac32c528f93bb7f4219258be2e7ed/ruff-0.15.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:939c995e9277e63ea632cc8d3fae17aa758526f49a9a850d2e7e758bfef46602", size = 10798618, upload-time = "2026-02-12T23:09:22.928Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0f/1d0d88bc862624247d82c20c10d4c0f6bb2f346559d8af281674cf327f15/ruff-0.15.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d83466455fdefe60b8d9c8df81d3c1bbb2115cede53549d3b522ce2bc703899", size = 10148518, upload-time = "2026-02-12T23:08:58.339Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c8/291c49cefaa4a9248e986256df2ade7add79388fe179e0691be06fae6f37/ruff-0.15.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9457e3c3291024866222b96108ab2d8265b477e5b1534c7ddb1810904858d16", size = 10518811, upload-time = "2026-02-12T23:09:31.865Z" }, + { url = "https://files.pythonhosted.org/packages/c3/1a/f5707440e5ae43ffa5365cac8bbb91e9665f4a883f560893829cf16a606b/ruff-0.15.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:92c92b003e9d4f7fbd33b1867bb15a1b785b1735069108dfc23821ba045b29bc", size = 10196169, upload-time = "2026-02-12T23:09:17.306Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ff/26ddc8c4da04c8fd3ee65a89c9fb99eaa5c30394269d424461467be2271f/ruff-0.15.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fe5c41ab43e3a06778844c586251eb5a510f67125427625f9eb2b9526535779", size = 10990491, upload-time = "2026-02-12T23:09:25.503Z" }, + { url = "https://files.pythonhosted.org/packages/fc/00/50920cb385b89413f7cdb4bb9bc8fc59c1b0f30028d8bccc294189a54955/ruff-0.15.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66a6dd6df4d80dc382c6484f8ce1bcceb55c32e9f27a8b94c32f6c7331bf14fb", size = 11843280, upload-time = "2026-02-12T23:09:19.88Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6d/2f5cad8380caf5632a15460c323ae326f1e1a2b5b90a6ee7519017a017ca/ruff-0.15.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a4a42cbb8af0bda9bcd7606b064d7c0bc311a88d141d02f78920be6acb5aa83", size = 11274336, upload-time = "2026-02-12T23:09:14.907Z" }, + { url = "https://files.pythonhosted.org/packages/a3/1d/5f56cae1d6c40b8a318513599b35ea4b075d7dc1cd1d04449578c29d1d75/ruff-0.15.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ab064052c31dddada35079901592dfba2e05f5b1e43af3954aafcbc1096a5b2", size = 11137288, upload-time = "2026-02-12T23:09:07.475Z" }, + { url = "https://files.pythonhosted.org/packages/cd/20/6f8d7d8f768c93b0382b33b9306b3b999918816da46537d5a61635514635/ruff-0.15.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5631c940fe9fe91f817a4c2ea4e81f47bee3ca4aa646134a24374f3c19ad9454", size = 11070681, upload-time = "2026-02-12T23:08:55.43Z" }, + { url = "https://files.pythonhosted.org/packages/9a/67/d640ac76069f64cdea59dba02af2e00b1fa30e2103c7f8d049c0cff4cafd/ruff-0.15.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68138a4ba184b4691ccdc39f7795c66b3c68160c586519e7e8444cf5a53e1b4c", size = 10486401, upload-time = "2026-02-12T23:09:27.927Z" }, + { url = "https://files.pythonhosted.org/packages/65/3d/e1429f64a3ff89297497916b88c32a5cc88eeca7e9c787072d0e7f1d3e1e/ruff-0.15.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:518f9af03bfc33c03bdb4cb63fabc935341bb7f54af500f92ac309ecfbba6330", size = 10197452, upload-time = "2026-02-12T23:09:12.147Z" }, + { url = "https://files.pythonhosted.org/packages/78/83/e2c3bade17dad63bf1e1c2ffaf11490603b760be149e1419b07049b36ef2/ruff-0.15.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:da79f4d6a826caaea95de0237a67e33b81e6ec2e25fc7e1993a4015dffca7c61", size = 10693900, upload-time = "2026-02-12T23:09:34.418Z" }, + { url = "https://files.pythonhosted.org/packages/a1/27/fdc0e11a813e6338e0706e8b39bb7a1d61ea5b36873b351acee7e524a72a/ruff-0.15.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3dd86dccb83cd7d4dcfac303ffc277e6048600dfc22e38158afa208e8bf94a1f", size = 11227302, upload-time = "2026-02-12T23:09:36.536Z" }, + { url = "https://files.pythonhosted.org/packages/f6/58/ac864a75067dcbd3b95be5ab4eb2b601d7fbc3d3d736a27e391a4f92a5c1/ruff-0.15.1-py3-none-win32.whl", hash = "sha256:660975d9cb49b5d5278b12b03bb9951d554543a90b74ed5d366b20e2c57c2098", size = 10462555, upload-time = "2026-02-12T23:09:29.899Z" }, + { url = "https://files.pythonhosted.org/packages/e0/5e/d4ccc8a27ecdb78116feac4935dfc39d1304536f4296168f91ed3ec00cd2/ruff-0.15.1-py3-none-win_amd64.whl", hash = "sha256:c820fef9dd5d4172a6570e5721704a96c6679b80cf7be41659ed439653f62336", size = 11599956, upload-time = "2026-02-12T23:09:01.157Z" }, + { url = "https://files.pythonhosted.org/packages/2a/07/5bda6a85b220c64c65686bc85bd0bbb23b29c62b3a9f9433fa55f17cda93/ruff-0.15.1-py3-none-win_arm64.whl", hash = "sha256:5ff7d5f0f88567850f45081fac8f4ec212be8d0b963e385c3f7d0d2eb4899416", size = 10874604, upload-time = "2026-02-12T23:09:05.515Z" }, ] [[package]] @@ -402,7 +402,7 @@ dev = [ { name = "faker", specifier = "==40.4.0" }, { name = "pyright", specifier = "==1.1.408" }, { name = "pytest", specifier = "==9.0.2" }, - { name = "ruff", specifier = "==0.15.0" }, + { name = "ruff", specifier = "==0.15.1" }, ] [[package]] From 1e203d9db3d5e438bd3b646a63c99bdef627d010 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sun, 15 Feb 2026 00:06:52 +0000 Subject: [PATCH 05/20] fix(deps): update dependency typer to v0.23.1 --- pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f526dda..7edb03b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ dependencies = [ "pydantic==2.12.5", "pydantic-settings[yaml]==2.12.0", "rich==14.3.2", - "typer==0.23.0", + "typer==0.23.1", "xdg-base-dirs==6.0.2", ] diff --git a/uv.lock b/uv.lock index bdacd98..30d9760 100644 --- a/uv.lock +++ b/uv.lock @@ -393,7 +393,7 @@ requires-dist = [ { name = "pydantic", specifier = "==2.12.5" }, { name = "pydantic-settings", extras = ["yaml"], specifier = "==2.12.0" }, { name = "rich", specifier = "==14.3.2" }, - { name = "typer", specifier = "==0.23.0" }, + { name = "typer", specifier = "==0.23.1" }, { name = "xdg-base-dirs", specifier = "==6.0.2" }, ] @@ -407,7 +407,7 @@ dev = [ [[package]] name = "typer" -version = "0.23.0" +version = "0.23.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, @@ -415,9 +415,9 @@ dependencies = [ { name = "rich" }, { name = "shellingham" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7e/e6/44e073787aa57cd71c151f44855232feb0f748428fd5242d7366e3c4ae8b/typer-0.23.0.tar.gz", hash = "sha256:d8378833e47ada5d3d093fa20c4c63427cc4e27127f6b349a6c359463087d8cc", size = 120181, upload-time = "2026-02-11T15:22:18.637Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/07/b822e1b307d40e263e8253d2384cf98c51aa2368cc7ba9a07e523a1d964b/typer-0.23.1.tar.gz", hash = "sha256:2070374e4d31c83e7b61362fd859aa683576432fd5b026b060ad6b4cd3b86134", size = 120047, upload-time = "2026-02-13T10:04:30.984Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/ed/d6fca788b51d0d4640c4bc82d0e85bad4b49809bca36bf4af01b4dcb66a7/typer-0.23.0-py3-none-any.whl", hash = "sha256:79f4bc262b6c37872091072a3cb7cb6d7d79ee98c0c658b4364bdcde3c42c913", size = 56668, upload-time = "2026-02-11T15:22:21.075Z" }, + { url = "https://files.pythonhosted.org/packages/d5/91/9b286ab899c008c2cb05e8be99814807e7fbbd33f0c0c960470826e5ac82/typer-0.23.1-py3-none-any.whl", hash = "sha256:3291ad0d3c701cbf522012faccfbb29352ff16ad262db2139e6b01f15781f14e", size = 56813, upload-time = "2026-02-13T10:04:32.008Z" }, ] [[package]] From 49cd9bcfa055608dfadfe9d7a0c3a9c9bf9a8376 Mon Sep 17 00:00:00 2001 From: Konstantin Fickel Date: Sun, 15 Feb 2026 17:10:09 +0100 Subject: [PATCH 06/20] fix: resolve all basedpyright warnings - Use collections.abc.Generator/Iterable instead of deprecated typing imports - Replace Optional with union syntax (X | None) - Add explicit type annotations to eliminate reportUnknownVariableType - Use typing.cast for untyped mistletoe attributes (content, level, line_number) - Replace mutable default arguments with None defaults (reportCallInDefaultInitializer) - Add ClassVar annotation for model_config (reportIncompatibleVariableOverride) - Add @override decorator for settings_customise_sources (reportImplicitOverride) - Annotate class attributes in Tag (reportUnannotatedClassAttribute) - Add parameter type annotations in test (reportMissingParameterType) - Assign unused call result to _ (reportUnusedCallResult) --- src/streamd/__init__.py | 126 +++++++++ src/streamd/localize/localize.py | 73 +++++ .../localize/repository_configuration.py | 106 +++++++ src/streamd/parse/extract_tag.py | 92 +++++++ src/streamd/parse/markdown_tag.py | 23 ++ src/streamd/parse/parse.py | 258 ++++++++++++++++++ src/streamd/query/find.py | 36 +++ src/streamd/settings/__init__.py | 38 +++ .../test_repository_configuration_merge.py | 20 +- test/timesheet/test_extract_timesheets.py | 14 +- 10 files changed, 770 insertions(+), 16 deletions(-) create mode 100644 src/streamd/__init__.py create mode 100644 src/streamd/localize/localize.py create mode 100644 src/streamd/localize/repository_configuration.py create mode 100644 src/streamd/parse/extract_tag.py create mode 100644 src/streamd/parse/markdown_tag.py create mode 100644 src/streamd/parse/parse.py create mode 100644 src/streamd/query/find.py create mode 100644 src/streamd/settings/__init__.py diff --git a/src/streamd/__init__.py b/src/streamd/__init__.py new file mode 100644 index 0000000..c72d098 --- /dev/null +++ b/src/streamd/__init__.py @@ -0,0 +1,126 @@ +import glob +import os +from collections.abc import Generator +from datetime import datetime +from shutil import move +from typing import Annotated + +import click +import typer +from rich import print +from rich.markdown import Markdown +from rich.panel import Panel + +from streamd.localize import ( + LocalizedShard, + RepositoryConfiguration, + localize_stream_file, +) +from streamd.localize.preconfigured_configurations import TaskConfiguration +from streamd.parse import parse_markdown_file +from streamd.query import find_shard_by_position +from streamd.settings import Settings +from streamd.timesheet.configuration import BasicTimesheetConfiguration +from streamd.timesheet.extract import extract_timesheets + +app = typer.Typer() + + +def all_files(config: RepositoryConfiguration) -> Generator[LocalizedShard]: + for file_name in glob.glob(f"{glob.escape(Settings().base_folder)}/*.md"): + with open(file_name, "r") as file: + file_content = file.read() + if shard := localize_stream_file( + parse_markdown_file(file_name, file_content), config + ): + yield shard + + +@app.command() +def todo() -> None: + all_shards = list(all_files(TaskConfiguration)) + + for task_shard in find_shard_by_position(all_shards, "task", "open"): + with open(task_shard.location["file"], "r") as file: + file_content = file.read().splitlines() + print( + Panel( + Markdown( + "\n".join( + file_content[ + task_shard.start_line - 1 : task_shard.end_line + ] + ) + ), + title=f"{task_shard.location['file']}:{task_shard.start_line}", + ) + ) + + +@app.command() +def edit(number: Annotated[int, typer.Argument()] = 1) -> None: + all_shards = list(all_files(TaskConfiguration)) + sorted_shards = sorted(all_shards, key=lambda s: s.moment) + + if abs(number) >= len(sorted_shards): + raise ValueError("Argument out of range") + + selected_number = number + if selected_number >= 0: + selected_number = len(sorted_shards) - selected_number + else: + selected_number = -selected_number + + click.edit(None, filename=sorted_shards[selected_number].location["file"]) + + +@app.command() +def timesheet() -> None: + all_shards = list(all_files(BasicTimesheetConfiguration)) + sheets = sorted(extract_timesheets(all_shards), key=lambda card: card.date) + for sheet in sheets: + print(sheet.date) + print( + ",".join( + map(lambda card: f"{card.from_time},{card.to_time}", sheet.timecards) + ), + ) + + +@app.command() +def new() -> None: + streamd_directory = Settings().base_folder + + timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") + preliminary_file_name = f"{timestamp}_wip.md" + prelimary_path = os.path.join(streamd_directory, preliminary_file_name) + + content = "# " + with open(prelimary_path, "w") as file: + _ = file.write(content) + + click.edit(None, filename=prelimary_path) + + with open(prelimary_path, "r") as file: + content = file.read() + parsed_content = parse_markdown_file(prelimary_path, content) + + final_file_name = f"{timestamp}.md" + if parsed_content.shard is not None and len( + markers := parsed_content.shard.markers + ): + final_file_name = f"{timestamp} {' '.join(markers)}.md" + + final_path = os.path.join(streamd_directory, final_file_name) + _ = move(prelimary_path, final_path) + print(f"Saved as [yellow]{final_file_name}") + + +@app.callback(invoke_without_command=True) +def main(ctx: typer.Context): + if ctx.invoked_subcommand is None: + new() + + +if __name__ == "__main__": + app() diff --git a/src/streamd/localize/localize.py b/src/streamd/localize/localize.py new file mode 100644 index 0000000..bc084f4 --- /dev/null +++ b/src/streamd/localize/localize.py @@ -0,0 +1,73 @@ +from datetime import datetime + +from streamd.parse.shard import Shard, StreamFile + +from .extract_datetime import ( + extract_datetime_from_file_name, + extract_datetime_from_marker_list, +) +from .localized_shard import LocalizedShard +from .repository_configuration import RepositoryConfiguration + + +def localize_shard( + shard: Shard, + config: RepositoryConfiguration, + propagated: dict[str, str], + moment: datetime, +) -> LocalizedShard: + position = {**propagated} + private_position: dict[str, str] = {} + + adjusted_moment: datetime = extract_datetime_from_marker_list(shard.markers, moment) + + for marker in shard.markers: + if marker in config.markers: + marker_definition = config.markers[marker] + for placement in marker_definition.placements: + if placement.if_with <= set(shard.markers): + dimension = config.dimensions[placement.dimension] + + value = placement.value or marker + + if placement.overwrites or ( + placement.dimension not in position + and placement.dimension not in private_position + ): + if dimension.propagate: + position[placement.dimension] = value + else: + private_position[placement.dimension] = value + + children = [ + localize_shard(child, config, position, adjusted_moment) + for child in shard.children + ] + + position.update(private_position) + + return LocalizedShard( + markers=shard.markers, + tags=shard.tags, + start_line=shard.start_line, + end_line=shard.end_line, + location=position, + children=children, + moment=adjusted_moment, + ) + + +def localize_stream_file( + stream_file: StreamFile, config: RepositoryConfiguration +) -> LocalizedShard | None: + shard_date = extract_datetime_from_file_name(stream_file.file_name) + + if not shard_date or not stream_file.shard: + raise ValueError("Could not extract date") + + return localize_shard( + stream_file.shard, config, {"file": stream_file.file_name}, shard_date + ) + + +__all__ = ["localize_stream_file"] diff --git a/src/streamd/localize/repository_configuration.py b/src/streamd/localize/repository_configuration.py new file mode 100644 index 0000000..4b83a03 --- /dev/null +++ b/src/streamd/localize/repository_configuration.py @@ -0,0 +1,106 @@ +from __future__ import annotations + +from pydantic import BaseModel + + +class Dimension(BaseModel): + display_name: str + comment: str | None = None + propagate: bool = False + + +class MarkerPlacement(BaseModel): + if_with: set[str] = set() + dimension: str + value: str | None = None + overwrites: bool = True + + +class Marker(BaseModel): + display_name: str + placements: list[MarkerPlacement] = [] + + +class RepositoryConfiguration(BaseModel): + dimensions: dict[str, Dimension] + markers: dict[str, Marker] + + +def merge_single_dimension(base: Dimension, second: Dimension) -> Dimension: + second_fields_set: set[str] = getattr(second, "model_fields_set", set()) + + return Dimension( + display_name=second.display_name or base.display_name, + comment=base.comment if second.comment is None else second.comment, + propagate=second.propagate + if "propagate" in second_fields_set + else base.propagate, + ) + + +def merge_dimensions( + base: dict[str, Dimension], second: dict[str, Dimension] +) -> dict[str, Dimension]: + merged: dict[str, Dimension] = dict(base) + for key, second_dimension in second.items(): + if key in merged: + merged[key] = merge_single_dimension(merged[key], second_dimension) + else: + merged[key] = second_dimension + return merged + + +def _placement_identity(p: MarkerPlacement) -> tuple[frozenset[str], str]: + return (frozenset(p.if_with), p.dimension) + + +def merge_single_marker(base: Marker, second: Marker) -> Marker: + merged_display_name = second.display_name or base.display_name + + merged_placements: list[MarkerPlacement] = [] + seen: dict[tuple[frozenset[str], str], int] = {} + + for placement in base.placements: + ident = _placement_identity(placement) + seen[ident] = len(merged_placements) + merged_placements.append(placement) + + for placement in second.placements: + ident = _placement_identity(placement) + if ident in seen: + merged_placements[seen[ident]] = placement + else: + seen[ident] = len(merged_placements) + merged_placements.append(placement) + + return Marker(display_name=merged_display_name, placements=merged_placements) + + +def merge_markers( + base: dict[str, Marker], second: dict[str, Marker] +) -> dict[str, Marker]: + merged: dict[str, Marker] = dict(base) + for key, second_marker in second.items(): + if key in merged: + merged[key] = merge_single_marker(merged[key], second_marker) + else: + merged[key] = second_marker + return merged + + +def merge_repository_configuration( + base: RepositoryConfiguration, second: RepositoryConfiguration +) -> RepositoryConfiguration: + return RepositoryConfiguration( + dimensions=merge_dimensions(base.dimensions, second.dimensions), + markers=merge_markers(base.markers, second.markers), + ) + + +__all__ = [ + "Dimension", + "Marker", + "MarkerPlacement", + "RepositoryConfiguration", + "merge_repository_configuration", +] diff --git a/src/streamd/parse/extract_tag.py b/src/streamd/parse/extract_tag.py new file mode 100644 index 0000000..b7bfd45 --- /dev/null +++ b/src/streamd/parse/extract_tag.py @@ -0,0 +1,92 @@ +import re +from collections.abc import Iterable +from typing import cast + +from mistletoe.block_token import BlockToken +from mistletoe.span_token import Emphasis, Link, RawText, Strikethrough, Strong +from mistletoe.token import Token + +from .markdown_tag import Tag + + +def extract_markers_and_tags_from_single_token( + token: Token, + marker_boundary_encountered: bool, + return_at_first_marker: bool = False, +) -> tuple[list[str], list[str], bool]: + result_markers: list[str] = [] + result_tags: list[str] = [] + result_marker_boundary_encountered = marker_boundary_encountered + + if isinstance(token, Tag): + content = cast(str, token.content) + if marker_boundary_encountered: + result_tags.append(content) + else: + result_markers.append(content) + elif isinstance(token, (Emphasis, Strong, Strikethrough, Link)): + children = list(token.children or []) + markers, tags, child_marker_boundary_encountered = ( + extract_markers_and_tags_from_tokens( + children, + marker_boundary_encountered, + return_at_first_marker, + ) + ) + result_markers.extend(markers) + result_tags.extend(tags) + result_marker_boundary_encountered = ( + marker_boundary_encountered or child_marker_boundary_encountered + ) + elif isinstance(token, RawText): + content_raw = cast(str, token.content) + if not re.match(r"^[\s]*$", content_raw): + result_marker_boundary_encountered = True + else: + result_marker_boundary_encountered = True + + return result_markers, result_tags, result_marker_boundary_encountered + + +def extract_markers_and_tags_from_tokens( + tokens: Iterable[Token], + marker_boundary_encountered: bool, + return_at_first_marker: bool = False, +) -> tuple[list[str], list[str], bool]: + result_markers: list[str] = [] + result_tags: list[str] = [] + result_marker_boundary_encountered = marker_boundary_encountered + + for child in tokens: + markers, tags, child_marker_boundary_encountered = ( + extract_markers_and_tags_from_single_token( + child, result_marker_boundary_encountered, return_at_first_marker + ) + ) + result_markers.extend(markers) + result_tags.extend(tags) + result_marker_boundary_encountered = ( + marker_boundary_encountered or child_marker_boundary_encountered + ) + + if len(result_markers) > 0 and return_at_first_marker: + break + + return result_markers, result_tags, result_marker_boundary_encountered + + +def extract_markers_and_tags(block_token: BlockToken) -> tuple[list[str], list[str]]: + children = list(block_token.children or []) + markers, tags, _ = extract_markers_and_tags_from_tokens(children, False) + return markers, tags + + +def has_markers(block_token: BlockToken) -> bool: + children = list(block_token.children or []) + markers, _, _ = extract_markers_and_tags_from_tokens( + children, False, return_at_first_marker=True + ) + return len(markers) > 0 + + +__all__ = ["extract_markers_and_tags", "has_markers"] diff --git a/src/streamd/parse/markdown_tag.py b/src/streamd/parse/markdown_tag.py new file mode 100644 index 0000000..798f10e --- /dev/null +++ b/src/streamd/parse/markdown_tag.py @@ -0,0 +1,23 @@ +import re +from typing import cast + +from mistletoe.markdown_renderer import Fragment, MarkdownRenderer +from mistletoe.span_token import SpanToken + + +class Tag(SpanToken): + parse_inner: bool = False + pattern: re.Pattern[str] = re.compile(r"@([^\s*\x60~\[\]]+)") + + +class TagMarkdownRenderer(MarkdownRenderer): + def __init__(self) -> None: + super().__init__(Tag) # pyright: ignore[reportUnknownMemberType] + + def render_tag(self, token: Tag): + content = cast(str, token.content) + yield Fragment("@") + yield Fragment(content) + + +__all__ = ["Tag", "TagMarkdownRenderer"] diff --git a/src/streamd/parse/parse.py b/src/streamd/parse/parse.py new file mode 100644 index 0000000..4d14fa3 --- /dev/null +++ b/src/streamd/parse/parse.py @@ -0,0 +1,258 @@ +from collections import Counter +from typing import cast + +from mistletoe.block_token import ( + BlockToken, + Document, + Heading, + List, + ListItem, + Paragraph, +) + +from .extract_tag import extract_markers_and_tags, has_markers +from .list import split_at +from .markdown_tag import TagMarkdownRenderer +from .shard import Shard, StreamFile + + +def get_line_number(block_token: BlockToken) -> int: + return cast(int, block_token.line_number) # pyright: ignore[reportAttributeAccessIssue] + + +def build_shard( + start_line: int, + end_line: int, + markers: list[str] | None = None, + tags: list[str] | None = None, + children: list[Shard] | None = None, +) -> Shard: + markers = markers or [] + tags = tags or [] + children = children or [] + + if ( + len(children) == 1 + and len(tags) == 0 + and len(markers) == 0 + and children[0].start_line == start_line + and children[0].end_line == end_line + ): + return children[0] + + return Shard( + markers=markers, + tags=tags, + children=children, + start_line=start_line, + end_line=end_line, + ) + + +def merge_into_first_shard( + shards: list[Shard], + start_line: int, + end_line: int, + additional_tags: list[str] | None = None, +) -> Shard: + return shards[0].model_copy( + update={ + "start_line": start_line, + "end_line": end_line, + "children": shards[1:], + "tags": shards[0].tags + (additional_tags or []), + } + ) + + +def find_paragraph_shard_positions(block_tokens: list[BlockToken]) -> list[int]: + return [ + index + for index, block_token in enumerate(block_tokens) + if isinstance(block_token, Paragraph) and has_markers(block_token) + ] + + +def _heading_level(heading: Heading) -> int: + return cast(int, heading.level) + + +def find_headings_by_level( + block_tokens: list[BlockToken], header_level: int +) -> list[int]: + return [ + index + for index, block_token in enumerate(block_tokens) + if isinstance(block_token, Heading) + and _heading_level(block_token) == header_level + ] + + +def calculate_heading_level_for_next_split( + block_tokens: list[BlockToken], +) -> int | None: + """ + If there is no marker in any heading, then return None. + If only the first token is a heading with a marker, then return None. + Otherwise: Return the heading level with the lowest level (h1 < h2), of which there are two or which has a marker (and doesn't stem from first) + """ + level_of_headings_without_first_with_marker: list[int] = [ + _heading_level(token) + for token in block_tokens[1:] + if isinstance(token, Heading) and has_markers(token) + ] + + if len(level_of_headings_without_first_with_marker) == 0: + return None + + heading_level_counter: Counter[int] = Counter( + [_heading_level(token) for token in block_tokens if isinstance(token, Heading)] + ) + + return min( + [level for level, count in heading_level_counter.items() if count >= 2] + + level_of_headings_without_first_with_marker + ) + + +def parse_single_block_shards( + block_token: BlockToken, start_line: int, end_line: int +) -> tuple[Shard | None, list[str]]: + markers: list[str] = [] + tags: list[str] = [] + children: list[Shard] = [] + + if isinstance(block_token, List): + list_items: list[ListItem] = ( # pyright: ignore[reportAssignmentType] + list(block_token.children) if block_token.children is not None else [] + ) + for index, list_item in enumerate(list_items): + list_item_start_line = get_line_number(list_item) + list_item_end_line = ( + get_line_number(list_items[index + 1]) - 1 + if index + 1 < len(list_items) + else end_line + ) + list_item_shard, list_item_tags = parse_multiple_block_shards( + list_item.children, # pyright: ignore[reportArgumentType] + list_item_start_line, + list_item_end_line, + ) + if list_item_shard is not None: + children.append(list_item_shard) + tags.extend(list_item_tags) + + elif isinstance(block_token, (Paragraph, Heading)): + markers, tags = extract_markers_and_tags(block_token) + + if len(markers) == 0 and len(children) == 0: + return None, tags + + return build_shard( + start_line, end_line, markers=markers, tags=tags, children=children + ), [] + + +def parse_multiple_block_shards( + block_tokens: list[BlockToken], + start_line: int, + end_line: int, + enforce_shard: bool = False, +) -> tuple[Shard | None, list[str]]: + is_first_block_heading = isinstance(block_tokens[0], Heading) and has_markers( + block_tokens[0] + ) + + paragraph_positions = find_paragraph_shard_positions(block_tokens) + children: list[Shard] = [] + tags: list[str] = [] + + is_first_block_only_with_marker = False + + for i, token in enumerate(block_tokens): + if i in paragraph_positions: + is_first_block_only_with_marker = i == 0 + + child_start_line = get_line_number(token) + child_end_line = ( + get_line_number(block_tokens[i + 1]) - 1 + if i + 1 < len(block_tokens) + else end_line + ) + + child_shard, child_tags = parse_single_block_shards( + token, child_start_line, child_end_line + ) + + if child_shard is not None: + children.append(child_shard) + if len(child_tags) > 0: + tags.extend(child_tags) + + if len(children) == 0 and not enforce_shard: + return None, tags + if is_first_block_heading or is_first_block_only_with_marker: + return merge_into_first_shard(children, start_line, end_line, tags), [] + else: + return build_shard(start_line, end_line, tags=tags, children=children), [] + + +def parse_header_shards( + block_tokens: list[BlockToken], + start_line: int, + end_line: int, + use_first_child_as_header: bool = False, +) -> Shard | None: + if len(block_tokens) == 0: + return build_shard(start_line, end_line) + + split_at_heading_level = calculate_heading_level_for_next_split(block_tokens) + + if split_at_heading_level is None: + return parse_multiple_block_shards( + block_tokens, start_line, end_line, enforce_shard=True + )[0] + + heading_positions = find_headings_by_level(block_tokens, split_at_heading_level) + + block_tokens_split_by_heading = split_at(block_tokens, heading_positions) + + children: list[Shard] = [] + for i, child_blocks in enumerate(block_tokens_split_by_heading): + child_start_line = get_line_number(child_blocks[0]) + child_end_line = ( + get_line_number(block_tokens_split_by_heading[i + 1][0]) - 1 + if i + 1 < len(block_tokens_split_by_heading) + else end_line + ) + if child_shard := parse_header_shards( + child_blocks, + child_start_line, + child_end_line, + use_first_child_as_header=i > 0 or 0 in heading_positions, + ): + children.append(child_shard) + + if use_first_child_as_header and len(children) > 0: + return merge_into_first_shard(children, start_line, end_line) + else: + return build_shard(start_line, end_line, children=children) + + +def parse_markdown_file(file_name: str, file_content: str) -> StreamFile: + shard = build_shard(1, max([len(file_content.splitlines()), 1])) + + with TagMarkdownRenderer(): + ast = Document(file_content) + + block_tokens: list[BlockToken] = ast.children # pyright: ignore[reportAssignmentType] + if len(block_tokens) > 0: + if parsed_shard := parse_header_shards( + block_tokens, shard.start_line, shard.end_line + ): + shard = parsed_shard + + return StreamFile(shard=shard, file_name=file_name) + + +__all__ = ["Shard", "StreamFile", "parse_markdown_file"] diff --git a/src/streamd/query/find.py b/src/streamd/query/find.py new file mode 100644 index 0000000..428e05a --- /dev/null +++ b/src/streamd/query/find.py @@ -0,0 +1,36 @@ +from typing import Callable + +from streamd.localize import LocalizedShard + + +def find_shard( + shards: list[LocalizedShard], query_function: Callable[[LocalizedShard], bool] +) -> list[LocalizedShard]: + found_shards: list[LocalizedShard] = [] + + for shard in shards: + if query_function(shard): + found_shards.append(shard) + found_shards.extend(find_shard(shard.children, query_function)) + + return found_shards + + +def find_shard_by_position( + shards: list[LocalizedShard], dimension: str, value: str +) -> list[LocalizedShard]: + return find_shard( + shards, + lambda shard: ( + dimension in shard.location and shard.location[dimension] == value + ), + ) + + +def find_shard_by_set_dimension( + shards: list[LocalizedShard], dimension: str +) -> list[LocalizedShard]: + return find_shard(shards, lambda shard: dimension in shard.location) + + +__all__ = ["find_shard_by_position", "find_shard", "find_shard_by_set_dimension"] diff --git a/src/streamd/settings/__init__.py b/src/streamd/settings/__init__.py new file mode 100644 index 0000000..bdf28b4 --- /dev/null +++ b/src/streamd/settings/__init__.py @@ -0,0 +1,38 @@ +import os +from typing import ClassVar, override + +from pydantic_settings import ( + BaseSettings, + PydanticBaseSettingsSource, + SettingsConfigDict, + YamlConfigSettingsSource, +) +from xdg_base_dirs import xdg_config_home + +SETTINGS_FILE = xdg_config_home() / "streamd" / "config.yaml" + + +class Settings(BaseSettings): + model_config: ClassVar[SettingsConfigDict] = SettingsConfigDict( + env_file_encoding="utf-8" + ) + + base_folder: str = os.getcwd() + + @classmethod + @override + def settings_customise_sources( + cls, + settings_cls: type[BaseSettings], + init_settings: PydanticBaseSettingsSource, + env_settings: PydanticBaseSettingsSource, + dotenv_settings: PydanticBaseSettingsSource, + file_secret_settings: PydanticBaseSettingsSource, + ) -> tuple[PydanticBaseSettingsSource, ...]: + return ( + init_settings, + YamlConfigSettingsSource(settings_cls, yaml_file=SETTINGS_FILE), + dotenv_settings, + env_settings, + file_secret_settings, + ) diff --git a/test/localize/test_repository_configuration_merge.py b/test/localize/test_repository_configuration_merge.py index f5d345d..ba74c76 100644 --- a/test/localize/test_repository_configuration_merge.py +++ b/test/localize/test_repository_configuration_merge.py @@ -1,6 +1,6 @@ import pytest -from streamer.localize.repository_configuration import ( +from streamd.localize.repository_configuration import ( Dimension, Marker, MarkerPlacement, @@ -252,8 +252,8 @@ class TestMergeRepositoryConfiguration: ), }, markers={ - "Streamer": Marker( - display_name="Streamer", + "Streamd": Marker( + display_name="Streamd", placements=[MarkerPlacement(dimension="project")], ) }, @@ -267,8 +267,8 @@ class TestMergeRepositoryConfiguration: ), }, markers={ - "Streamer": Marker( - display_name="Streamer2", + "Streamd": Marker( + display_name="Streamd2", placements=[ MarkerPlacement( if_with={"Timesheet"}, dimension="timesheet", value="coding" @@ -291,9 +291,9 @@ class TestMergeRepositoryConfiguration: assert merged.dimensions["moment"].display_name == "Moment" assert merged.dimensions["timesheet"].display_name == "Timesheet" - assert set(merged.markers.keys()) == {"Streamer", "JobHunting"} - assert merged.markers["Streamer"].display_name == "Streamer2" - assert merged.markers["Streamer"].placements == [ + assert set(merged.markers.keys()) == {"Streamd", "JobHunting"} + assert merged.markers["Streamd"].display_name == "Streamd2" + assert merged.markers["Streamd"].placements == [ MarkerPlacement(dimension="project", value=None, if_with=set()), MarkerPlacement( if_with={"Timesheet"}, dimension="timesheet", value="coding" @@ -359,7 +359,9 @@ class TestMergeRepositoryConfiguration: ], ) def test_merge_repository_configuration_propagate_preserves_base_when_omitted( - base, second, expected_propagate + base: RepositoryConfiguration, + second: RepositoryConfiguration, + expected_propagate: bool, ): merged = merge_repository_configuration(base, second) assert merged.dimensions["d"].propagate is expected_propagate diff --git a/test/timesheet/test_extract_timesheets.py b/test/timesheet/test_extract_timesheets.py index c5a0bbe..b54befc 100644 --- a/test/timesheet/test_extract_timesheets.py +++ b/test/timesheet/test_extract_timesheets.py @@ -4,13 +4,13 @@ from datetime import datetime, time import pytest -from streamer.localize.localized_shard import LocalizedShard -from streamer.timesheet.configuration import ( +from streamd.localize.localized_shard import LocalizedShard +from streamd.timesheet.configuration import ( TIMESHEET_DIMENSION_NAME, TimesheetPointType, ) -from streamer.timesheet.extract import extract_timesheets -from streamer.timesheet.timecard import SpecialDayType, Timecard, Timesheet +from streamd.timesheet.extract import extract_timesheets +from streamd.timesheet.timecard import SpecialDayType, Timecard, Timesheet def point(at: datetime, type: TimesheetPointType) -> LocalizedShard: @@ -243,7 +243,7 @@ class TestExtractTimesheets: ] with pytest.raises(ValueError, match=r"Last Timecard of .* is not a break"): - extract_timesheets(shards) + _ = extract_timesheets(shards) def test_two_special_day_types_same_day_is_invalid(self): """ @@ -257,7 +257,7 @@ class TestExtractTimesheets: ] with pytest.raises(ValueError, match=r"is both .* and .*"): - extract_timesheets(shards) + _ = extract_timesheets(shards) def test_points_with_mixed_dates_inside_one_group_raises(self): """ @@ -273,7 +273,7 @@ class TestExtractTimesheets: ] with pytest.raises(ValueError, match=r"Last Timecard of .* is not a break"): - extract_timesheets(shards) + _ = extract_timesheets(shards) def test_day_with_only_breaks_is_ignored(self): """ From af2debc19b03c515dde7057c73bf2389a167870b Mon Sep 17 00:00:00 2001 From: Konstantin Fickel Date: Sun, 15 Feb 2026 17:30:31 +0100 Subject: [PATCH 07/20] refactor!: rename package from streamer to streamd - Rename src/streamer/ to src/streamd/ - Update all internal imports - Update pyproject.toml project name and entry point - Update README branding (Streamer -> Strea.md) - Switch from pyright to basedpyright - Bump requires-python to >=3.13 --- .python-version | 2 +- README.md | 20 +- pyproject.toml | 8 +- .../localize/__init__.py | 0 .../localize/extract_datetime.py | 0 .../localize/localized_shard.py | 2 +- .../localize/preconfigured_configurations.py | 2 +- src/{streamer => streamd}/parse/__init__.py | 0 src/{streamer => streamd}/parse/list.py | 0 src/{streamer => streamd}/parse/shard.py | 0 src/{streamer => streamd}/query/__init__.py | 0 .../timesheet/configuration.py | 4 +- .../timesheet/extract.py | 5 +- .../timesheet/timecard.py | 0 src/streamer/__init__.py | 126 --------- src/streamer/localize/localize.py | 70 ----- .../localize/repository_configuration.py | 108 -------- src/streamer/parse/extract_tag.py | 84 ------ src/streamer/parse/markdown_tag.py | 20 -- src/streamer/parse/parse.py | 242 ------------------ src/streamer/query/find.py | 35 --- src/streamer/settings/__init__.py | 33 --- uv.lock | 76 ++---- 23 files changed, 48 insertions(+), 789 deletions(-) rename src/{streamer => streamd}/localize/__init__.py (100%) rename src/{streamer => streamd}/localize/extract_datetime.py (100%) rename src/{streamer => streamd}/localize/localized_shard.py (87%) rename src/{streamer => streamd}/localize/preconfigured_configurations.py (95%) rename src/{streamer => streamd}/parse/__init__.py (100%) rename src/{streamer => streamd}/parse/list.py (100%) rename src/{streamer => streamd}/parse/shard.py (100%) rename src/{streamer => streamd}/query/__init__.py (100%) rename src/{streamer => streamd}/timesheet/configuration.py (96%) rename src/{streamer => streamd}/timesheet/extract.py (97%) rename src/{streamer => streamd}/timesheet/timecard.py (100%) delete mode 100644 src/streamer/__init__.py delete mode 100644 src/streamer/localize/localize.py delete mode 100644 src/streamer/localize/repository_configuration.py delete mode 100644 src/streamer/parse/extract_tag.py delete mode 100644 src/streamer/parse/markdown_tag.py delete mode 100644 src/streamer/parse/parse.py delete mode 100644 src/streamer/query/find.py delete mode 100644 src/streamer/settings/__init__.py diff --git a/.python-version b/.python-version index da71773..24ee5b1 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.14.3 +3.13 diff --git a/README.md b/README.md index fca374d..5389e0b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# streamer +# strea.md -Streamer is a personal knowledge management and time-tracking CLI tool. It organizes time-ordered markdown files using `@tag` annotations, letting you manage tasks, track time, and query your notes from the terminal. +Strea.md is a personal knowledge management and time-tracking CLI tool. It organizes time-ordered markdown files using `@tag` annotations, letting you manage tasks, track time, and query your notes from the terminal. ## Core Concepts @@ -12,23 +12,23 @@ Streamer is a personal knowledge management and time-tracking CLI tool. It organ Markdown files are named with a timestamp: `YYYYMMDD-HHMMSS [markers].md` -For example: `20260131-210000 Task Streamer.md` +For example: `20260131-210000 Task Streamd.md` Within files, `@`-prefixed markers at the beginning of paragraphs or headings define how a shard is categorized. ## Commands -- `streamer` / `streamer new` — Create a new timestamped markdown entry, opening your editor -- `streamer todo` — Show all open tasks (shards with `@Task` markers) -- `streamer edit [number]` — Edit a stream file by index (most recent first) -- `streamer timesheet` — Generate time reports from `@Timesheet` markers +- `streamd` / `streamd new` — Create a new timestamped markdown entry, opening your editor +- `streamd todo` — Show all open tasks (shards with `@Task` markers) +- `streamd edit [number]` — Edit a stream file by index (most recent first) +- `streamd timesheet` — Generate time reports from `@Timesheet` markers ## Configuration -Streamer reads its configuration from `~/.config/streamer/config.yaml` (XDG standard). The main setting is `base_folder`, which points to the directory containing your stream files (defaults to the current working directory). +Streamd reads its configuration from `~/.config/streamd/config.yaml` (XDG standard). The main setting is `base_folder`, which points to the directory containing your stream files (defaults to the current working directory). ## Usage -Running `streamer` opens your editor to create a new entry. After saving, the file is renamed based on its timestamp and any markers found in the content. +Running `streamd` opens your editor to create a new entry. After saving, the file is renamed based on its timestamp and any markers found in the content. -Running `streamer todo` finds all shards marked as open tasks and displays them as rich-formatted panels in your terminal. +Running `streamd todo` finds all shards marked as open tasks and displays them as rich-formatted panels in your terminal. diff --git a/pyproject.toml b/pyproject.toml index 7edb03b..4cd209f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,9 @@ [project] -name = "streamer" +name = "streamd" version = "0.1.0" description = "Searching for tags in streams" readme = "README.md" -requires-python = ">=3.12" +requires-python = ">=3.13" dependencies = [ "click==8.3.1", "mistletoe==1.5.1", @@ -15,7 +15,7 @@ dependencies = [ ] [project.scripts] -streamer = "streamer:app" +streamd = "streamd:app" [build-system] requires = ["hatchling"] @@ -23,8 +23,8 @@ build-backend = "hatchling.build" [dependency-groups] dev = [ + "basedpyright==1.38.0", "faker==40.4.0", - "pyright==1.1.408", "pytest==9.0.2", "ruff==0.15.1", ] diff --git a/src/streamer/localize/__init__.py b/src/streamd/localize/__init__.py similarity index 100% rename from src/streamer/localize/__init__.py rename to src/streamd/localize/__init__.py diff --git a/src/streamer/localize/extract_datetime.py b/src/streamd/localize/extract_datetime.py similarity index 100% rename from src/streamer/localize/extract_datetime.py rename to src/streamd/localize/extract_datetime.py diff --git a/src/streamer/localize/localized_shard.py b/src/streamd/localize/localized_shard.py similarity index 87% rename from src/streamer/localize/localized_shard.py rename to src/streamd/localize/localized_shard.py index 05cb016..871af20 100644 --- a/src/streamer/localize/localized_shard.py +++ b/src/streamd/localize/localized_shard.py @@ -2,7 +2,7 @@ from __future__ import annotations from datetime import datetime -from streamer.parse.shard import Shard +from streamd.parse.shard import Shard class LocalizedShard(Shard): diff --git a/src/streamer/localize/preconfigured_configurations.py b/src/streamd/localize/preconfigured_configurations.py similarity index 95% rename from src/streamer/localize/preconfigured_configurations.py rename to src/streamd/localize/preconfigured_configurations.py index 0da82d7..a949ddc 100644 --- a/src/streamer/localize/preconfigured_configurations.py +++ b/src/streamd/localize/preconfigured_configurations.py @@ -1,4 +1,4 @@ -from streamer.localize.repository_configuration import ( +from streamd.localize.repository_configuration import ( Dimension, Marker, MarkerPlacement, diff --git a/src/streamer/parse/__init__.py b/src/streamd/parse/__init__.py similarity index 100% rename from src/streamer/parse/__init__.py rename to src/streamd/parse/__init__.py diff --git a/src/streamer/parse/list.py b/src/streamd/parse/list.py similarity index 100% rename from src/streamer/parse/list.py rename to src/streamd/parse/list.py diff --git a/src/streamer/parse/shard.py b/src/streamd/parse/shard.py similarity index 100% rename from src/streamer/parse/shard.py rename to src/streamd/parse/shard.py diff --git a/src/streamer/query/__init__.py b/src/streamd/query/__init__.py similarity index 100% rename from src/streamer/query/__init__.py rename to src/streamd/query/__init__.py diff --git a/src/streamer/timesheet/configuration.py b/src/streamd/timesheet/configuration.py similarity index 96% rename from src/streamer/timesheet/configuration.py rename to src/streamd/timesheet/configuration.py index 6b6c55d..8453dc9 100644 --- a/src/streamer/timesheet/configuration.py +++ b/src/streamd/timesheet/configuration.py @@ -1,7 +1,7 @@ from enum import StrEnum -from streamer.localize import RepositoryConfiguration -from streamer.localize.repository_configuration import ( +from streamd.localize import RepositoryConfiguration +from streamd.localize.repository_configuration import ( Dimension, Marker, MarkerPlacement, diff --git a/src/streamer/timesheet/extract.py b/src/streamd/timesheet/extract.py similarity index 97% rename from src/streamer/timesheet/extract.py rename to src/streamd/timesheet/extract.py index 85547cd..0754f7d 100644 --- a/src/streamer/timesheet/extract.py +++ b/src/streamd/timesheet/extract.py @@ -2,9 +2,8 @@ from datetime import datetime from itertools import groupby from pydantic import BaseModel - -from streamer.localize import LocalizedShard -from streamer.query.find import find_shard_by_set_dimension +from streamd.localize import LocalizedShard +from streamd.query.find import find_shard_by_set_dimension from .configuration import TIMESHEET_DIMENSION_NAME, TimesheetPointType from .timecard import SpecialDayType, Timecard, Timesheet diff --git a/src/streamer/timesheet/timecard.py b/src/streamd/timesheet/timecard.py similarity index 100% rename from src/streamer/timesheet/timecard.py rename to src/streamd/timesheet/timecard.py diff --git a/src/streamer/__init__.py b/src/streamer/__init__.py deleted file mode 100644 index 6109ca8..0000000 --- a/src/streamer/__init__.py +++ /dev/null @@ -1,126 +0,0 @@ -import glob -import os -from datetime import datetime -from shutil import move -from typing import Annotated, Generator - -import click -import typer -from rich import print -from rich.markdown import Markdown -from rich.panel import Panel - -from streamer.localize import ( - LocalizedShard, - RepositoryConfiguration, - localize_stream_file, -) -from streamer.localize.preconfigured_configurations import TaskConfiguration -from streamer.parse import parse_markdown_file -from streamer.query import find_shard_by_position -from streamer.query.find import find_shard_by_set_dimension -from streamer.settings import Settings -from streamer.timesheet.configuration import BasicTimesheetConfiguration -from streamer.timesheet.extract import extract_timesheets - -app = typer.Typer() - - -def all_files(config: RepositoryConfiguration) -> Generator[LocalizedShard]: - for file_name in glob.glob(f"{glob.escape(Settings().base_folder)}/*.md"): - with open(file_name, "r") as file: - file_content = file.read() - if shard := localize_stream_file( - parse_markdown_file(file_name, file_content), config - ): - yield shard - - -@app.command() -def todo() -> None: - all_shards = list(all_files(TaskConfiguration)) - - for task_shard in find_shard_by_position(all_shards, "task", "open"): - with open(task_shard.location["file"], "r") as file: - file_content = file.read().splitlines() - print( - Panel( - Markdown( - "\n".join( - file_content[ - task_shard.start_line - 1 : task_shard.end_line - ] - ) - ), - title=f"{task_shard.location['file']}:{task_shard.start_line}", - ) - ) - - -@app.command() -def edit(number: Annotated[int, typer.Argument()] = 1) -> None: - all_shards = list(all_files(TaskConfiguration)) - sorted_shards = sorted(all_shards, key=lambda s: s.moment) - - if abs(number) >= len(sorted_shards): - raise ValueError("Argument out of range") - - selected_number = number - if selected_number >= 0: - selected_number = len(sorted_shards) - selected_number - else: - selected_number = -selected_number - - click.edit(None, filename=sorted_shards[selected_number].location["file"]) - - -@app.command() -def timesheet() -> None: - all_shards = list(all_files(BasicTimesheetConfiguration)) - sheets = sorted(extract_timesheets(all_shards), key=lambda card: card.date) - for sheet in sheets: - print(sheet.date) - print( - ",".join( - map(lambda card: f"{card.from_time},{card.to_time}", sheet.timecards) - ), - ) - - -@app.command() -def new() -> None: - streamer_directory = Settings().base_folder - - timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") - preliminary_file_name = f"{timestamp}_wip.md" - prelimary_path = os.path.join(streamer_directory, preliminary_file_name) - - content = "# " - with open(prelimary_path, "w") as file: - _ = file.write(content) - - click.edit(None, filename=prelimary_path) - - with open(prelimary_path, "r") as file: - content = file.read() - parsed_content = parse_markdown_file(prelimary_path, content) - - final_file_name = f"{timestamp}.md" - if parsed_content.shard is not None and len( - markers := parsed_content.shard.markers - ): - final_file_name = f"{timestamp} {' '.join(markers)}.md" - - final_path = os.path.join(streamer_directory, final_file_name) - _ = move(prelimary_path, final_path) - print(f"Saved as [yellow]{final_file_name}") - - -@app.callback(invoke_without_command=True) -def main(ctx: typer.Context): - if ctx.invoked_subcommand is None: - new() - - -if __name__ == "__main__": - app() diff --git a/src/streamer/localize/localize.py b/src/streamer/localize/localize.py deleted file mode 100644 index b241fc9..0000000 --- a/src/streamer/localize/localize.py +++ /dev/null @@ -1,70 +0,0 @@ -from datetime import datetime - -from streamer.parse.shard import Shard, StreamFile - -from .extract_datetime import ( - extract_datetime_from_file_name, - extract_datetime_from_marker_list, -) -from .localized_shard import LocalizedShard -from .repository_configuration import RepositoryConfiguration - - -def localize_shard( - shard: Shard, - config: RepositoryConfiguration, - propagated: dict[str, str], - moment: datetime, -) -> LocalizedShard: - position = {**propagated} - private_position: dict[str, str] = {} - - adjusted_moment: datetime = extract_datetime_from_marker_list(shard.markers, moment) - - for marker in shard.markers: - if marker in config.markers: - marker_definition = config.markers[marker] - for placement in marker_definition.placements: - if placement.if_with <= set(shard.markers): - dimension = config.dimensions[placement.dimension] - - value = placement.value or marker - - if placement.overwrites or ( - placement.dimension not in position - and placement.dimension not in private_position - ): - if dimension.propagate: - position[placement.dimension] = value - else: - private_position[placement.dimension] = value - - children = [ - localize_shard(child, config, position, adjusted_moment) - for child in shard.children - ] - - position.update(private_position) - - return LocalizedShard( - **shard.model_dump(exclude={"children"}), - location=position, - children=children, - moment=adjusted_moment, - ) - - -def localize_stream_file( - stream_file: StreamFile, config: RepositoryConfiguration -) -> LocalizedShard | None: - shard_date = extract_datetime_from_file_name(stream_file.file_name) - - if not shard_date or not stream_file.shard: - raise ValueError("Could not extract date") - - return localize_shard( - stream_file.shard, config, {"file": stream_file.file_name}, shard_date - ) - - -__all__ = ["localize_stream_file"] diff --git a/src/streamer/localize/repository_configuration.py b/src/streamer/localize/repository_configuration.py deleted file mode 100644 index f21c556..0000000 --- a/src/streamer/localize/repository_configuration.py +++ /dev/null @@ -1,108 +0,0 @@ -from __future__ import annotations - -from typing import Optional - -from pydantic import BaseModel - - -class Dimension(BaseModel): - display_name: str - comment: Optional[str] = None - propagate: bool = False - - -class MarkerPlacement(BaseModel): - if_with: set[str] = set() - dimension: str - value: str | None = None - overwrites: bool = True - - -class Marker(BaseModel): - display_name: str - placements: list[MarkerPlacement] = [] - - -class RepositoryConfiguration(BaseModel): - dimensions: dict[str, Dimension] - markers: dict[str, Marker] - - -def merge_single_dimension(base: Dimension, second: Dimension) -> Dimension: - second_fields_set = getattr(second, "model_fields_set", set()) - - return Dimension( - display_name=second.display_name or base.display_name, - comment=base.comment if second.comment is None else second.comment, - propagate=second.propagate - if "propagate" in second_fields_set - else base.propagate, - ) - - -def merge_dimensions( - base: dict[str, Dimension], second: dict[str, Dimension] -) -> dict[str, Dimension]: - merged: dict[str, Dimension] = dict(base) - for key, second_dimension in second.items(): - if key in merged: - merged[key] = merge_single_dimension(merged[key], second_dimension) - else: - merged[key] = second_dimension - return merged - - -def _placement_identity(p: MarkerPlacement) -> tuple[frozenset[str], str]: - return (frozenset(p.if_with), p.dimension) - - -def merge_single_marker(base: Marker, second: Marker) -> Marker: - merged_display_name = second.display_name or base.display_name - - merged_placements: list[MarkerPlacement] = [] - seen: dict[tuple[frozenset[str], str], int] = {} - - for placement in base.placements: - ident = _placement_identity(placement) - seen[ident] = len(merged_placements) - merged_placements.append(placement) - - for placement in second.placements: - ident = _placement_identity(placement) - if ident in seen: - merged_placements[seen[ident]] = placement - else: - seen[ident] = len(merged_placements) - merged_placements.append(placement) - - return Marker(display_name=merged_display_name, placements=merged_placements) - - -def merge_markers( - base: dict[str, Marker], second: dict[str, Marker] -) -> dict[str, Marker]: - merged: dict[str, Marker] = dict(base) - for key, second_marker in second.items(): - if key in merged: - merged[key] = merge_single_marker(merged[key], second_marker) - else: - merged[key] = second_marker - return merged - - -def merge_repository_configuration( - base: RepositoryConfiguration, second: RepositoryConfiguration -) -> RepositoryConfiguration: - return RepositoryConfiguration( - dimensions=merge_dimensions(base.dimensions, second.dimensions), - markers=merge_markers(base.markers, second.markers), - ) - - -__all__ = [ - "Dimension", - "Marker", - "MarkerPlacement", - "RepositoryConfiguration", - "merge_repository_configuration", -] diff --git a/src/streamer/parse/extract_tag.py b/src/streamer/parse/extract_tag.py deleted file mode 100644 index ace1258..0000000 --- a/src/streamer/parse/extract_tag.py +++ /dev/null @@ -1,84 +0,0 @@ -import re -from typing import Iterable -from mistletoe.block_token import BlockToken -from mistletoe.span_token import Emphasis, RawText, Strikethrough, Strong, Link -from mistletoe.token import Token - -from .markdown_tag import Tag - - -def extract_markers_and_tags_from_single_token( - token: Token, - marker_boundary_encountered: bool, - return_at_first_marker: bool = False, -) -> tuple[list[str], list[str], bool]: - result_markers, result_tags = [], [] - result_marker_boundary_encountered = marker_boundary_encountered - - if isinstance(token, Tag): - if marker_boundary_encountered: - result_tags.append(token.content) - else: - result_markers.append(token.content) - elif isinstance(token, (Emphasis, Strong, Strikethrough, Link)): - markers, tags, child_marker_boundary_encountered = ( - extract_markers_and_tags_from_tokens( - token.children or [], - marker_boundary_encountered, - return_at_first_marker, - ) - ) - result_markers.extend(markers) - result_tags.extend(tags) - result_marker_boundary_encountered = ( - marker_boundary_encountered or child_marker_boundary_encountered - ) - elif isinstance(token, RawText) and re.match(r"^[\s]*$", token.content): - pass - else: - result_marker_boundary_encountered = True - - return result_markers, result_tags, result_marker_boundary_encountered - - -def extract_markers_and_tags_from_tokens( - tokens: Iterable[Token], - marker_boundary_encountered: bool, - return_at_first_marker: bool = False, -) -> tuple[list[str], list[str], bool]: - result_markers, result_tags = [], [] - result_marker_boundary_encountered = marker_boundary_encountered - - for child in tokens: - markers, tags, child_marker_boundary_encountered = ( - extract_markers_and_tags_from_single_token( - child, result_marker_boundary_encountered, return_at_first_marker - ) - ) - result_markers.extend(markers) - result_tags.extend(tags) - result_marker_boundary_encountered = ( - marker_boundary_encountered or child_marker_boundary_encountered - ) - - if len(result_markers) > 0 and return_at_first_marker: - break - - return result_markers, result_tags, result_marker_boundary_encountered - - -def extract_markers_and_tags(block_token: BlockToken) -> tuple[list[str], list[str]]: - markers, tags, _ = extract_markers_and_tags_from_tokens( - block_token.children or [], False - ) - return markers, tags - - -def has_markers(block_token: BlockToken) -> bool: - markers, _, _ = extract_markers_and_tags_from_tokens( - block_token.children or [], False, return_at_first_marker=True - ) - return len(markers) > 0 - - -__all__ = ["extract_markers_and_tags", "has_markers"] diff --git a/src/streamer/parse/markdown_tag.py b/src/streamer/parse/markdown_tag.py deleted file mode 100644 index 4de6d35..0000000 --- a/src/streamer/parse/markdown_tag.py +++ /dev/null @@ -1,20 +0,0 @@ -import re -from mistletoe.markdown_renderer import Fragment, MarkdownRenderer -from mistletoe.span_token import SpanToken - - -class Tag(SpanToken): - parse_inner = False - pattern = re.compile(r"@([^\s*\x60~\[\]]+)") - - -class TagMarkdownRenderer(MarkdownRenderer): - def __init__(self): - super().__init__(Tag) - - def render_tag(self, token: Tag): - yield Fragment("@") - yield Fragment(token.content) - - -__all__ = ["Tag", "TagMarkdownRenderer"] diff --git a/src/streamer/parse/parse.py b/src/streamer/parse/parse.py deleted file mode 100644 index 058912a..0000000 --- a/src/streamer/parse/parse.py +++ /dev/null @@ -1,242 +0,0 @@ -from collections import Counter - -from mistletoe.block_token import ( - BlockToken, - Document, - Heading, - List, - ListItem, - Paragraph, -) - -from .extract_tag import extract_markers_and_tags, has_markers -from .list import split_at -from .markdown_tag import TagMarkdownRenderer -from .shard import Shard, StreamFile - - -def get_line_number(block_token: BlockToken) -> int: - return block_token.line_number # type: ignore - - -def build_shard( - start_line: int, - end_line: int, - markers: list[str] = [], - tags: list[str] = [], - children: list[Shard] = [], -) -> Shard: - if ( - len(children) == 1 - and len(tags) == 0 - and len(markers) == 0 - and children[0].start_line == start_line - and children[0].end_line == end_line - ): - return children[0] - - return Shard( - markers=markers, - tags=tags, - children=children, - start_line=start_line, - end_line=end_line, - ) - - -def merge_into_first_shard( - shards: list[Shard], start_line: int, end_line: int, additional_tags: list[str] = [] -): - return shards[0].model_copy( - update={ - "start_line": start_line, - "end_line": end_line, - "children": shards[1:], - "tags": shards[0].tags + additional_tags, - } - ) - - -def find_paragraph_shard_positions(block_tokens: list[BlockToken]) -> list[int]: - return [ - index - for index, block_token in enumerate(block_tokens) - if isinstance(block_token, Paragraph) and has_markers(block_token) - ] - - -def find_headings_by_level( - block_tokens: list[BlockToken], header_level: int -) -> list[int]: - return [ - index - for index, block_token in enumerate(block_tokens) - if isinstance(block_token, Heading) and block_token.level == header_level - ] - - -def calculate_heading_level_for_next_split( - block_tokens: list[BlockToken], -) -> int | None: - """ - If there is no marker in any heading, then return None. - If only the first token is a heading with a marker, then return None. - Otherwise: Return the heading level with the lowest level (h1 < h2), of which there are two or which has a marker (and doesn't stem from first) - """ - level_of_headings_without_first_with_marker = [ - token.level - for token in block_tokens[1:] - if isinstance(token, Heading) and has_markers(token) - ] - - if len(level_of_headings_without_first_with_marker) == 0: - return None - - heading_level_counter = Counter( - [token.level for token in block_tokens if isinstance(token, Heading)] - ) - - return min( - [level for level, count in heading_level_counter.items() if count >= 2] - + level_of_headings_without_first_with_marker - ) - - -def parse_single_block_shards( - block_token: BlockToken, start_line: int, end_line: int -) -> tuple[Shard | None, list[str]]: - markers, tags, children = [], [], [] - - if isinstance(block_token, List): - list_items: list[ListItem] = ( # type: ignore - list(block_token.children) if block_token.children is not None else [] - ) - for index, list_item in enumerate(list_items): - list_item_start_line = get_line_number(list_item) - list_item_end_line = ( - get_line_number(list_items[index + 1]) - 1 - if index + 1 < len(list_items) - else end_line - ) - list_item_shard, list_item_tags = parse_multiple_block_shards( - list_item.children, # type: ignore - list_item_start_line, - list_item_end_line, - ) - if list_item_shard is not None: - children.append(list_item_shard) - tags.extend(list_item_tags) - - elif isinstance(block_token, (Paragraph, Heading)): - markers, tags = extract_markers_and_tags(block_token) - - if len(markers) == 0 and len(children) == 0: - return None, tags - - return build_shard( - start_line, end_line, markers=markers, tags=tags, children=children - ), [] - - -def parse_multiple_block_shards( - block_tokens: list[BlockToken], - start_line: int, - end_line: int, - enforce_shard: bool = False, -) -> tuple[Shard | None, list[str]]: - is_first_block_heading = isinstance(block_tokens[0], Heading) and has_markers( - block_tokens[0] - ) - - paragraph_positions = find_paragraph_shard_positions(block_tokens) - children, tags = [], [] - - is_first_block_only_with_marker = False - - for i, token in enumerate(block_tokens): - if i in paragraph_positions: - is_first_block_only_with_marker = i == 0 - - child_start_line = get_line_number(token) - child_end_line = ( - get_line_number(block_tokens[i + 1]) - 1 - if i + 1 < len(block_tokens) - else end_line - ) - - child_shard, child_tags = parse_single_block_shards( - token, child_start_line, child_end_line - ) - - if child_shard is not None: - children.append(child_shard) - if len(child_tags) > 0: - tags.extend(child_tags) - - if len(children) == 0 and not enforce_shard: - return None, tags - if is_first_block_heading or is_first_block_only_with_marker: - return merge_into_first_shard(children, start_line, end_line, tags), [] - else: - return build_shard(start_line, end_line, tags=tags, children=children), [] - - -def parse_header_shards( - block_tokens: list[BlockToken], - start_line: int, - end_line: int, - use_first_child_as_header: bool = False, -) -> Shard | None: - if len(block_tokens) == 0: - return build_shard(start_line, end_line) - - split_at_heading_level = calculate_heading_level_for_next_split(block_tokens) - - if split_at_heading_level is None: - return parse_multiple_block_shards( - block_tokens, start_line, end_line, enforce_shard=True - )[0] - - heading_positions = find_headings_by_level(block_tokens, split_at_heading_level) - - block_tokens_split_by_heading = split_at(block_tokens, heading_positions) - - children = [] - for i, child_blocks in enumerate(block_tokens_split_by_heading): - child_start_line = get_line_number(child_blocks[0]) - child_end_line = ( - get_line_number(block_tokens_split_by_heading[i + 1][0]) - 1 - if i + 1 < len(block_tokens_split_by_heading) - else end_line - ) - if child_shard := parse_header_shards( - child_blocks, - child_start_line, - child_end_line, - use_first_child_as_header=i > 0 or 0 in heading_positions, - ): - children.append(child_shard) - - if use_first_child_as_header and len(children) > 0: - return merge_into_first_shard(children, start_line, end_line) - else: - return build_shard(start_line, end_line, children=children) - - -def parse_markdown_file(file_name: str, file_content: str) -> StreamFile: - shard = build_shard(1, max([len(file_content.splitlines()), 1])) - - with TagMarkdownRenderer(): - ast = Document(file_content) - - block_tokens: list[BlockToken] = ast.children # type: ignore - if len(block_tokens) > 0: - if parsed_shard := parse_header_shards( - block_tokens, shard.start_line, shard.end_line - ): - shard = parsed_shard - - return StreamFile(shard=shard, file_name=file_name) - - -__all__ = ["Shard", "StreamFile", "parse_markdown_file"] diff --git a/src/streamer/query/find.py b/src/streamer/query/find.py deleted file mode 100644 index 49beeca..0000000 --- a/src/streamer/query/find.py +++ /dev/null @@ -1,35 +0,0 @@ -from typing import Callable - -from streamer.localize import LocalizedShard - - -def find_shard( - shards: list[LocalizedShard], query_function: Callable[[LocalizedShard], bool] -) -> list[LocalizedShard]: - found_shards = [] - - for shard in shards: - if query_function(shard): - found_shards.append(shard) - found_shards.extend(find_shard(shard.children, query_function)) - - return found_shards - - -def find_shard_by_position( - shards: list[LocalizedShard], dimension: str, value: str -) -> list[LocalizedShard]: - return find_shard( - shards, - lambda shard: dimension in shard.location - and shard.location[dimension] == value, - ) - - -def find_shard_by_set_dimension( - shards: list[LocalizedShard], dimension: str -) -> list[LocalizedShard]: - return find_shard(shards, lambda shard: dimension in shard.location) - - -__all__ = ["find_shard_by_position", "find_shard", "find_shard_by_set_dimension"] diff --git a/src/streamer/settings/__init__.py b/src/streamer/settings/__init__.py deleted file mode 100644 index 8099b01..0000000 --- a/src/streamer/settings/__init__.py +++ /dev/null @@ -1,33 +0,0 @@ -import os -from pydantic_settings import ( - BaseSettings, - PydanticBaseSettingsSource, - SettingsConfigDict, - YamlConfigSettingsSource, -) -from xdg_base_dirs import xdg_config_home - -SETTINGS_FILE = xdg_config_home() / "streamer" / "config.yaml" - - -class Settings(BaseSettings): - model_config = SettingsConfigDict(env_file_encoding="utf-8") - - base_folder: str = os.getcwd() - - @classmethod - def settings_customise_sources( - cls, - settings_cls: type[BaseSettings], - init_settings: PydanticBaseSettingsSource, - env_settings: PydanticBaseSettingsSource, - dotenv_settings: PydanticBaseSettingsSource, - file_secret_settings: PydanticBaseSettingsSource, - ) -> tuple[PydanticBaseSettingsSource, ...]: - return ( - init_settings, - YamlConfigSettingsSource(settings_cls, yaml_file=SETTINGS_FILE), - dotenv_settings, - env_settings, - file_secret_settings, - ) diff --git a/uv.lock b/uv.lock index 30d9760..f5999a8 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,6 @@ version = 1 revision = 3 -requires-python = ">=3.12" +requires-python = ">=3.13" [[package]] name = "annotated-doc" @@ -20,6 +20,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] +[[package]] +name = "basedpyright" +version = "1.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodejs-wheel-binaries" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/d4/4ac6eeba6cfe2ad8586dcf87fdb9e8b045aa467b559bc2e24e91e84f58b2/basedpyright-1.38.0.tar.gz", hash = "sha256:7a9cf631d7eaf5859022a4352b51ed0e78ce115435a8599402239804000d0cdf", size = 25257385, upload-time = "2026-02-11T16:05:47.834Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/90/1883cec16d667d944b08e8d8909b9b2f46cc1d2b9731e855e3c71f9b0450/basedpyright-1.38.0-py3-none-any.whl", hash = "sha256:a6c11a343fd12a2152a0d721b0e92f54f2e2e3322ee2562197e27dad952f1a61", size = 12303557, upload-time = "2026-02-11T16:05:44.863Z" }, +] + [[package]] name = "click" version = "8.3.1" @@ -93,12 +105,19 @@ wheels = [ ] [[package]] -name = "nodeenv" -version = "1.10.0" +name = "nodejs-wheel-binaries" +version = "24.13.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/d0/81d98b8fddc45332f79d6ad5749b1c7409fb18723545eae75d9b7e0048fb/nodejs_wheel_binaries-24.13.1.tar.gz", hash = "sha256:512659a67449a038231e2e972d49e77049d2cf789ae27db39eff4ab1ca52ac57", size = 8056, upload-time = "2026-02-12T17:31:04.368Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, + { url = "https://files.pythonhosted.org/packages/aa/04/1ffe1838306654fcb50bcf46172567d50c8e27a76f4b9e55a1971fab5c4f/nodejs_wheel_binaries-24.13.1-py2.py3-none-macosx_13_0_arm64.whl", hash = "sha256:360ac9382c651de294c23c4933a02358c4e11331294983f3cf50ca1ac32666b1", size = 54757440, upload-time = "2026-02-12T17:30:35.748Z" }, + { url = "https://files.pythonhosted.org/packages/66/f6/81ad81bc3bd919a20b110130c4fd318c7b6a5abb37eb53daa353ad908012/nodejs_wheel_binaries-24.13.1-py2.py3-none-macosx_13_0_x86_64.whl", hash = "sha256:035b718946793986762cdd50deee7f5f1a8f1b0bad0f0cfd57cad5492f5ea018", size = 54932957, upload-time = "2026-02-12T17:30:40.114Z" }, + { url = "https://files.pythonhosted.org/packages/14/be/8e8a2bd50953c4c5b7e0fca07368d287917b84054dc3c93dd26a2940f0f9/nodejs_wheel_binaries-24.13.1-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:f795e9238438c4225f76fbd01e2b8e1a322116bbd0dc15a7dbd585a3ad97961e", size = 59287257, upload-time = "2026-02-12T17:30:43.781Z" }, + { url = "https://files.pythonhosted.org/packages/58/57/92f6dfa40647702a9fa6d32393ce4595d0fc03c1daa9b245df66cc60e959/nodejs_wheel_binaries-24.13.1-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:978328e3ad522571eb163b042dfbd7518187a13968fe372738f90fdfe8a46afc", size = 59781783, upload-time = "2026-02-12T17:30:47.387Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a5/457b984cf675cf86ace7903204b9c36edf7a2d1b4325ddf71eaf8d1027c7/nodejs_wheel_binaries-24.13.1-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e1dc893df85299420cd2a5feea0c3f8482a719b5f7f82d5977d58718b8b78b5f", size = 61287166, upload-time = "2026-02-12T17:30:50.646Z" }, + { url = "https://files.pythonhosted.org/packages/3c/99/da515f7bc3bce35cfa6005f0e0c4e3c4042a466782b143112eb393b663be/nodejs_wheel_binaries-24.13.1-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0e581ae219a39073dcadd398a2eb648f0707b0f5d68c565586139f919c91cbe9", size = 61870142, upload-time = "2026-02-12T17:30:54.563Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c0/22001d2c96d8200834af7d1de5e72daa3266c7270330275104c3d9ddd143/nodejs_wheel_binaries-24.13.1-py2.py3-none-win_amd64.whl", hash = "sha256:d4c969ea0bcb8c8b20bc6a7b4ad2796146d820278f17d4dc20229b088c833e22", size = 41185473, upload-time = "2026-02-12T17:30:57.524Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c4/7532325f968ecfc078e8a028e69a52e4c3f95fb800906bf6931ac1e89e2b/nodejs_wheel_binaries-24.13.1-py2.py3-none-win_arm64.whl", hash = "sha256:caec398cb9e94c560bacdcba56b3828df22a355749eb291f47431af88cbf26dc", size = 38881194, upload-time = "2026-02-12T17:31:00.214Z" }, ] [[package]] @@ -143,20 +162,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, - { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, - { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, - { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, - { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, - { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, - { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, - { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, - { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, - { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, - { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, - { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, - { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, - { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, @@ -199,10 +204,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, - { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, - { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, - { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, - { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, ] [[package]] @@ -233,19 +234,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] -[[package]] -name = "pyright" -version = "1.1.408" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "nodeenv" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/74/b2/5db700e52554b8f025faa9c3c624c59f1f6c8841ba81ab97641b54322f16/pyright-1.1.408.tar.gz", hash = "sha256:f28f2321f96852fa50b5829ea492f6adb0e6954568d1caa3f3af3a5f555eb684", size = 4400578, upload-time = "2026-01-08T08:07:38.795Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/82/a2c93e32800940d9573fb28c346772a14778b84ba7524e691b324620ab89/pyright-1.1.408-py3-none-any.whl", hash = "sha256:090b32865f4fdb1e0e6cd82bf5618480d48eecd2eb2e70f960982a3d9a4c17c1", size = 6399144, upload-time = "2026-01-08T08:07:37.082Z" }, -] - [[package]] name = "pytest" version = "9.0.2" @@ -277,16 +265,6 @@ version = "6.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, - { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, - { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, - { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, - { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, - { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, - { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, - { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, - { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, - { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, @@ -365,7 +343,7 @@ wheels = [ ] [[package]] -name = "streamer" +name = "streamd" version = "0.1.0" source = { editable = "." } dependencies = [ @@ -380,8 +358,8 @@ dependencies = [ [package.dev-dependencies] dev = [ + { name = "basedpyright" }, { name = "faker" }, - { name = "pyright" }, { name = "pytest" }, { name = "ruff" }, ] @@ -399,8 +377,8 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ + { name = "basedpyright", specifier = "==1.38.0" }, { name = "faker", specifier = "==40.4.0" }, - { name = "pyright", specifier = "==1.1.408" }, { name = "pytest", specifier = "==9.0.2" }, { name = "ruff", specifier = "==0.15.1" }, ] From f9ed0463f7670ecbe7972daf03e66d3ce9a1fac2 Mon Sep 17 00:00:00 2001 From: Konstantin Fickel Date: Sun, 15 Feb 2026 17:30:44 +0100 Subject: [PATCH 08/20] refactor: rename test/ to tests/ --- .../localize/test_extract_datetime.py | 2 +- .../test_repository_configuration_merge.py | 0 {test => tests}/parse/test_parse.py | 3 +-- {test => tests}/query/test_find.py | 4 ++-- {test => tests}/test_localize.py | 24 +++++++++---------- .../timesheet/test_extract_timesheets.py | 0 6 files changed, 16 insertions(+), 17 deletions(-) rename {test => tests}/localize/test_extract_datetime.py (99%) rename {test => tests}/localize/test_repository_configuration_merge.py (100%) rename {test => tests}/parse/test_parse.py (99%) rename {test => tests}/query/test_find.py (96%) rename {test => tests}/test_localize.py (93%) rename {test => tests}/timesheet/test_extract_timesheets.py (100%) diff --git a/test/localize/test_extract_datetime.py b/tests/localize/test_extract_datetime.py similarity index 99% rename from test/localize/test_extract_datetime.py rename to tests/localize/test_extract_datetime.py index 279f194..8758ebf 100644 --- a/test/localize/test_extract_datetime.py +++ b/tests/localize/test_extract_datetime.py @@ -1,6 +1,6 @@ from datetime import date, datetime, time -from streamer.localize.extract_datetime import ( +from streamd.localize.extract_datetime import ( extract_date_from_marker, extract_datetime_from_file_name, extract_datetime_from_marker, diff --git a/test/localize/test_repository_configuration_merge.py b/tests/localize/test_repository_configuration_merge.py similarity index 100% rename from test/localize/test_repository_configuration_merge.py rename to tests/localize/test_repository_configuration_merge.py diff --git a/test/parse/test_parse.py b/tests/parse/test_parse.py similarity index 99% rename from test/parse/test_parse.py rename to tests/parse/test_parse.py index b692120..ef72e5a 100644 --- a/test/parse/test_parse.py +++ b/tests/parse/test_parse.py @@ -1,6 +1,5 @@ from faker import Faker - -from streamer.parse import Shard, StreamFile, parse_markdown_file +from streamd.parse import Shard, StreamFile, parse_markdown_file fake = Faker() diff --git a/test/query/test_find.py b/tests/query/test_find.py similarity index 96% rename from test/query/test_find.py rename to tests/query/test_find.py index 725d3b2..6b72e87 100644 --- a/test/query/test_find.py +++ b/tests/query/test_find.py @@ -2,8 +2,8 @@ from __future__ import annotations from datetime import datetime -from streamer.localize import LocalizedShard -from streamer.query.find import find_shard, find_shard_by_position +from streamd.localize import LocalizedShard +from streamd.query.find import find_shard, find_shard_by_position def generate_localized_shard( diff --git a/test/test_localize.py b/tests/test_localize.py similarity index 93% rename from test/test_localize.py rename to tests/test_localize.py index 678604b..ecdb864 100644 --- a/test/test_localize.py +++ b/tests/test_localize.py @@ -1,14 +1,14 @@ from datetime import datetime -from streamer.localize.localize import localize_stream_file -from streamer.localize.localized_shard import LocalizedShard -from streamer.localize.repository_configuration import ( +from streamd.localize.localize import localize_stream_file +from streamd.localize.localized_shard import LocalizedShard +from streamd.localize.repository_configuration import ( Dimension, Marker, MarkerPlacement, RepositoryConfiguration, ) -from streamer.parse.shard import Shard, StreamFile +from streamd.parse.shard import Shard, StreamFile repository_configuration = RepositoryConfiguration( dimensions={ @@ -29,8 +29,8 @@ repository_configuration = RepositoryConfiguration( ), }, markers={ - "Streamer": Marker( - display_name="Streamer", + "Streamd": Marker( + display_name="Streamd", placements=[ MarkerPlacement(dimension="project"), MarkerPlacement( @@ -49,39 +49,39 @@ class TestLocalize: def test_project_simple_stream_file(self): stream_file = StreamFile( file_name="20250622-121000 Test File.md", - shard=Shard(start_line=1, end_line=1, markers=["Streamer"]), + shard=Shard(start_line=1, end_line=1, markers=["Streamd"]), ) assert localize_stream_file( stream_file, repository_configuration ) == LocalizedShard( moment=datetime(2025, 6, 22, 12, 10, 0, 0), - markers=["Streamer"], + markers=["Streamd"], tags=[], start_line=1, end_line=1, children=[], - location={"project": "Streamer", "file": stream_file.file_name}, + location={"project": "Streamd", "file": stream_file.file_name}, ) def test_timesheet_use_case(self): stream_file = StreamFile( file_name="20260131-210000 Test File.md", - shard=Shard(start_line=1, end_line=1, markers=["Timesheet", "Streamer"]), + shard=Shard(start_line=1, end_line=1, markers=["Timesheet", "Streamd"]), ) assert localize_stream_file( stream_file, repository_configuration ) == LocalizedShard( moment=datetime(2026, 1, 31, 21, 0, 0, 0), - markers=["Timesheet", "Streamer"], + markers=["Timesheet", "Streamd"], tags=[], start_line=1, end_line=1, children=[], location={ "file": stream_file.file_name, - "project": "Streamer", + "project": "Streamd", "timesheet": "coding", }, ) diff --git a/test/timesheet/test_extract_timesheets.py b/tests/timesheet/test_extract_timesheets.py similarity index 100% rename from test/timesheet/test_extract_timesheets.py rename to tests/timesheet/test_extract_timesheets.py From b4848bb6610fe016f608a303e75c6d3924af0dda Mon Sep 17 00:00:00 2001 From: Konstantin Fickel Date: Sun, 15 Feb 2026 17:31:02 +0100 Subject: [PATCH 09/20] build: migrate to devenv - Add devenv.nix with Python/uv setup and git-hooks - Add devenv.yaml and devenv.lock - Refactor flake.nix to use devenv integration - Update .envrc from flake to devenv - Add devenv artifacts to .gitignore --- .envrc | 4 +- .gitignore | 3 + devenv.lock | 123 +++++++++++++++++++++ devenv.nix | 28 +++++ devenv.yaml | 6 + flake.lock | 82 ++++++++++++-- flake.nix | 311 +++++++++++++++++++++++----------------------------- 7 files changed, 373 insertions(+), 184 deletions(-) create mode 100644 devenv.lock create mode 100644 devenv.nix create mode 100644 devenv.yaml diff --git a/.envrc b/.envrc index 99764ea..163bbd9 100644 --- a/.envrc +++ b/.envrc @@ -1 +1,3 @@ -use flake .#impure +#!/usr/bin/env bash +eval "$(devenv direnvrc)" +use devenv diff --git a/.gitignore b/.gitignore index 2b9abaa..ff4088b 100644 --- a/.gitignore +++ b/.gitignore @@ -177,3 +177,6 @@ pyrightconfig.json .direnv test-report.xml +.devenv +.devenv.flake.nix +.pre-commit-config.yaml diff --git a/devenv.lock b/devenv.lock new file mode 100644 index 0000000..9996189 --- /dev/null +++ b/devenv.lock @@ -0,0 +1,123 @@ +{ + "nodes": { + "devenv": { + "locked": { + "dir": "src/modules", + "lastModified": 1771157881, + "owner": "cachix", + "repo": "devenv", + "rev": "b0b3dfa70ec90fa49f672e579f186faf4f61bd4b", + "type": "github" + }, + "original": { + "dir": "src/modules", + "owner": "cachix", + "repo": "devenv", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1767039857, + "owner": "NixOS", + "repo": "flake-compat", + "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "flake-compat", + "type": "github" + } + }, + "git-hooks": { + "inputs": { + "flake-compat": "flake-compat", + "gitignore": "gitignore", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1770726378, + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "5eaaedde414f6eb1aea8b8525c466dc37bba95ae", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "git-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1762808025, + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "cb5e3fdca1de58ccbc3ef53de65bd372b48f567c", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "nixpkgs": { + "inputs": { + "nixpkgs-src": "nixpkgs-src" + }, + "locked": { + "lastModified": 1770434727, + "owner": "cachix", + "repo": "devenv-nixpkgs", + "rev": "8430f16a39c27bdeef236f1eeb56f0b51b33d348", + "type": "github" + }, + "original": { + "owner": "cachix", + "ref": "rolling", + "repo": "devenv-nixpkgs", + "type": "github" + } + }, + "nixpkgs-src": { + "flake": false, + "locked": { + "lastModified": 1769922788, + "narHash": "sha256-H3AfG4ObMDTkTJYkd8cz1/RbY9LatN5Mk4UF48VuSXc=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "207d15f1a6603226e1e223dc79ac29c7846da32e", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "devenv": "devenv", + "git-hooks": "git-hooks", + "nixpkgs": "nixpkgs", + "pre-commit-hooks": [ + "git-hooks" + ] + } + } + }, + "root": "root", + "version": 7 +} diff --git a/devenv.nix b/devenv.nix new file mode 100644 index 0000000..ce1097e --- /dev/null +++ b/devenv.nix @@ -0,0 +1,28 @@ +{ + pkgs, + ... +}: +{ + languages = { + python = { + enable = true; + uv.enable = true; + }; + }; + + packages = [ + pkgs.basedpyright + ]; + + git-hooks.hooks = { + basedpyright = { + enable = true; + entry = "${pkgs.basedpyright}/bin/basedpyright"; + files = "\\.py$"; + types = [ "file" ]; + }; + ruff.enable = true; + ruff-format.enable = true; + commitizen.enable = true; + }; +} diff --git a/devenv.yaml b/devenv.yaml new file mode 100644 index 0000000..28877ba --- /dev/null +++ b/devenv.yaml @@ -0,0 +1,6 @@ +inputs: + git-hooks: + url: github:cachix/git-hooks.nix + inputs: + nixpkgs: + follows: nixpkgs diff --git a/flake.lock b/flake.lock index 4fa0a20..b04d8e3 100644 --- a/flake.lock +++ b/flake.lock @@ -1,16 +1,75 @@ { "nodes": { - "nixpkgs": { + "flake-compat": { + "flake": false, "locked": { - "lastModified": 1770197578, - "narHash": "sha256-AYqlWrX09+HvGs8zM6ebZ1pwUqjkfpnv8mewYwAo+iM=", + "lastModified": 1767039857, + "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=", "owner": "NixOS", - "repo": "nixpkgs", - "rev": "00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", + "repo": "flake-compat", + "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", "type": "github" }, "original": { "owner": "NixOS", + "repo": "flake-compat", + "type": "github" + } + }, + "git-hooks": { + "inputs": { + "flake-compat": "flake-compat", + "gitignore": "gitignore", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1770726378, + "narHash": "sha256-kck+vIbGOaM/dHea7aTBxdFYpeUl/jHOy5W3eyRvVx8=", + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "5eaaedde414f6eb1aea8b8525c466dc37bba95ae", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "git-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1771008912, + "narHash": "sha256-gf2AmWVTs8lEq7z/3ZAsgnZDhWIckkb+ZnAo5RzSxJg=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "a82ccc39b39b621151d6732718e3e250109076fa", + "type": "github" + }, + "original": { + "owner": "nixos", "ref": "nixos-unstable", "repo": "nixpkgs", "type": "github" @@ -29,11 +88,11 @@ ] }, "locked": { - "lastModified": 1763662255, - "narHash": "sha256-4bocaOyLa3AfiS8KrWjZQYu+IAta05u3gYZzZ6zXbT0=", + "lastModified": 1771039651, + "narHash": "sha256-WZOfX4APbc6vmL14ZWJXgBeRfEER8H+OIX0D0nSmv0M=", "owner": "pyproject-nix", "repo": "build-system-pkgs", - "rev": "042904167604c681a090c07eb6967b4dd4dae88c", + "rev": "69bc2b53b79cbd6ce9f66f506fc962b45b5e68b9", "type": "github" }, "original": { @@ -64,6 +123,7 @@ }, "root": { "inputs": { + "git-hooks": "git-hooks", "nixpkgs": "nixpkgs", "pyproject-build-systems": "pyproject-build-systems", "pyproject-nix": "pyproject-nix", @@ -80,11 +140,11 @@ ] }, "locked": { - "lastModified": 1770331927, - "narHash": "sha256-jlOvO++uvne/lTgWqdI4VhTV5OpVWi70ZDVBlT6vGSs=", + "lastModified": 1770770348, + "narHash": "sha256-A2GzkmzdYvdgmMEu5yxW+xhossP+txrYb7RuzRaqhlg=", "owner": "pyproject-nix", "repo": "uv2nix", - "rev": "5b43a934e15b23bfba6c408cba1c570eccf80080", + "rev": "5d1b2cb4fe3158043fbafbbe2e46238abbc954b0", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index d112b66..02359db 100644 --- a/flake.nix +++ b/flake.nix @@ -1,8 +1,8 @@ { - description = "Hello world flake using uv2nix"; + description = "Using Markdown Files to organize your life as a @Tag-Stream"; inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; pyproject-nix = { url = "github:pyproject-nix/pyproject.nix"; @@ -21,70 +21,135 @@ inputs.uv2nix.follows = "uv2nix"; inputs.nixpkgs.follows = "nixpkgs"; }; + + git-hooks = { + url = "github:cachix/git-hooks.nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; }; outputs = { self, nixpkgs, - uv2nix, pyproject-nix, + uv2nix, pyproject-build-systems, + git-hooks, ... }: let inherit (nixpkgs) lib; + forAllSystems = lib.genAttrs lib.systems.flakeExposed; - # Load a uv workspace from a workspace root. - # Uv2nix treats all uv projects as workspace projects. workspace = uv2nix.lib.workspace.loadWorkspace { workspaceRoot = ./.; }; - # Create package overlay from workspace. overlay = workspace.mkPyprojectOverlay { - # Prefer prebuilt binary wheels as a package source. - # Sdists are less likely to "just work" because of the metadata missing from uv.lock. - # Binary wheels are more likely to, but may still require overrides for library dependencies. - sourcePreference = "wheel"; # or sourcePreference = "sdist"; - # Optionally customise PEP 508 environment - # environ = { - # platform_release = "5.10.65"; - # }; + sourcePreference = "wheel"; }; - # Extend generated overlay with build fixups - # - # Uv2nix can only work with what it has, and uv.lock is missing essential metadata to perform some builds. - # This is an additional overlay implementing build fixups. - # See: - # - https://pyproject-nix.github.io/uv2nix/FAQ.html - pyprojectOverrides = _final: _prev: { - # Implement build fixups here. - # Note that uv2nix is _not_ using Nixpkgs buildPythonPackage. - # It's using https://pyproject-nix.github.io/pyproject.nix/build.html + editableOverlay = workspace.mkEditablePyprojectOverlay { + root = "$REPO_ROOT"; }; - # This example is only using x86_64-linux - pkgs = nixpkgs.legacyPackages.x86_64-linux; + pythonSets = forAllSystems ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + inherit (pkgs) stdenv; - # Use Python 3.14 from nixpkgs - python = pkgs.python314; + baseSet = pkgs.callPackage pyproject-nix.build.packages { + python = pkgs.python313; + }; - # Construct package set - pythonSet = - # Use base package set from pyproject.nix builders - (pkgs.callPackage pyproject-nix.build.packages { - inherit python; - }).overrideScope - ( - lib.composeManyExtensions [ - pyproject-build-systems.overlays.default - overlay - pyprojectOverrides - ] - ); + pyprojectOverrides = _final: prev: { + streamd = prev.streamd.overrideAttrs (old: { + passthru = old.passthru // { + tests = (old.passthru.tests or { }) // { + pytest = stdenv.mkDerivation { + name = "${_final.streamd.name}-pytest"; + inherit (_final.streamd) src; + nativeBuildInputs = [ + (_final.mkVirtualEnv "streamd-pytest-env" { + streamd = [ "dev" ]; + }) + ]; + dontConfigure = true; + buildPhase = '' + runHook preBuild + # Exit code 5 means no tests collected — allow it so the + # check succeeds on an empty test suite. + pytest || [ $? -eq 5 ] + runHook postBuild + ''; + installPhase = '' + runHook preInstall + touch $out + runHook postInstall + ''; + }; + }; + }; + }); + }; + + in + baseSet.overrideScope ( + lib.composeManyExtensions [ + pyproject-build-systems.overlays.default + overlay + pyprojectOverrides + ] + ) + ); + + mkGitHooksCheck = + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + pythonSet = pythonSets.${system}; + venv = pythonSet.mkVirtualEnv "streamd-check-env" workspace.deps.default; + in + git-hooks.lib.${system}.run { + src = ./.; + hooks = { + basedpyright = { + enable = true; + entry = "${pkgs.basedpyright}/bin/basedpyright --pythonpath ${venv}/bin/python --project ${ + pkgs.writeText "pyrightconfig.json" ( + builtins.toJSON { + reportMissingTypeStubs = false; + reportUnnecessaryTypeIgnoreComment = false; + } + ) + }"; + files = "\\.py$"; + types = [ "file" ]; + }; + ruff.enable = true; + ruff-format.enable = true; + commitizen.enable = true; + }; + }; in { + packages = forAllSystems ( + system: + let + pythonSet = pythonSets.${system}; + pkgs = nixpkgs.legacyPackages.${system}; + inherit (pkgs.callPackages pyproject-nix.build.util { }) mkApplication; + in + rec { + streamd = mkApplication { + venv = pythonSet.mkVirtualEnv "streamd-env" workspace.deps.default; + package = pythonSet.streamd; + }; + default = streamd; + } + ); + homeManagerModules.default = { lib, @@ -93,176 +158,78 @@ ... }: let - cfg = config.programs.streamer; + cfg = config.programs.streamd; in { - options.programs.streamer = { - enable = lib.mkEnableOption "streamer"; + options.programs.streamd = { + enable = lib.mkEnableOption "streamd"; base-folder = lib.mkOption { type = lib.types.str; - description = "Base Folder of Streamer"; + description = "Base Folder of streamd"; }; package = lib.mkOption { type = lib.types.package; - default = self.packages.${pkgs.system}.streamer; - defaultText = lib.literalExpression "inputs.streamer.packages.\${pkgs.system}.streamer"; - description = "The package to use for the streamer binary."; + default = self.packages.${pkgs.system}.streamd; + defaultText = lib.literalExpression "inputs.streamd.packages.\${pkgs.system}.streamd"; + description = "The package to use for the streamd binary."; }; }; config = lib.mkIf cfg.enable { assertions = [ - (lib.hm.assertions.assertPlatform "programs.streamer" pkgs lib.platforms.linux) + (lib.hm.assertions.assertPlatform "programs.streamd" pkgs lib.platforms.linux) ]; home.packages = [ cfg.package ]; - xdg.configFile."streamer/config.yaml".source = - (pkgs.formats.yaml { }).generate "streamer-configuration" + xdg.configFile."streamd/config.yaml".source = + (pkgs.formats.yaml { }).generate "streamd-configuration" { base_folder = cfg.base-folder; }; - home.shellAliases.s = "streamer"; + home.shellAliases.s = "streamd"; }; }; - # Package a virtual environment as our main application. - # - # Enable no optional dependencies for production build. - packages.x86_64-linux = + checks = forAllSystems ( + system: let - streamer = pythonSet.mkVirtualEnv "streamer-env" workspace.deps.default; + pythonSet = pythonSets.${system}; in { - inherit streamer; - default = streamer; - }; + inherit (pythonSet.streamd.passthru.tests) pytest; + pre-commit = mkGitHooksCheck system; + } + ); - # Make streamer runnable with `nix run` - apps.x86_64-linux = { - default = { - type = "app"; - program = "${self.packages.x86_64-linux.default}/bin/streamer"; - }; - }; - - # This example provides two different modes of development: - # - Impurely using uv to manage virtual environments - # - Pure development using uv2nix to manage virtual environments - devShells.x86_64-linux = { - # It is of course perfectly OK to keep using an impure virtualenv workflow and only use uv2nix to build packages. - # This devShell simply adds Python and undoes the dependency leakage done by Nixpkgs Python infrastructure. - impure = pkgs.mkShell { - packages = with pkgs; [ - python - uv - pre-commit - bashInteractive - ]; - env = { - # Prevent uv from managing Python downloads - UV_PYTHON_DOWNLOADS = "never"; - # Force uv to use nixpkgs Python interpreter - UV_PYTHON = python.interpreter; - } - // lib.optionalAttrs pkgs.stdenv.isLinux { - # Python libraries often load native shared objects using dlopen(3). - # Setting LD_LIBRARY_PATH makes the dynamic library loader aware of libraries without using RPATH for lookup. - LD_LIBRARY_PATH = lib.makeLibraryPath pkgs.pythonManylinuxPackages.manylinux1; - }; - shellHook = '' - unset PYTHONPATH - ''; - }; - - # This devShell uses uv2nix to construct a virtual environment purely from Nix, using the same dependency specification as the application. - # The notable difference is that we also apply another overlay here enabling editable mode ( https://setuptools.pypa.io/en/latest/userguide/development_mode.html ). - # - # This means that any changes done to your local files do not require a rebuild. - # - # Note: Editable package support is still unstable and subject to change. - uv2nix = - let - # Create an overlay enabling editable mode for all local dependencies. - editableOverlay = workspace.mkEditablePyprojectOverlay { - # Use environment variable - root = "$REPO_ROOT"; - # Optional: Only enable editable for these packages - # members = [ "streamer" ]; - }; - - # Override previous set with our overrideable overlay. - editablePythonSet = pythonSet.overrideScope ( - lib.composeManyExtensions [ - editableOverlay - - # Apply fixups for building an editable package of your workspace packages - (final: prev: { - streamer = prev.streamer.overrideAttrs (old: { - # It's a good idea to filter the sources going into an editable build - # so the editable package doesn't have to be rebuilt on every change. - src = lib.fileset.toSource { - root = old.src; - fileset = lib.fileset.unions [ - (old.src + "/pyproject.toml") - (old.src + "/README.md") - (old.src + "/src/streamer/__init__.py") - ]; - }; - - # Hatchling (our build system) has a dependency on the `editables` package when building editables. - # - # In normal Python flows this dependency is dynamically handled, and doesn't need to be explicitly declared. - # This behaviour is documented in PEP-660. - # - # With Nix the dependency needs to be explicitly declared. - nativeBuildInputs = - old.nativeBuildInputs - ++ final.resolveBuildSystem { - editables = [ ]; - }; - }); - - }) - ] - ); - - # Build virtual environment, with local packages being editable. - # - # Enable all optional dependencies for development. - virtualenv = editablePythonSet.mkVirtualEnv "streamer-dev-env" workspace.deps.all; - - in - pkgs.mkShell { - packages = with pkgs; [ + devShells = forAllSystems ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + pythonSet = pythonSets.${system}.overrideScope editableOverlay; + virtualenv = pythonSet.mkVirtualEnv "streamd-dev-env" workspace.deps.all; + in + { + default = pkgs.mkShell { + packages = [ virtualenv - uv - pre-commit - bashInteractive + pkgs.uv ]; - env = { - # Don't create venv using uv UV_NO_SYNC = "1"; - - # Force uv to use Python interpreter from venv - UV_PYTHON = "${virtualenv}/bin/python"; - - # Prevent uv from downloading managed Python's + UV_PYTHON = pythonSet.python.interpreter; UV_PYTHON_DOWNLOADS = "never"; }; - shellHook = '' - # Undo dependency propagation by nixpkgs. unset PYTHONPATH - - # Get repository root using git. This is expanded at runtime by the editable `.pth` machinery. export REPO_ROOT=$(git rev-parse --show-toplevel) + ${(mkGitHooksCheck system).shellHook} ''; }; - }; + } + ); }; } From ce5e476b2326643f5d963aa11b058df48ca1e2c8 Mon Sep 17 00:00:00 2001 From: Konstantin Fickel Date: Sun, 15 Feb 2026 17:31:18 +0100 Subject: [PATCH 10/20] ci: split workflow into check and build jobs --- .forgejo/workflows/ci.yml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index 7fcb54b..6ce781f 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -5,7 +5,7 @@ on: workflow_dispatch: jobs: - build-and-lint: + check: name: Lint, Check & Test runs-on: nix @@ -16,8 +16,12 @@ jobs: - run: nix --version - run: nix flake check - - name: Install the project - run: 'nix develop .#impure --command bash -c "uv sync --locked --all-extras --dev"' + build: + name: Build Package + runs-on: nix - - name: Test with PyTest - run: nix develop .#impure --command bash -c "uv run pytest --junit-xml test-report.xml" + steps: + - name: Check out Repository + uses: https://git.konstantinfickel.de/actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - run: nix build From ca6b5bbd4d872257d3ed53be1e68e0fe3385b256 Mon Sep 17 00:00:00 2001 From: Konstantin Fickel Date: Sun, 15 Feb 2026 17:32:39 +0100 Subject: [PATCH 11/20] fix: include dev deps in basedpyright check environment Use workspace.deps.all instead of workspace.deps.default so that basedpyright can resolve test dependencies like faker. --- flake.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 02359db..32093d0 100644 --- a/flake.nix +++ b/flake.nix @@ -108,7 +108,7 @@ let pkgs = nixpkgs.legacyPackages.${system}; pythonSet = pythonSets.${system}; - venv = pythonSet.mkVirtualEnv "streamd-check-env" workspace.deps.default; + venv = pythonSet.mkVirtualEnv "streamd-check-env" workspace.deps.all; in git-hooks.lib.${system}.run { src = ./.; From d89ad8b131a6d89049f871e3297e84ca7d4050a6 Mon Sep 17 00:00:00 2001 From: Konstantin Fickel Date: Sun, 15 Feb 2026 17:33:12 +0100 Subject: [PATCH 12/20] build: add /result to .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index ff4088b..5976f3d 100644 --- a/.gitignore +++ b/.gitignore @@ -180,3 +180,5 @@ test-report.xml .devenv .devenv.flake.nix .pre-commit-config.yaml + +result From 20a3e8b43709547c24b17c9da4342fb448a6c3c9 Mon Sep 17 00:00:00 2001 From: Konstantin Fickel Date: Sun, 15 Feb 2026 17:40:17 +0100 Subject: [PATCH 13/20] feat: add streamd logo --- README.md | 2 + streamd.svg | 283 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 285 insertions(+) create mode 100644 streamd.svg diff --git a/README.md b/README.md index 5389e0b..9e5fa28 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # strea.md +![The Strea.md-Logo: A tag on an endless paper roll](./streamd.svg) + Strea.md is a personal knowledge management and time-tracking CLI tool. It organizes time-ordered markdown files using `@tag` annotations, letting you manage tasks, track time, and query your notes from the terminal. ## Core Concepts diff --git a/streamd.svg b/streamd.svg new file mode 100644 index 0000000..11973db --- /dev/null +++ b/streamd.svg @@ -0,0 +1,283 @@ + + + + + Streamd + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Streamd + + + Konstantin Fickel + + + + + + From ed493cff29a22f603a8045e137ef13eae9410bd0 Mon Sep 17 00:00:00 2001 From: Konstantin Fickel Date: Sun, 29 Mar 2026 18:19:15 +0200 Subject: [PATCH 14/20] refactor: rewrite in rust --- .envrc | 4 +- .gitignore | 198 +-- .pre-commit-config.yaml | 11 - .python-version | 1 - CLAUDE.md | 35 + Cargo.lock | 1299 +++++++++++++++++ Cargo.toml | 40 + REQUIREMENTS.md | 375 +++++ devenv.lock | 123 -- devenv.nix | 28 - devenv.yaml | 6 - flake.lock | 97 +- flake.nix | 197 ++- pyproject.toml | 30 - src/cli/args.rs | 40 + src/cli/commands/completions.rs | 11 + src/cli/commands/edit.rs | 73 + src/cli/commands/mod.rs | 5 + src/cli/commands/new.rs | 60 + src/cli/commands/timesheet.rs | 52 + src/cli/commands/todo.rs | 56 + src/cli/mod.rs | 4 + src/config.rs | 44 + src/error.rs | 25 + src/extract/mod.rs | 5 + src/extract/parser.rs | 739 ++++++++++ src/extract/tag_extraction.rs | 219 +++ src/lib.rs | 14 + src/localize/configuration.rs | 448 ++++++ src/localize/datetime.rs | 365 +++++ src/localize/mod.rs | 15 + src/localize/preconfigured.rs | 46 + src/localize/shard.rs | 282 ++++ src/main.rs | 19 + src/models/dimension.rs | 42 + src/models/localized_shard.rs | 63 + src/models/marker.rs | 76 + src/models/mod.rs | 11 + src/models/shard.rs | 115 ++ src/models/timecard.rs | 77 + src/query/find.rs | 209 +++ src/query/mod.rs | 3 + src/streamd/__init__.py | 126 -- src/streamd/localize/__init__.py | 9 - src/streamd/localize/extract_datetime.py | 92 -- src/streamd/localize/localize.py | 73 - src/streamd/localize/localized_shard.py | 14 - .../localize/preconfigured_configurations.py | 43 - .../localize/repository_configuration.py | 106 -- src/streamd/parse/__init__.py | 4 - src/streamd/parse/extract_tag.py | 92 -- src/streamd/parse/list.py | 13 - src/streamd/parse/markdown_tag.py | 23 - src/streamd/parse/parse.py | 258 ---- src/streamd/parse/shard.py | 19 - src/streamd/query/__init__.py | 3 - src/streamd/query/find.py | 36 - src/streamd/settings/__init__.py | 38 - src/streamd/timesheet/configuration.py | 115 -- src/streamd/timesheet/extract.py | 113 -- src/streamd/timesheet/timecard.py | 23 - src/timesheet/configuration.rs | 84 ++ src/timesheet/extract.rs | 537 +++++++ src/timesheet/mod.rs | 7 + src/timesheet/point_types.rs | 54 + tests/localize/test_extract_datetime.py | 157 -- .../test_repository_configuration_merge.py | 367 ----- tests/parse/test_parse.py | 343 ----- tests/query/test_find.py | 104 -- tests/test_localize.py | 231 --- tests/timesheet/test_extract_timesheets.py | 288 ---- uv.lock | 438 ------ 72 files changed, 5684 insertions(+), 3688 deletions(-) delete mode 100644 .pre-commit-config.yaml delete mode 100644 .python-version create mode 100644 CLAUDE.md create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 REQUIREMENTS.md delete mode 100644 devenv.lock delete mode 100644 devenv.nix delete mode 100644 devenv.yaml delete mode 100644 pyproject.toml create mode 100644 src/cli/args.rs create mode 100644 src/cli/commands/completions.rs create mode 100644 src/cli/commands/edit.rs create mode 100644 src/cli/commands/mod.rs create mode 100644 src/cli/commands/new.rs create mode 100644 src/cli/commands/timesheet.rs create mode 100644 src/cli/commands/todo.rs create mode 100644 src/cli/mod.rs create mode 100644 src/config.rs create mode 100644 src/error.rs create mode 100644 src/extract/mod.rs create mode 100644 src/extract/parser.rs create mode 100644 src/extract/tag_extraction.rs create mode 100644 src/lib.rs create mode 100644 src/localize/configuration.rs create mode 100644 src/localize/datetime.rs create mode 100644 src/localize/mod.rs create mode 100644 src/localize/preconfigured.rs create mode 100644 src/localize/shard.rs create mode 100644 src/main.rs create mode 100644 src/models/dimension.rs create mode 100644 src/models/localized_shard.rs create mode 100644 src/models/marker.rs create mode 100644 src/models/mod.rs create mode 100644 src/models/shard.rs create mode 100644 src/models/timecard.rs create mode 100644 src/query/find.rs create mode 100644 src/query/mod.rs delete mode 100644 src/streamd/__init__.py delete mode 100644 src/streamd/localize/__init__.py delete mode 100644 src/streamd/localize/extract_datetime.py delete mode 100644 src/streamd/localize/localize.py delete mode 100644 src/streamd/localize/localized_shard.py delete mode 100644 src/streamd/localize/preconfigured_configurations.py delete mode 100644 src/streamd/localize/repository_configuration.py delete mode 100644 src/streamd/parse/__init__.py delete mode 100644 src/streamd/parse/extract_tag.py delete mode 100644 src/streamd/parse/list.py delete mode 100644 src/streamd/parse/markdown_tag.py delete mode 100644 src/streamd/parse/parse.py delete mode 100644 src/streamd/parse/shard.py delete mode 100644 src/streamd/query/__init__.py delete mode 100644 src/streamd/query/find.py delete mode 100644 src/streamd/settings/__init__.py delete mode 100644 src/streamd/timesheet/configuration.py delete mode 100644 src/streamd/timesheet/extract.py delete mode 100644 src/streamd/timesheet/timecard.py create mode 100644 src/timesheet/configuration.rs create mode 100644 src/timesheet/extract.rs create mode 100644 src/timesheet/mod.rs create mode 100644 src/timesheet/point_types.rs delete mode 100644 tests/localize/test_extract_datetime.py delete mode 100644 tests/localize/test_repository_configuration_merge.py delete mode 100644 tests/parse/test_parse.py delete mode 100644 tests/query/test_find.py delete mode 100644 tests/test_localize.py delete mode 100644 tests/timesheet/test_extract_timesheets.py delete mode 100644 uv.lock diff --git a/.envrc b/.envrc index 163bbd9..3550a30 100644 --- a/.envrc +++ b/.envrc @@ -1,3 +1 @@ -#!/usr/bin/env bash -eval "$(devenv direnvrc)" -use devenv +use flake diff --git a/.gitignore b/.gitignore index 5976f3d..f1f0b4c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,184 +1,24 @@ -# Created by https://www.toptal.com/developers/gitignore/api/python -# Edit at https://www.toptal.com/developers/gitignore?templates=python +# Rust build artifacts +/target/ -### Python ### -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/#use-with-ide -.pdm.toml - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ - -### Python Patch ### -# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration -poetry.toml - -# ruff -.ruff_cache/ - -# LSP config files -pyrightconfig.json - -# End of https://www.toptal.com/developers/gitignore/api/python +# OS +.DS_Store +Thumbs.db +# Nix .direnv -test-report.xml -.devenv -.devenv.flake.nix -.pre-commit-config.yaml - result +result-* + +# Test artifacts +*.profraw +*.profdata + +.pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index 46f707d..0000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,11 +0,0 @@ -repos: - - repo: https://github.com/astral-sh/uv-pre-commit - rev: 0.7.13 - hooks: - - id: uv-lock - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.0 - hooks: - - id: ruff - args: [ --fix ] - - id: ruff-format \ No newline at end of file diff --git a/.python-version b/.python-version deleted file mode 100644 index 24ee5b1..0000000 --- a/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.13 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..3ca95b6 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,35 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Build & Test Commands + +```bash +nix develop # Enter dev shell with Rust toolchain +nix build # Build the package +nix flake check # Run all checks (clippy, fmt, tests, pre-commit) + +# Inside nix develop: +cargo test # Run all tests +cargo test test_name # Run a specific test +cargo clippy # Lint +cargo fmt # Format +``` + +## Architecture + +Streamd parses markdown files into hierarchical **Shards**, then **localizes** them by assigning temporal moments and dimensional placements based on `@Tag` markers. + +**Data flow:** Markdown → `extract::parser` → `Shard` tree → `localize::shard` → `LocalizedShard` tree + +**Key modules:** +- `models/` — Core types: `Shard`, `LocalizedShard`, `Dimension`, `Marker`, `Timecard` +- `extract/` — Tag extraction (`tag_extraction.rs`) and markdown parsing (`parser.rs`) +- `localize/` — DateTime extraction, configuration merging, shard localization +- `timesheet/` — State machine that converts localized shards into timecards +- `query/` — Recursive search functions for finding shards by predicate +- `cli/` — Clap-based CLI commands + +## Requirements + +`REQUIREMENTS.md` contains the formal specification. Update it along with the `README.md` whenever implementing or changing features. diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..5f20392 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1299 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + +[[package]] +name = "backtrace-ext" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50" +dependencies = [ + "backtrace", +] + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "cc" +version = "1.2.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_complete" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19c9f1dde76b736e3681f28cec9d5a61299cbaae0fce80a68e43724ad56031eb" +dependencies = [ + "clap", +] + +[[package]] +name = "clap_derive" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "directories" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "getopts" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +dependencies = [ + "unicode-width 0.2.2", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "is_ci" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4c90f45aa2e6eacbe8645f77fdea542ac97a494bcd117a67df9ff4d611f995" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "libredox" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08" +dependencies = [ + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "miette" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" +dependencies = [ + "backtrace", + "backtrace-ext", + "cfg-if", + "miette-derive", + "owo-colors", + "supports-color", + "supports-hyperlinks", + "supports-unicode", + "terminal_size", + "textwrap", + "unicode-width 0.1.14", +] + +[[package]] +name = "miette-derive" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "owo-colors" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" + +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pulldown-cmark" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86ba2052aebccc42cbbb3ed234b8b13ce76f75c3551a303cb2bcffcff12bb14" +dependencies = [ + "bitflags", + "getopts", + "memchr", + "pulldown-cmark-escape", + "unicase", +] + +[[package]] +name = "pulldown-cmark-escape" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "streamd" +version = "0.1.0" +dependencies = [ + "chrono", + "clap", + "clap_complete", + "directories", + "indexmap", + "itertools", + "miette", + "once_cell", + "pretty_assertions", + "pulldown-cmark", + "regex", + "serde", + "serde_yaml", + "tempfile", + "thiserror 2.0.18", + "walkdir", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "supports-color" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6" +dependencies = [ + "is_ci", +] + +[[package]] +name = "supports-hyperlinks" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e396b6523b11ccb83120b115a0b7366de372751aa6edf19844dfb13a6af97e91" + +[[package]] +name = "supports-unicode" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "terminal_size" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" +dependencies = [ + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" +dependencies = [ + "unicode-linebreak", + "unicode-width 0.2.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.115" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6523d69017b7633e396a89c5efab138161ed5aafcbc8d3e5c5a42ae38f50495a" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.115" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e3a6c758eb2f701ed3d052ff5737f5bfe6614326ea7f3bbac7156192dc32e67" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.115" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "921de2737904886b52bcbb237301552d05969a6f9c40d261eb0533c8b055fedf" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.115" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a93e946af942b58934c604527337bad9ae33ba1d5c6900bbb41c2c07c2364a93" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..bc2aaba --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "streamd" +version = "0.1.0" +edition = "2021" +description = "Personal knowledge management and time-tracking CLI using @Tag annotations" +license = "AGPL-3.0-only" +authors = ["Konstantin Fickel"] +repository = "https://github.com/konstantinfickel/streamd" + +[dependencies] +clap = { version = "4", features = ["derive", "env"] } +clap_complete = "4" +serde = { version = "1", features = ["derive"] } +serde_yaml = "0.9" +thiserror = "2" +miette = { version = "7", features = ["fancy"] } +pulldown-cmark = "0.12" +regex = "1" +once_cell = "1" +chrono = { version = "0.4", features = ["serde"] } +walkdir = "2" +indexmap = { version = "2", features = ["serde"] } +itertools = "0.13" +directories = "5" + +[dev-dependencies] +pretty_assertions = "1" +tempfile = "3" + +[[bin]] +name = "streamd" +path = "src/main.rs" + +[lib] +name = "streamd" +path = "src/lib.rs" + +[profile.release] +lto = true +strip = true diff --git a/REQUIREMENTS.md b/REQUIREMENTS.md new file mode 100644 index 0000000..aae1809 --- /dev/null +++ b/REQUIREMENTS.md @@ -0,0 +1,375 @@ +# Streamd Requirements + +Streamd (stylized as "Strea.md") is a personal knowledge management and time-tracking CLI tool that organizes time-ordered markdown files using `@Tag` annotations. + +## Core Concepts + +### Shard + +A **Shard** is the fundamental unit of content. It represents a section of a markdown file (paragraph, heading, list item) that can contain markers and tags. + +``` +Shard { + markers: [String] // @Tag annotations at START of content + tags: [String] // @Tag annotations AFTER content begins + start_line: int + end_line: int + children: [Shard] // Nested shards (hierarchical) +} +``` + +### LocalizedShard + +A **LocalizedShard** extends Shard with temporal and dimensional placement information. + +``` +LocalizedShard { + markers: [String] + tags: [String] + start_line: int + end_line: int + moment: DateTime // When this entry was created + location: Map // Dimension placements + children: [LocalizedShard] +} +``` + +--- + +## Tag Extraction Logic + +### R1: Tag Recognition Pattern + +Tags are recognized by the regex pattern: `@([^\s*\x60~\[\]]+)` + +A tag is `@` followed by word characters, excluding: +- Whitespace +- Asterisks `*` +- Backticks `` ` `` +- Tildes `~` +- Brackets `[]` + +**Examples of valid tags:** +- `@Task`, `@Done`, `@Waiting` +- `@Timesheet`, `@Break` +- `@ProjectName`, `@Client-ABC` + +### R2: Marker vs Tag Distinction + +The extraction MUST distinguish between **markers** and **tags** based on their position within a block: + +| Type | Position | Purpose | +|------|----------|---------| +| **Marker** | Before any non-whitespace content | Semantic classification (triggers shard creation) | +| **Tag** | After non-whitespace content | Metadata annotation (does not trigger shard creation) | + +**Example:** +```markdown +@Task @Streamd Working on feature +Some text here @CompletedFeature +``` + +### R3: Marker Boundary Tracking + +The extraction algorithm MUST track a "marker boundary" state: + +1. Start with `marker_boundary_encountered = false` +2. While processing tokens: + - If whitespace-only: continue (boundary not crossed) + - If `@Tag` token found AND boundary NOT crossed: add to markers + - If `@Tag` token found AND boundary crossed: add to tags + - If any non-whitespace content found: set boundary = crossed + +### R4: Nested Token Handling + +Tag extraction MUST handle nested markdown formatting: + +- Emphasis: `*@Tag*` or `_@Tag_` +- Strong: `**@Tag**` or `__@Tag__` +- Strikethrough: `~~@Tag~~` +- Links: `[@Tag](url)` + +Tags inside these formatting elements are still valid and should be extracted. + +### R5: Applicable Block Types + +Tag extraction applies to: +- Headings (`# Heading with @Tag`) +- Paragraphs (`@Tag in paragraph`) +- Quoute Blocks (`> @Tag in Quote`) +- List items (each item can have its own markers) + +--- + +## Parsing Logic + +### R6: Heading-Based Hierarchy + +The parser MUST create a hierarchical shard structure based on markdown headings. + +**Algorithm for determining split level:** + +1. Find the minimum heading level that either: + - Appears 2+ times in the block list, OR + - Has markers AND is not the first heading +2. If no such level exists, do not split (return None) + +**Example:** +```markdown +# Main Title +Content here + +## Section A +Section A content + +## Section B +Section B content +``` + +### R7: List Item Shard Creation + +Each list item with markers MUST become its own shard: + +```markdown +- @Task Item one +- @Task Item two +- Item three +``` + +### R8: Shard Simplification + +When building shards, apply this optimization: +- If a shard has exactly 1 child AND no markers AND no tags +- Return the child directly instead of wrapping it + +--- + +## Dimension Placement Logic + +### R9: Dimension Configuration + +A **Dimension** defines a classification axis: + +``` +Dimension { + display_name: String // For UI display + comment: String? // Documentation + propagate: bool // Whether children inherit this dimension +} +``` + +### R10: Marker Configuration + +A **Marker** defines how a tag affects dimension placement: + +``` +Marker { + display_name: String + placements: [MarkerPlacement] +} + +MarkerPlacement { + if_with: Set // Conditional: only apply if ALL these markers present + dimension: String // Target dimension name + value: String? // Value to assign (defaults to marker name) + overwrites: bool // Can overwrite existing placement +} +``` + +### R11: Conditional Placement + +Placements with `if_with` conditions MUST only apply when ALL specified markers are present on the same shard. + +**Example Configuration:** +``` +Marker "Task" { + placements: [ + { dimension: "task", value: "open" }, + { if_with: ["Done"], dimension: "task", value: "done" }, + { if_with: ["Waiting"], dimension: "task", value: "waiting" }, + ] +} +``` + +**Behavior:** +- `@Task` alone → `task: "open"` +- `@Task @Done` → `task: "done"` (conditional overrides default) +- `@Task @Waiting` → `task: "waiting"` + +### R12: Localization Algorithm + +The localization process MUST follow this algorithm: + +``` +function localize_shard(shard, config, propagated_from_parent, moment): + position = copy(propagated_from_parent) // Start with inherited + private_position = {} // Non-propagating dimensions + + for marker in shard.markers: + if marker in config.markers: + for placement in marker.placements: + // Check conditional + if placement.if_with is subset of shard.markers: + dimension = config.dimensions[placement.dimension] + value = placement.value OR marker + + // Check if we can apply this placement + target = dimension.propagate ? position : private_position + if placement.dimension not in target OR placement.overwrites: + target[placement.dimension] = value + + // Recursively localize children with propagating dimensions + children = [ + localize_shard(child, config, position, moment) + for child in shard.children + ] + + // Merge private dimensions into final position + position.update(private_position) + + return LocalizedShard( + markers: shard.markers, + tags: shard.tags, + location: position, + moment: moment, + children: children, + ) +``` + +### R13: Dimension Propagation + +When `propagate = true`: +- Children inherit the dimension value from their parent +- Child can override with their own placement + +When `propagate = false`: +- Dimension value is NOT inherited by children +- Each shard must have its own marker to be placed in this dimension + +**Example:** +``` +dimensions: { + "project": { propagate: true }, // Children inherit project + "task": { propagate: false }, // Each task is independent +} +``` + +```markdown +# @Project-X +## @Task Item A +### Sub-item +## @Task Item B +``` + +### R14: Overwrite Behavior + +Default: A placement does NOT overwrite an existing value in the dimension. + +With `overwrites: true`: The placement WILL replace any existing value. + +This allows conditional placements to override base placements. + +--- + +## File Naming Convention + +### R15: File Name Format + +Files follow the pattern: `YYYYMMDD-HHMMSS [markers].md` + +- `YYYYMMDD`: Date (8 digits, required) +- `HHMMSS`: Time (4-6 digits, optional, pads with zeros) +- `[markers]`: Space-separated marker names extracted from file content + +**Extraction regex:** `^(?P\d{8})(?:-(?P