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:
parent
d6689a6c83
commit
ecdd28e8a3
8 changed files with 513 additions and 17 deletions
44
AGENTS.md
44
AGENTS.md
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
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
|
||||
a human-readable summary view.
|
||||
a human-readable summary view and can fetch notes directly from Joplin.
|
||||
|
||||
### Package layout
|
||||
|
||||
|
|
@ -14,9 +14,10 @@ timesheets/
|
|||
├── AGENTS.md
|
||||
└── src/timesheets/
|
||||
├── 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
|
||||
├── 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.)
|
||||
```
|
||||
|
||||
|
|
@ -27,7 +28,8 @@ tests/
|
|||
├── test_utils.py
|
||||
├── test_parser.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
|
||||
|
||||
# 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
|
||||
uv run timesheets input.md --map /path/to/project_map.json
|
||||
|
|
@ -67,13 +69,44 @@ uv run timesheets input.md --summary
|
|||
|
||||
# Read from stdin
|
||||
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
|
||||
`--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
|
||||
|
||||
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.
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@ authors = [
|
|||
{ name = "Jef Roosens", email = "roosenj@factry.io" }
|
||||
]
|
||||
requires-python = ">=3.13"
|
||||
dependencies = []
|
||||
dependencies = [
|
||||
"joppy>=1.0.2",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
timesheets = "timesheets.cli:main"
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import sys
|
|||
from datetime import date
|
||||
|
||||
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 .utils import format_date
|
||||
|
||||
|
|
@ -13,10 +13,29 @@ def build_parser() -> argparse.ArgumentParser:
|
|||
parser = argparse.ArgumentParser(
|
||||
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",
|
||||
nargs="?",
|
||||
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(
|
||||
"-o",
|
||||
"--output",
|
||||
|
|
@ -44,23 +63,66 @@ def build_parser() -> argparse.ArgumentParser:
|
|||
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:
|
||||
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 == "-":
|
||||
content = sys.stdin.read()
|
||||
else:
|
||||
if args.joplin:
|
||||
# Late import so joppy is only required when --joplin is used
|
||||
from .joplin import fetch_week_note
|
||||
|
||||
token = _resolve_token(args)
|
||||
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)
|
||||
content = fetch_week_note(token, target_date)
|
||||
except RuntimeError as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
lines = content.splitlines()
|
||||
rows = parse_document(lines)
|
||||
lines = content.splitlines()
|
||||
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:
|
||||
print("Warning: no timesheet rows found in input.", file=sys.stderr)
|
||||
|
|
|
|||
68
src/timesheets/joplin.py
Normal file
68
src/timesheets/joplin.py
Normal 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}'"
|
||||
)
|
||||
|
|
@ -1,8 +1,12 @@
|
|||
import re
|
||||
from collections import defaultdict
|
||||
from datetime import date
|
||||
|
||||
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:
|
||||
"""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
|
||||
|
||||
|
||||
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:
|
||||
"""Combine story and note into a single description string."""
|
||||
parts = [p.strip() for p in [story, note] if p.strip()]
|
||||
|
|
|
|||
116
tests/test_joplin.py
Normal file
116
tests/test_joplin.py
Normal 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))
|
||||
|
|
@ -7,6 +7,7 @@ from timesheets.parser import (
|
|||
build_description,
|
||||
detect_has_duration_column,
|
||||
extract_table_blocks,
|
||||
filter_rows_by_date,
|
||||
parse_document,
|
||||
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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
115
uv.lock
generated
115
uv.lock
generated
|
|
@ -2,6 +2,72 @@ version = 1
|
|||
revision = 3
|
||||
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]]
|
||||
name = "colorama"
|
||||
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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "iniconfig"
|
||||
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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "packaging"
|
||||
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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "timesheets"
|
||||
version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "joppy" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
|
|
@ -158,9 +263,19 @@ dev = [
|
|||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "joppy", specifier = ">=1.0.2" }]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "pytest", specifier = ">=9.0.3" },
|
||||
{ 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" },
|
||||
]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue