feat(joplin): add --joplin flag to fetch weekly timesheet note from Joplin

- Add joplin.py with fetch_week_note() that walks Work > Timesheets > YYYY
  and returns the body of the matching YYYY-WNN note via joppy ClientApi
- Add filter_rows_by_date() to parser.py to extract only rows belonging
  to a specific day based on '# ... YYYY-MM-DD' headings in the document
- Update cli.py: input and --joplin are now a mutually exclusive required
  group; add --token flag with JOPLIN_TOKEN env var fallback; --date is
  parsed into a real date object used for both output and day filtering
- Add joppy as a runtime dependency (lazy-imported in cli.py)
- Add tests for filter_rows_by_date and full mocked coverage of joplin.py
- Update AGENTS.md with Joplin usage, notebook structure, and test rules

The actual Joplin structure has notes directly inside the year notebook
(Work > Timesheets > YYYY), not in per-week sub-notebooks as initially
assumed. fetch_week_note() reflects this flat structure.
This commit is contained in:
Jef Roosens 2026-05-22 10:33:00 +02:00
parent d6689a6c83
commit ecdd28e8a3
Signed by: Jef Roosens
GPG key ID: 119385BCAA005C21
8 changed files with 513 additions and 17 deletions

View file

@ -4,7 +4,7 @@
A Python CLI tool that parses markdown pipe-delimited timesheet tables and A Python CLI tool that parses markdown pipe-delimited timesheet tables and
exports them to CSV for import into Odoo (or similar tools). It also supports exports them to CSV for import into Odoo (or similar tools). It also supports
a human-readable summary view. a human-readable summary view and can fetch notes directly from Joplin.
### Package layout ### Package layout
@ -14,9 +14,10 @@ timesheets/
├── AGENTS.md ├── AGENTS.md
└── src/timesheets/ └── src/timesheets/
├── cli.py # argument parsing, main() entry point ├── cli.py # argument parsing, main() entry point
├── parser.py # markdown table parsing and row aggregation ├── parser.py # markdown table parsing, aggregation, date filtering
├── projects.py # project_map.json loading and key resolution ├── projects.py # project_map.json loading and key resolution
├── output.py # CSV writing and summary printing ├── output.py # CSV writing and summary printing
├── joplin.py # Joplin API integration (notebook traversal, note fetching)
└── utils.py # shared low-level helpers (duration parsing, formatting, etc.) └── utils.py # shared low-level helpers (duration parsing, formatting, etc.)
``` ```
@ -27,7 +28,8 @@ tests/
├── test_utils.py ├── test_utils.py
├── test_parser.py ├── test_parser.py
├── test_projects.py ├── test_projects.py
└── test_output.py ├── test_output.py
└── test_joplin.py
``` ```
--- ---
@ -57,7 +59,7 @@ uv run timesheets input.md
uv run timesheets input.md -o output.csv uv run timesheets input.md -o output.csv
# Override the date (DD/MM/YY) # Override the date (DD/MM/YY)
uv run timesheets input.md --date 22/03/26 uv run timesheets input.md --date 22/05/26
# Use a specific project map file # Use a specific project map file
uv run timesheets input.md --map /path/to/project_map.json uv run timesheets input.md --map /path/to/project_map.json
@ -67,13 +69,44 @@ uv run timesheets input.md --summary
# Read from stdin # Read from stdin
cat input.md | uv run timesheets - cat input.md | uv run timesheets -
# Fetch today's entries from Joplin (token via env var)
JOPLIN_TOKEN=your_token uv run timesheets --joplin
# Fetch entries for a specific date from Joplin
uv run timesheets --joplin --date 22/05/26 --token your_token
``` ```
The `--joplin` flag and the file `input` argument are mutually exclusive.
When `--joplin` is used, only entries matching the target date (from `--date`,
or today) are returned, filtered by the `# ... YYYY-MM-DD` day heading in the note.
The API token can be provided via:
- `--token <token>` CLI flag
- `JOPLIN_TOKEN` environment variable
`project_map.json` is auto-discovered in the current working directory if `project_map.json` is auto-discovered in the current working directory if
`--map` is not provided. `--map` is not provided.
--- ---
## Joplin notebook structure
The `--joplin` flag expects the following notebook hierarchy in Joplin:
```
Work/
└── Timesheets/
└── YYYY/
└── YYYY - WNN/ ← notebook per week
└── YYYY - WNN ← note with the same title as the notebook
```
The note body contains one markdown table per day, each preceded by a heading
of the form `# <weekday> - YYYY-MM-DD`.
---
## Testing ## Testing
The test suite uses **pytest** with **pytest-cov** for coverage reporting. The test suite uses **pytest** with **pytest-cov** for coverage reporting.
@ -110,3 +143,6 @@ uv run pytest tests/test_parser.py::TestParseTable::test_empty_input
4. `cli.py` is intentionally excluded from unit tests — it is thin glue code. 4. `cli.py` is intentionally excluded from unit tests — it is thin glue code.
All logic worth testing belongs in the other modules. All logic worth testing belongs in the other modules.
5. Joplin integration tests in `test_joplin.py` must mock `ClientApi` — do not
require a live Joplin instance.

View file

@ -7,7 +7,9 @@ authors = [
{ name = "Jef Roosens", email = "roosenj@factry.io" } { name = "Jef Roosens", email = "roosenj@factry.io" }
] ]
requires-python = ">=3.13" requires-python = ">=3.13"
dependencies = [] dependencies = [
"joppy>=1.0.2",
]
[project.scripts] [project.scripts]
timesheets = "timesheets.cli:main" timesheets = "timesheets.cli:main"

View file

@ -4,7 +4,7 @@ import sys
from datetime import date from datetime import date
from .output import print_summary, write_csv from .output import print_summary, write_csv
from .parser import aggregate_rows, parse_document from .parser import aggregate_rows, filter_rows_by_date, parse_document
from .projects import load_project_map from .projects import load_project_map
from .utils import format_date from .utils import format_date
@ -13,10 +13,29 @@ def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Parse a markdown timesheet table and output a CSV file." description="Parse a markdown timesheet table and output a CSV file."
) )
parser.add_argument(
source = parser.add_mutually_exclusive_group(required=True)
source.add_argument(
"input", "input",
nargs="?",
help="Path to the markdown file containing the timesheet table, or '-' to read from stdin.", help="Path to the markdown file containing the timesheet table, or '-' to read from stdin.",
) )
source.add_argument(
"--joplin",
action="store_true",
default=False,
help=(
"Fetch the weekly timesheet note from Joplin instead of reading a file. "
"Only entries for today (or --date) are included. "
"Requires a Joplin API token via --token or the JOPLIN_TOKEN environment variable."
),
)
parser.add_argument(
"--token",
help="Joplin API token. Falls back to the JOPLIN_TOKEN environment variable.",
default=None,
)
parser.add_argument( parser.add_argument(
"-o", "-o",
"--output", "--output",
@ -44,23 +63,66 @@ def build_parser() -> argparse.ArgumentParser:
return parser return parser
def _resolve_token(args: argparse.Namespace) -> str:
token = args.token or os.environ.get("JOPLIN_TOKEN")
if not token:
print(
"Error: Joplin API token required. "
"Provide --token or set the JOPLIN_TOKEN environment variable.",
file=sys.stderr,
)
sys.exit(1)
return token
def _parse_date(date_str: str | None) -> date:
"""Parse DD/MM/YY date string, or return today."""
if date_str is None:
return date.today()
try:
from datetime import datetime
return datetime.strptime(date_str, "%d/%m/%y").date()
except ValueError:
print(
f"Error: invalid date format {date_str!r}, expected DD/MM/YY.",
file=sys.stderr,
)
sys.exit(1)
def main() -> None: def main() -> None:
args = build_parser().parse_args() args = build_parser().parse_args()
date_str = args.date or format_date(date.today()) target_date = _parse_date(args.date)
date_str = format_date(target_date)
if args.input == "-": if args.joplin:
content = sys.stdin.read() # Late import so joppy is only required when --joplin is used
else: from .joplin import fetch_week_note
token = _resolve_token(args)
try: try:
with open(args.input, "r", encoding="utf-8") as f: content = fetch_week_note(token, target_date)
content = f.read() except RuntimeError as e:
except FileNotFoundError: print(f"Error: {e}", file=sys.stderr)
print(f"Error: file not found: {args.input}", file=sys.stderr)
sys.exit(1) sys.exit(1)
lines = content.splitlines() lines = content.splitlines()
rows = parse_document(lines) rows = filter_rows_by_date(lines, target_date)
else:
if args.input == "-":
content = sys.stdin.read()
else:
try:
with open(args.input, "r", encoding="utf-8") as f:
content = f.read()
except FileNotFoundError:
print(f"Error: file not found: {args.input}", file=sys.stderr)
sys.exit(1)
lines = content.splitlines()
rows = parse_document(lines)
if not rows: if not rows:
print("Warning: no timesheet rows found in input.", file=sys.stderr) print("Warning: no timesheet rows found in input.", file=sys.stderr)

68
src/timesheets/joplin.py Normal file
View file

@ -0,0 +1,68 @@
"""
Joplin integration via the joppy ClientApi.
Actual notebook structure:
Work > Timesheets > YYYY
Notes live directly inside the year notebook and are titled 'YYYY - WNN'.
"""
from datetime import date
from typing import Optional
from joppy.client_api import ClientApi
def _iso_week_label(d: date) -> str:
"""Return the note title for the week containing the given date, e.g. '2026 - W21'."""
year, week, _ = d.isocalendar()
return f"{year} - W{week:02d}"
def _find_notebook(
api: ClientApi, title: str, parent_id: Optional[str] = None
) -> Optional[str]:
"""Return the ID of a notebook matching title (and optionally parent_id), or None."""
for nb in api.get_all_notebooks():
if nb.title == title:
if parent_id is None or nb.parent_id == parent_id:
return nb.id
return None
def fetch_week_note(token: str, target_date: date) -> str:
"""
Fetch the body of the weekly timesheet note from Joplin for the week
containing target_date.
Notebook path: Work > Timesheets > YYYY
Note title: YYYY - WNN
Raises RuntimeError if any notebook or the note cannot be found.
"""
api = ClientApi(token=token)
week_label = _iso_week_label(target_date)
year_str = str(target_date.year)
work_id = _find_notebook(api, "Work")
if work_id is None:
raise RuntimeError("Joplin notebook 'Work' not found")
timesheets_id = _find_notebook(api, "Timesheets", parent_id=work_id)
if timesheets_id is None:
raise RuntimeError("Joplin notebook 'Work > Timesheets' not found")
year_id = _find_notebook(api, year_str, parent_id=timesheets_id)
if year_id is None:
raise RuntimeError(
f"Joplin notebook 'Work > Timesheets > {year_str}' not found"
)
notes = api.get_all_notes(notebook_id=year_id, fields="id,title,body")
for note in notes:
if note.title == week_label:
return note.body
raise RuntimeError(
f"Joplin note '{week_label}' not found in 'Work > Timesheets > {year_str}'"
)

View file

@ -1,8 +1,12 @@
import re import re
from collections import defaultdict from collections import defaultdict
from datetime import date
from .utils import duration_from_start_end, parse_duration, strip_markdown_link from .utils import duration_from_start_end, parse_duration, strip_markdown_link
# Matches a date in YYYY-MM-DD format anywhere in a heading line
_DATE_HEADING_RE = re.compile(r"^#+.*?(\d{4}-\d{2}-\d{2})")
def _is_table_line(line: str) -> bool: def _is_table_line(line: str) -> bool:
"""Return True if the line looks like part of a markdown table.""" """Return True if the line looks like part of a markdown table."""
@ -141,6 +145,37 @@ def parse_document(lines: list[str]) -> list[dict]:
return rows return rows
def filter_rows_by_date(lines: list[str], target: date) -> list[dict]:
"""
Parse a document and return only rows that fall under the heading for
target date. Headings are detected by the pattern '# ... YYYY-MM-DD'.
Rows before the first dated heading are discarded.
"""
target_str = target.strftime("%Y-%m-%d")
sections: list[tuple[str | None, list[str]]] = []
current_date: str | None = None
current_lines: list[str] = []
for line in lines:
m = _DATE_HEADING_RE.match(line)
if m:
if current_lines:
sections.append((current_date, current_lines))
current_date = m.group(1)
current_lines = []
else:
current_lines.append(line)
if current_lines:
sections.append((current_date, current_lines))
rows: list[dict] = []
for section_date, section_lines in sections:
if section_date == target_str:
rows.extend(parse_document(section_lines))
return rows
def build_description(story: str, note: str) -> str: def build_description(story: str, note: str) -> str:
"""Combine story and note into a single description string.""" """Combine story and note into a single description string."""
parts = [p.strip() for p in [story, note] if p.strip()] parts = [p.strip() for p in [story, note] if p.strip()]

116
tests/test_joplin.py Normal file
View file

@ -0,0 +1,116 @@
"""
Tests for the Joplin integration module.
All tests mock the joppy ClientApi so no live Joplin instance is required.
"""
from datetime import date
from unittest.mock import MagicMock, patch
import pytest
from timesheets.joplin import _iso_week_label, fetch_week_note
# ---------------------------------------------------------------------------
# _iso_week_label
# ---------------------------------------------------------------------------
class TestIsoWeekLabel:
@pytest.mark.parametrize(
"d, expected",
[
(date(2026, 5, 22), "2026 - W21"),
(date(2026, 5, 18), "2026 - W21"), # Monday of same week
(date(2026, 1, 1), "2026 - W01"),
(date(2026, 12, 28), "2026 - W53"),
],
)
def test_label(self, d, expected):
assert _iso_week_label(d) == expected
# ---------------------------------------------------------------------------
# fetch_week_note — mocked API
# ---------------------------------------------------------------------------
def _make_notebook(id_, title, parent_id=""):
nb = MagicMock()
nb.id = id_
nb.title = title
nb.parent_id = parent_id
return nb
def _make_note(id_, title, body):
note = MagicMock()
note.id = id_
note.title = title
note.body = body
return note
def _build_api(note_body: str, week_label: str = "2026 - W21"):
"""Return a mocked ClientApi that serves a minimal notebook tree."""
api = MagicMock()
api.get_all_notebooks.return_value = [
_make_notebook("work_id", "Work", parent_id=""),
_make_notebook("ts_id", "Timesheets", parent_id="work_id"),
_make_notebook("yr_id", "2026", parent_id="ts_id"),
]
api.get_all_notes.return_value = [
_make_note("note_id", week_label, note_body),
]
return api
class TestFetchWeekNote:
def _patch(self, api):
return patch("timesheets.joplin.ClientApi", return_value=api)
def test_returns_note_body(self):
api = _build_api("# content")
with self._patch(api):
result = fetch_week_note("dummy_token", date(2026, 5, 22))
assert result == "# content"
def test_constructs_client_with_token(self):
api = _build_api("body")
with patch("timesheets.joplin.ClientApi", return_value=api) as mock_cls:
fetch_week_note("my_secret_token", date(2026, 5, 22))
mock_cls.assert_called_once_with(token="my_secret_token")
def test_missing_work_notebook_raises(self):
api = MagicMock()
api.get_all_notebooks.return_value = []
with self._patch(api):
with pytest.raises(RuntimeError, match="'Work' not found"):
fetch_week_note("token", date(2026, 5, 22))
def test_missing_timesheets_notebook_raises(self):
api = MagicMock()
api.get_all_notebooks.return_value = [
_make_notebook("work_id", "Work", parent_id=""),
]
with self._patch(api):
with pytest.raises(RuntimeError, match="Timesheets"):
fetch_week_note("token", date(2026, 5, 22))
def test_missing_year_notebook_raises(self):
api = MagicMock()
api.get_all_notebooks.return_value = [
_make_notebook("work_id", "Work", parent_id=""),
_make_notebook("ts_id", "Timesheets", parent_id="work_id"),
]
with self._patch(api):
with pytest.raises(RuntimeError, match="2026"):
fetch_week_note("token", date(2026, 5, 22))
def test_missing_note_raises(self):
api = _build_api("body", week_label="2026 - W21")
# Return a note with the wrong title
api.get_all_notes.return_value = [_make_note("x", "wrong title", "body")]
with self._patch(api):
with pytest.raises(RuntimeError, match="note '2026 - W21' not found"):
fetch_week_note("token", date(2026, 5, 22))

View file

@ -7,6 +7,7 @@ from timesheets.parser import (
build_description, build_description,
detect_has_duration_column, detect_has_duration_column,
extract_table_blocks, extract_table_blocks,
filter_rows_by_date,
parse_document, parse_document,
parse_table, parse_table,
) )
@ -215,6 +216,67 @@ class TestParseDocument:
) )
# ---------------------------------------------------------------------------
# filter_rows_by_date
# ---------------------------------------------------------------------------
class TestFilterRowsByDate:
# Reuse the W21 file which has one table per day-heading
with open(WEEK_FILE, encoding="utf-8") as _f:
_WEEK_LINES = _f.read().splitlines()
def test_returns_only_matching_day(self):
from datetime import date
rows = filter_rows_by_date(self._WEEK_LINES, date(2026, 5, 22))
assert len(rows) > 0
# Friday has these projects in the sample file
projects = {r["project"] for r in rows}
assert "scrum" in projects
def test_different_day_returns_different_rows(self):
from datetime import date
rows_fri = filter_rows_by_date(self._WEEK_LINES, date(2026, 5, 22))
rows_mon = filter_rows_by_date(self._WEEK_LINES, date(2026, 5, 18))
assert rows_fri != rows_mon
assert len(rows_mon) > 0
def test_no_match_returns_empty(self):
from datetime import date
rows = filter_rows_by_date(self._WEEK_LINES, date(2026, 1, 1))
assert rows == []
def test_inline_document(self):
from datetime import date
lines = [
"# Maandag - 2026-05-18",
"| Start | End | Project | Story | Note |",
"|-------|-------|---------|-------|------||",
"| 08:00 | 08:30 | bugs | | fix |",
"",
"# Dinsdag - 2026-05-19",
"| Start | End | Project | Story | Note |",
"|-------|-------|---------|-------|------||",
"| 09:00 | 09:30 | scrum | | dsu |",
]
rows = filter_rows_by_date(lines, date(2026, 5, 18))
assert len(rows) == 1
assert rows[0]["project"] == "bugs"
rows = filter_rows_by_date(lines, date(2026, 5, 19))
assert len(rows) == 1
assert rows[0]["project"] == "scrum"
def test_empty_input(self):
from datetime import date
assert filter_rows_by_date([], date(2026, 5, 22)) == []
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# build_description # build_description
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------

115
uv.lock generated
View file

@ -2,6 +2,72 @@ version = 1
revision = 3 revision = 3
requires-python = ">=3.13" requires-python = ">=3.13"
[[package]]
name = "certifi"
version = "2026.5.20"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" },
]
[[package]]
name = "charset-normalizer"
version = "3.4.7"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" },
{ url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" },
{ url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" },
{ url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" },
{ url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" },
{ url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" },
{ url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" },
{ url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" },
{ url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" },
{ url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" },
{ url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" },
{ url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" },
{ url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" },
{ url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" },
{ url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" },
{ url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" },
{ url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" },
{ url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" },
{ url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" },
{ url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" },
{ url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" },
{ url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" },
{ url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" },
{ url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" },
{ url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" },
{ url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" },
{ url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" },
{ url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" },
{ url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" },
{ url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" },
{ url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" },
{ url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" },
{ url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" },
{ url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" },
{ url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" },
{ url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" },
{ url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" },
{ url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" },
{ url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" },
{ url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" },
{ url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" },
{ url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" },
{ url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" },
{ url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" },
{ url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" },
{ url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" },
{ url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" },
{ url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" },
{ url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" },
]
[[package]] [[package]]
name = "colorama" name = "colorama"
version = "0.4.6" version = "0.4.6"
@ -80,6 +146,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/61/e8/cb8e80d6f9f55b99588625062822bf946cf03ed06315df4bd8397f5632a1/coverage-7.14.0-py3-none-any.whl", hash = "sha256:8de5b61163aee3d05c8a2beab6f47913df7981dad1baf82c414d99158c286ab1", size = 211764, upload-time = "2026-05-10T18:02:29.538Z" }, { url = "https://files.pythonhosted.org/packages/61/e8/cb8e80d6f9f55b99588625062822bf946cf03ed06315df4bd8397f5632a1/coverage-7.14.0-py3-none-any.whl", hash = "sha256:8de5b61163aee3d05c8a2beab6f47913df7981dad1baf82c414d99158c286ab1", size = 211764, upload-time = "2026-05-10T18:02:29.538Z" },
] ]
[[package]]
name = "idna"
version = "3.16"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1a/88/bcf9709822fe69d02c2a6a77956c98ce6ea8ca8767a9aadcedc7eb6a2390/idna-3.16.tar.gz", hash = "sha256:d7a6da03db833450fca25d2358ac9ff06cd624577a4aea3a596d5c0f77b8e03d", size = 203770, upload-time = "2026-05-22T00:16:18.781Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/94/16/70255075a9859a0e3adb789b68ceb0e210dec03934245fd98d248226572f/idna-3.16-py3-none-any.whl", hash = "sha256:cc246e3a3f89580c3a951b5ad298ca4638078b2cdd4f115654332b5c26daded5", size = 74165, upload-time = "2026-05-22T00:16:16.698Z" },
]
[[package]] [[package]]
name = "iniconfig" name = "iniconfig"
version = "2.3.0" version = "2.3.0"
@ -89,6 +164,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
] ]
[[package]]
name = "joppy"
version = "1.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "requests" },
]
sdist = { url = "https://files.pythonhosted.org/packages/44/f9/3f6691ecba05f0695246a87940f9d2ea556f64d05ca1490fbcfa9797dee9/joppy-1.0.2.tar.gz", hash = "sha256:f9f8650b088bc5bf7230580c7207aea548731429072c834e5dbfa2d517d06b3a", size = 36759, upload-time = "2026-03-10T19:13:52.634Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/62/ca/79f80b83614b9c3f8125e5d5f3ab15491fe266fafb8b07d3420b6febbf30/joppy-1.0.2-py3-none-any.whl", hash = "sha256:a82c5d2952845eb33521a37dcc83baf660b92e46bddfffdd1070c8521fc62ee2", size = 25417, upload-time = "2026-03-10T19:13:50.775Z" },
]
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "26.2" version = "26.2"
@ -146,10 +233,28 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" },
] ]
[[package]]
name = "requests"
version = "2.34.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" },
]
[[package]] [[package]]
name = "timesheets" name = "timesheets"
version = "0.1.0" version = "0.1.0"
source = { editable = "." } source = { editable = "." }
dependencies = [
{ name = "joppy" },
]
[package.dev-dependencies] [package.dev-dependencies]
dev = [ dev = [
@ -158,9 +263,19 @@ dev = [
] ]
[package.metadata] [package.metadata]
requires-dist = [{ name = "joppy", specifier = ">=1.0.2" }]
[package.metadata.requires-dev] [package.metadata.requires-dev]
dev = [ dev = [
{ name = "pytest", specifier = ">=9.0.3" }, { name = "pytest", specifier = ">=9.0.3" },
{ name = "pytest-cov", specifier = ">=7.1.0" }, { name = "pytest-cov", specifier = ">=7.1.0" },
] ]
[[package]]
name = "urllib3"
version = "2.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
]