Compare commits

...

13 commits

Author SHA1 Message Date
28e214c359
doc: Auto deploy to github pages
Some checks are pending
website / build (push) Waiting to run
website / deploy (push) Blocked by required conditions
2025-04-07 10:49:17 +02:00
ee6667844e
doc: Add pdoc documentation generator 2025-04-07 10:49:16 +02:00
0277f15ca2
doc: Add docstring documentation 2025-04-07 10:49:16 +02:00
0f10789e9c
chore: Change conf options to dot-notation 2025-04-07 10:49:15 +02:00
6e1761e690
chore: Fit env var names to conf option names 2025-04-07 10:49:14 +02:00
8c3ecfa431
chore: Restructure script 2025-04-07 10:49:14 +02:00
cf7e9dd5fe
ref: Ensure paths are Path objects 2025-04-07 10:49:13 +02:00
0e167cf08a
ref: Turn dict into conf obj 2025-04-07 10:49:13 +02:00
c6f05d0b64
feat: Specify taskrc as cli option
In addition to task-data we also allow specifying the taskrc file on the
command line.
2025-04-07 10:49:12 +02:00
3fded7315c
fix: Parse EDITOR and VISUAL env vars with lower precedence
Since the precedence now goes (from lowest to highest):

defaults -> conf_file -> env -> cli

existing EDITOR or VISUAL variables would overwrite those
explicitly set in the conf file. While we do want the general
precedence of env over conf file, this is a special case where
we get the value of the _DEFAULT_ variable from the env, and
only then start parsing our own options. So now, for these vars
we do:

default -> env (EDITOR/VISUAL) -> conf_file -> env (TOPEN_EDITOR) -> cli
2025-04-07 10:49:12 +02:00
96422d254b
fix: Reintegrate env var parsing
Was removed from parsing when conf parsing was added, now re-added into
configuration parsing.
2025-04-07 10:49:11 +02:00
f1f3041928
feat: Add taskrc parsing
We first parse the taskrc file now, before overwriting those options
with ones given on the command line.
2025-04-07 10:49:11 +02:00
1ec3755344
chore: Simplify funcs by removing default args
In preparation for reading the config also from a taskrc file I want to
simplify the configuration management beforehand. There should be one
place where we grab it from the environment, one place to grab from CLI
and then one place from conf file. This helps with simplifying a little
by not again injecting defaults at the mid-point.

Ultimately, we should create one 'config' data structure, probably dict
or NameSpace which we pass around and each function receives it.
2025-04-07 10:49:07 +02:00
5 changed files with 344 additions and 79 deletions

48
.github/workflows/docs.yaml vendored Normal file
View file

@ -0,0 +1,48 @@
name: website
# build documentation for new commits on main
on:
push:
branches:
- main
# Alternative: only build for tags.
# tags:
# - '*'
permissions:
contents: read
jobs:
# Build the documentation and upload the static HTML files as an artifact.
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.13'
- name: Install the latest version of uv
uses: astral-sh/setup-uv@v5
# ADJUST THIS: build your documentation into docs/.
# We use a custom build script for pdoc itself, ideally you just run `pdoc -o docs/ ...` here.
- run: uv run pdoc -o docs/
- uses: actions/upload-pages-artifact@v3
with:
path: docs/
# Deploy the artifact to GitHub pages.
# This is a separate job so that only actions/deploy-pages has the necessary permissions.
deploy:
needs: build
runs-on: ubuntu-latest
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v4

View file

@ -6,9 +6,11 @@ Focuses on letting you quickly:
- create notes for taskwarrior tasks
- edit notes for taskwarrior tasks
Does both by simply being invoked with `topen <task-id>`.
It does both by simply being invoked with `topen <task-id>`.
Automatically appends a small 'Note' annotation to your task so you know you already have notes for it.
Provide a taskwarrior task id or uuid and topen creates a new note file or lets
you edit an existing one. Additionally it adds a small annotation to the task
to let you see that there exists a note file next time you view the task.
Should just work as-is without additional configuration in most modern taskwarrior setups.[^moderntw]

View file

@ -16,3 +16,8 @@ topen = "topen:main"
[tool.pyright]
typeCheckingMode = "standard"
[dependency-groups]
dev = [
"pdoc>=15.0.1",
]

293
topen.py
View file

@ -1,33 +1,167 @@
#!/usr/bin/env python
# Open or create a note file
# for a taskwarrior task.
# Takes a taskwarrior ID or UUID for a single task.
# Edits an existing task note file,
# or creates a new one.
"""
.. include:: ./README.md
# It currently assumes an XDG-compliant taskwarrior configuration by default.
# Usage as library
While normal operation is intended through the commandline to open or create
note files for taskwarrior tasks, the topen.py file can be used as a library to
open and edit taskwarrior notes programmatically.
You can make use of the open editor and utility functions to find and edit
notes, either filling in the required configuration manually or passing around
a TConf configuration object containing them all. If choosing the latter, you can
read the configuration in part from a `taskrc` file using the utility function
`parse_conf()`.
"""
import argparse
import configparser
import os
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path
from tasklib import Task, TaskWarrior
# TODO: This should not assume XDG compliance for
# no-setup TW instances.
TASK_RC = os.getenv("TASKRC", "~/.config/task/taskrc")
TASK_DATA_DIR = os.getenv("TASKDATA", "~/.local/share/task")
TOPEN_DIR = os.getenv("TOPEN_DIR", "~/.local/share/task/notes")
TOPEN_EXT = os.getenv("TOPEN_EXT", "md")
TOPEN_ANNOT = os.getenv("TOPEN_ANNOT", "Note")
TOPEN_EDITOR = os.getenv("EDITOR") or os.getenv("VISUAL", "nano")
TOPEN_QUIET = os.getenv("TOPEN_QUIET", False)
DEFAULTS_DICT = {
"task.rc": "~/.config/task/taskrc",
"task.data": "~/.local/share/task",
"notes.dir": "~/.local/share/task/notes",
"notes.ext": "md",
"notes.annot": "Note",
"notes.editor": os.getenv("EDITOR") or os.getenv("VISUAL") or "nano",
"notes.quiet": "False",
}
def parse_cli() -> argparse.Namespace:
def main():
"""Runs the cli interface.
First sets up the correct options, with overrides in the following order:
`defaults -> taskrc -> env vars -> cli opts`
with cli options having the highest priority.
Then uses those options to get the task corresponding to the task id passed
in as an argument, finds the matching notes file path and opens an editor
pointing to the file.
If the task does not yet have a note annotation it also adds it automatically.
"""
opts_override = {"task.rc": DEFAULTS_DICT["task.rc"]} | parse_env() | parse_cli()
conf_file = _real_path(opts_override["task.rc"])
opts: dict = parse_conf(conf_file) | opts_override
cfg = conf_from_dict(opts)
if not cfg.task_id:
_ = sys.stderr.write("Please provide task ID as argument.\n")
if cfg.notes_quiet:
global IS_QUIET
IS_QUIET = True
task = get_task(id=cfg.task_id, data_location=cfg.task_data)
uuid = task["uuid"]
if not uuid:
_ = sys.stderr.write(f"Could not find task for ID: {cfg.task_id}.")
sys.exit(1)
fname = get_notes_file(uuid, notes_dir=cfg.notes_dir, notes_ext=cfg.notes_ext)
open_editor(fname, editor=cfg.notes_editor)
add_annotation_if_missing(task, annotation_content=cfg.notes_annot)
def get_task(id: str | int, data_location: Path) -> Task:
"""Finds a taskwarrior task from an id.
`id` can be either a taskwarrior id or uuid.
"""
tw = TaskWarrior(data_location)
try:
t = tw.tasks.get(id=id)
except Task.DoesNotExist:
t = tw.tasks.get(uuid=id)
return t
def get_notes_file(uuid: str, notes_dir: Path, notes_ext: str) -> Path:
"""Finds the notes file corresponding to a taskwarrior task."""
return Path(notes_dir).joinpath(f"{uuid}.{notes_ext}")
def open_editor(file: Path, editor: str) -> None:
"""Opens a file with the chosen editor."""
_ = whisper(f"Editing note: {file}")
proc = subprocess.Popen(f"{editor} {file}", shell=True)
_ = proc.wait()
def add_annotation_if_missing(task: Task, annotation_content: str) -> None:
"""Conditionally adds an annotation to a task.
Only adds the annotation if the task does not yet have an
annotation with exactly that content (i.e. avoids
duplication).
"""
for annot in task["annotations"] or []:
if annot["description"] == annotation_content:
return
task.add_annotation(annotation_content)
_ = whisper(f"Added annotation: {annotation_content}")
@dataclass()
class TConf:
"""Topen Configuration
Contains all the configuration options that can affect Topen note creation.
"""
task_rc: Path
"""The path to the taskwarrior taskrc file."""
task_data: Path
"""The path to the taskwarrior data directory."""
task_id: int
"""The id (or uuid) of the task to edit a note for."""
notes_dir: Path
"""The path to the notes directory."""
notes_ext: str
"""The extension of note files."""
notes_annot: str
"""The annotation to add to taskwarrior tasks with notes."""
notes_editor: str
"""The editor to open note files with."""
notes_quiet: bool
"""If set topen will give no feedback on note editing."""
def conf_from_dict(d: dict) -> TConf:
"""Generate a TConf class from a dictionary.
Turns a dictionary containing all the necessary entries into a TConf configuration file.
Will error if one any of the entries are missing.
"""
return TConf(
task_rc=_real_path(d["task.rc"]),
task_data=_real_path(d["task.data"]),
task_id=d["task.id"],
notes_dir=_real_path(d["notes.dir"]),
notes_ext=d["notes.ext"],
notes_annot=d["notes.annot"],
notes_editor=d["notes.editor"],
notes_quiet=d["notes.quiet"],
)
def parse_cli() -> dict:
"""Parse cli options and arguments.
Returns them as a simple dict object.
"""
parser = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter,
description="Taskwarrior note editing made easy.",
@ -44,31 +178,79 @@ you view the task.
_ = parser.add_argument(
"-d",
"--notes-dir",
default=TOPEN_DIR,
help="Location of topen notes files",
)
_ = parser.add_argument(
"--quiet",
default=TOPEN_QUIET,
action="store_true",
help="Silence any verbose displayed information",
)
_ = parser.add_argument(
"--extension", default=TOPEN_EXT, help="Extension of note files"
)
_ = parser.add_argument("--extension", help="Extension of note files")
_ = parser.add_argument(
"--annotation",
default=TOPEN_ANNOT,
help="Annotation content to set within taskwarrior",
)
_ = parser.add_argument("--editor", help="Program to open note files with")
_ = parser.add_argument("--task-rc", help="Location of taskwarrior config file")
_ = parser.add_argument(
"--task-data", default=TASK_DATA_DIR, help="Location of taskwarrior data"
)
_ = parser.add_argument(
"--editor", default=TOPEN_EDITOR, help="Program to open note files with"
"--task-data", help="Location of taskwarrior data directory"
)
return parser.parse_args()
p = parser.parse_args()
return _filtered_dict(
{
"task.id": p.id,
"task.rc": p.task_rc,
"task.data": p.task_data,
"notes.dir": p.notes_dir,
"notes.ext": p.extension,
"notes.annot": p.annotation,
"notes.editor": p.editor,
"notes.quiet": p.quiet,
}
)
def parse_env() -> dict:
"""Parse environment variable options.
Returns them as a simple dict object.
"""
return _filtered_dict(
{
"task.rc": os.getenv("TASKRC"),
"task.data": os.getenv("TASKDATA"),
"notes.dir": os.getenv("TOPEN_NOTES_DIR"),
"notes.ext": os.getenv("TOPEN_NOTES_EXT"),
"notes.annot": os.getenv("TOPEN_NOTES_ANNOT"),
"notes.editor": os.getenv("TOPEN_NOTES_EDITOR"),
"notes.quiet": os.getenv("TOPEN_NOTES_QUIET"),
}
)
def parse_conf(conf_file: Path) -> dict:
"""Parse taskrc configuration file options.
Returns them as a simple dict object.
Uses dot.annotation for options just like taskwarrior settings.
"""
c = configparser.ConfigParser(
defaults=DEFAULTS_DICT, allow_unnamed_section=True, allow_no_value=True
)
with open(conf_file.expanduser()) as f:
c.read_string("[DEFAULT]\n" + f.read())
return _filtered_dict(
{
"task.data": c.get("DEFAULT", "data.location"),
"notes.dir": c.get("DEFAULT", "notes.dir"),
"notes.ext": c.get("DEFAULT", "notes.ext"),
"notes.annot": c.get("DEFAULT", "notes.annot"),
"notes.editor": c.get("DEFAULT", "notes.editor"),
"notes.quiet": c.get("DEFAULT", "notes.quiet"),
}
)
IS_QUIET = False
@ -79,57 +261,14 @@ def whisper(text: str) -> None:
print(text)
def main():
args = parse_cli()
if not args.id:
_ = sys.stderr.write("Please provide task ID as argument.\n")
if args.quiet:
global IS_QUIET
IS_QUIET = True
task = get_task(id=args.id, data_location=args.task_data)
uuid = task["uuid"]
if not uuid:
_ = sys.stderr.write(f"Could not find task for ID: {args.id}.")
sys.exit(1)
fname = get_notes_file(uuid, notes_dir=args.notes_dir, notes_ext=args.extension)
open_editor(fname, editor=args.editor)
add_annotation_if_missing(task, annotation_content=args.annotation)
def _real_path(p: Path | str) -> Path:
return Path(os.path.expandvars(p)).expanduser()
def get_task(id: str, data_location: str = TASK_DATA_DIR) -> Task:
tw = TaskWarrior(data_location)
try:
t = tw.tasks.get(id=id)
except Task.DoesNotExist:
t = tw.tasks.get(uuid=id)
return t
def get_notes_file(
uuid: str, notes_dir: str = TOPEN_DIR, notes_ext: str = TOPEN_EXT
) -> Path:
return Path(notes_dir).joinpath(f"{uuid}.{notes_ext}")
def open_editor(file: Path, editor: str = TOPEN_EDITOR) -> None:
_ = whisper(f"Editing note: {file}")
proc = subprocess.Popen(f"{editor} {file}", shell=True)
_ = proc.wait()
def add_annotation_if_missing(
task: Task, annotation_content: str = TOPEN_ANNOT
) -> None:
for annot in task["annotations"] or []:
if annot["description"] == annotation_content:
return
task.add_annotation(annotation_content)
_ = whisper(f"Added annotation: {annotation_content}")
# A None-filtered dict which only contains
# keys which have a value.
def _filtered_dict(d: dict) -> dict:
return {k: v for (k, v) in d.items() if v}
if __name__ == "__main__":

71
uv.lock generated
View file

@ -2,6 +2,69 @@ version = 1
revision = 1
requires-python = ">=3.13"
[[package]]
name = "jinja2"
version = "3.1.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 },
]
[[package]]
name = "markupsafe"
version = "3.0.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 },
{ url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 },
{ url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 },
{ url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 },
{ url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 },
{ url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 },
{ url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 },
{ url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 },
{ url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 },
{ url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 },
{ url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 },
{ url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 },
{ url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 },
{ url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 },
{ url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 },
{ url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 },
{ url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 },
{ url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 },
{ url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 },
{ url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 },
]
[[package]]
name = "pdoc"
version = "15.0.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jinja2" },
{ name = "markupsafe" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/bf/16/1b542af6f18a27de059f722c487a596681127897b6d31f78e46d6e5bf2fe/pdoc-15.0.1.tar.gz", hash = "sha256:3b08382c9d312243ee6c2a1813d0ff517a6ab84d596fa2c6c6b5255b17c3d666", size = 154174 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2f/4d/60d856a1b12fbf6ac1539efccfa138e57c6b88675c9867d84bbb46455cc1/pdoc-15.0.1-py3-none-any.whl", hash = "sha256:fd437ab8eb55f9b942226af7865a3801e2fb731665199b74fd9a44737dbe20f9", size = 144186 },
]
[[package]]
name = "pygments"
version = "2.19.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 },
]
[[package]]
name = "tasklib"
version = "2.5.1"
@ -16,5 +79,13 @@ dependencies = [
{ name = "tasklib" },
]
[package.dev-dependencies]
dev = [
{ name = "pdoc" },
]
[package.metadata]
requires-dist = [{ name = "tasklib", specifier = ">=2.5.1" }]
[package.metadata.requires-dev]
dev = [{ name = "pdoc", specifier = ">=15.0.1" }]