holded-tt 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- holded_tt-1.0.0/.github/workflows/cd.yml +88 -0
- holded_tt-1.0.0/.github/workflows/ci.yml +212 -0
- holded_tt-1.0.0/.gitignore +50 -0
- holded_tt-1.0.0/AGENTS.md +161 -0
- holded_tt-1.0.0/CLAUDE.md +161 -0
- holded_tt-1.0.0/LICENSE +21 -0
- holded_tt-1.0.0/PKG-INFO +367 -0
- holded_tt-1.0.0/README.md +331 -0
- holded_tt-1.0.0/pyproject.toml +48 -0
- holded_tt-1.0.0/pytest.xml +1 -0
- holded_tt-1.0.0/src/holded_tt/__init__.py +3 -0
- holded_tt-1.0.0/src/holded_tt/auth.py +289 -0
- holded_tt-1.0.0/src/holded_tt/cli.py +120 -0
- holded_tt-1.0.0/src/holded_tt/commands/__init__.py +26 -0
- holded_tt-1.0.0/src/holded_tt/commands/clock.py +200 -0
- holded_tt-1.0.0/src/holded_tt/commands/config.py +123 -0
- holded_tt-1.0.0/src/holded_tt/commands/employee.py +248 -0
- holded_tt-1.0.0/src/holded_tt/commands/employees.py +74 -0
- holded_tt-1.0.0/src/holded_tt/commands/export.py +292 -0
- holded_tt-1.0.0/src/holded_tt/commands/login.py +38 -0
- holded_tt-1.0.0/src/holded_tt/commands/session.py +74 -0
- holded_tt-1.0.0/src/holded_tt/commands/track.py +939 -0
- holded_tt-1.0.0/src/holded_tt/commands/workplaces.py +40 -0
- holded_tt-1.0.0/src/holded_tt/config.py +43 -0
- holded_tt-1.0.0/src/holded_tt/console.py +29 -0
- holded_tt-1.0.0/src/holded_tt/dates.py +40 -0
- holded_tt-1.0.0/src/holded_tt/errors.py +31 -0
- holded_tt-1.0.0/src/holded_tt/exit_codes.py +5 -0
- holded_tt-1.0.0/src/holded_tt/holded_client.py +306 -0
- holded_tt-1.0.0/src/holded_tt/holidays.py +97 -0
- holded_tt-1.0.0/src/holded_tt/paths.py +13 -0
- holded_tt-1.0.0/src/holded_tt/renderers.py +26 -0
- holded_tt-1.0.0/src/holded_tt/session.py +72 -0
- holded_tt-1.0.0/src/holded_tt/state.py +31 -0
- holded_tt-1.0.0/tests/conftest.py +39 -0
- holded_tt-1.0.0/tests/test_auth_core.py +399 -0
- holded_tt-1.0.0/tests/test_cli_surface.py +218 -0
- holded_tt-1.0.0/tests/test_clock_command.py +265 -0
- holded_tt-1.0.0/tests/test_clock_helpers.py +226 -0
- holded_tt-1.0.0/tests/test_dates.py +71 -0
- holded_tt-1.0.0/tests/test_employee_command.py +270 -0
- holded_tt-1.0.0/tests/test_employees_command.py +96 -0
- holded_tt-1.0.0/tests/test_export_command.py +481 -0
- holded_tt-1.0.0/tests/test_holded_client.py +541 -0
- holded_tt-1.0.0/tests/test_holidays.py +263 -0
- holded_tt-1.0.0/tests/test_login_command.py +159 -0
- holded_tt-1.0.0/tests/test_output_contract.py +74 -0
- holded_tt-1.0.0/tests/test_package_metadata.py +34 -0
- holded_tt-1.0.0/tests/test_runtime_storage.py +143 -0
- holded_tt-1.0.0/tests/test_session_command.py +121 -0
- holded_tt-1.0.0/tests/test_track_command.py +1480 -0
- holded_tt-1.0.0/tests/test_workplaces_command.py +49 -0
- holded_tt-1.0.0/uv.lock +505 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
name: CD
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
inputs:
|
|
9
|
+
tag:
|
|
10
|
+
description: "Tag name to release (e.g. v0.1.0)"
|
|
11
|
+
required: true
|
|
12
|
+
type: string
|
|
13
|
+
|
|
14
|
+
env:
|
|
15
|
+
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
ci:
|
|
19
|
+
name: CI gate
|
|
20
|
+
uses: ./.github/workflows/ci.yml
|
|
21
|
+
|
|
22
|
+
publish:
|
|
23
|
+
name: Build and publish to PyPI
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
needs: ci
|
|
26
|
+
environment: pypi
|
|
27
|
+
permissions:
|
|
28
|
+
id-token: write
|
|
29
|
+
steps:
|
|
30
|
+
- name: Checkout
|
|
31
|
+
uses: actions/checkout@v6.0.2
|
|
32
|
+
|
|
33
|
+
- name: Set up Python
|
|
34
|
+
uses: actions/setup-python@v6.2.0
|
|
35
|
+
with:
|
|
36
|
+
python-version: "3.14"
|
|
37
|
+
|
|
38
|
+
- name: Set up uv
|
|
39
|
+
uses: astral-sh/setup-uv@v8.0.0
|
|
40
|
+
with:
|
|
41
|
+
enable-cache: true
|
|
42
|
+
|
|
43
|
+
- name: Build package
|
|
44
|
+
run: uv build
|
|
45
|
+
|
|
46
|
+
- name: Publish to PyPI
|
|
47
|
+
uses: pypa/gh-action-pypi-publish@v1.12.4
|
|
48
|
+
|
|
49
|
+
- name: Upload dist artifacts
|
|
50
|
+
uses: actions/upload-artifact@v4
|
|
51
|
+
with:
|
|
52
|
+
name: dist
|
|
53
|
+
path: dist/
|
|
54
|
+
|
|
55
|
+
release:
|
|
56
|
+
name: Create GitHub release
|
|
57
|
+
runs-on: ubuntu-latest
|
|
58
|
+
needs: publish
|
|
59
|
+
permissions:
|
|
60
|
+
contents: write
|
|
61
|
+
steps:
|
|
62
|
+
- name: Resolve tag
|
|
63
|
+
id: meta
|
|
64
|
+
shell: bash
|
|
65
|
+
run: |
|
|
66
|
+
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
|
|
67
|
+
echo "tag=${{ inputs.tag }}" >> "$GITHUB_OUTPUT"
|
|
68
|
+
else
|
|
69
|
+
echo "tag=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
- name: Download dist artifacts
|
|
73
|
+
uses: actions/download-artifact@v4
|
|
74
|
+
with:
|
|
75
|
+
name: dist
|
|
76
|
+
path: dist/
|
|
77
|
+
|
|
78
|
+
- name: Create GitHub release
|
|
79
|
+
env:
|
|
80
|
+
GH_TOKEN: ${{ github.token }}
|
|
81
|
+
TAG: ${{ steps.meta.outputs.tag }}
|
|
82
|
+
shell: bash
|
|
83
|
+
run: |
|
|
84
|
+
if gh release view "$TAG" >/dev/null 2>&1; then
|
|
85
|
+
gh release upload "$TAG" dist/* --clobber
|
|
86
|
+
else
|
|
87
|
+
gh release create "$TAG" dist/* --title "$TAG" --generate-notes
|
|
88
|
+
fi
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
push:
|
|
8
|
+
branches:
|
|
9
|
+
- main
|
|
10
|
+
workflow_dispatch:
|
|
11
|
+
workflow_call:
|
|
12
|
+
|
|
13
|
+
env:
|
|
14
|
+
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
15
|
+
|
|
16
|
+
permissions:
|
|
17
|
+
contents: read
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
lint:
|
|
21
|
+
name: Lint
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
steps:
|
|
24
|
+
- name: Checkout
|
|
25
|
+
uses: actions/checkout@v6.0.2
|
|
26
|
+
|
|
27
|
+
- name: Set up Python
|
|
28
|
+
uses: actions/setup-python@v6.2.0
|
|
29
|
+
with:
|
|
30
|
+
python-version: "3.14"
|
|
31
|
+
|
|
32
|
+
- name: Set up uv
|
|
33
|
+
uses: astral-sh/setup-uv@v8.0.0
|
|
34
|
+
with:
|
|
35
|
+
enable-cache: true
|
|
36
|
+
|
|
37
|
+
- name: Sync dependencies
|
|
38
|
+
run: uv sync --group dev --locked
|
|
39
|
+
|
|
40
|
+
- name: Ruff check
|
|
41
|
+
run: uv run ruff check .
|
|
42
|
+
|
|
43
|
+
test:
|
|
44
|
+
name: Tests (${{ matrix.os }}, Python ${{ matrix.python-version }})
|
|
45
|
+
runs-on: ${{ matrix.os }}
|
|
46
|
+
strategy:
|
|
47
|
+
fail-fast: false
|
|
48
|
+
matrix:
|
|
49
|
+
os:
|
|
50
|
+
- ubuntu-latest
|
|
51
|
+
- windows-latest
|
|
52
|
+
- macos-latest
|
|
53
|
+
python-version:
|
|
54
|
+
- "3.11"
|
|
55
|
+
- "3.12"
|
|
56
|
+
- "3.13"
|
|
57
|
+
- "3.14"
|
|
58
|
+
steps:
|
|
59
|
+
- name: Checkout
|
|
60
|
+
uses: actions/checkout@v6.0.2
|
|
61
|
+
|
|
62
|
+
- name: Set up Python
|
|
63
|
+
uses: actions/setup-python@v6.2.0
|
|
64
|
+
with:
|
|
65
|
+
python-version: ${{ matrix.python-version }}
|
|
66
|
+
|
|
67
|
+
- name: Set up uv
|
|
68
|
+
uses: astral-sh/setup-uv@v8.0.0
|
|
69
|
+
with:
|
|
70
|
+
enable-cache: true
|
|
71
|
+
|
|
72
|
+
- name: Sync dependencies
|
|
73
|
+
run: uv sync --group dev --locked
|
|
74
|
+
|
|
75
|
+
- name: Run tests
|
|
76
|
+
run: uv run python -m pytest
|
|
77
|
+
|
|
78
|
+
package:
|
|
79
|
+
name: Package smoke test
|
|
80
|
+
runs-on: ubuntu-latest
|
|
81
|
+
needs:
|
|
82
|
+
- lint
|
|
83
|
+
steps:
|
|
84
|
+
- name: Checkout
|
|
85
|
+
uses: actions/checkout@v6.0.2
|
|
86
|
+
|
|
87
|
+
- name: Set up Python
|
|
88
|
+
uses: actions/setup-python@v6.2.0
|
|
89
|
+
with:
|
|
90
|
+
python-version: "3.14"
|
|
91
|
+
|
|
92
|
+
- name: Set up uv
|
|
93
|
+
uses: astral-sh/setup-uv@v8.0.0
|
|
94
|
+
with:
|
|
95
|
+
enable-cache: true
|
|
96
|
+
|
|
97
|
+
- name: Build package
|
|
98
|
+
run: uv build
|
|
99
|
+
|
|
100
|
+
- name: Install built wheel in isolated venv
|
|
101
|
+
run: |
|
|
102
|
+
uv venv .package-venv
|
|
103
|
+
uv pip install --python .package-venv/bin/python dist/*.whl
|
|
104
|
+
|
|
105
|
+
- name: Smoke test installed CLI
|
|
106
|
+
run: |
|
|
107
|
+
.package-venv/bin/holded-tt --version
|
|
108
|
+
.package-venv/bin/holded-tt --help
|
|
109
|
+
.package-venv/bin/holded-tt track --help
|
|
110
|
+
|
|
111
|
+
coverage:
|
|
112
|
+
name: Coverage
|
|
113
|
+
runs-on: ubuntu-latest
|
|
114
|
+
needs:
|
|
115
|
+
- lint
|
|
116
|
+
steps:
|
|
117
|
+
- name: Checkout
|
|
118
|
+
uses: actions/checkout@v6.0.2
|
|
119
|
+
|
|
120
|
+
- name: Set up Python
|
|
121
|
+
uses: actions/setup-python@v6.2.0
|
|
122
|
+
with:
|
|
123
|
+
python-version: "3.14"
|
|
124
|
+
|
|
125
|
+
- name: Set up uv
|
|
126
|
+
uses: astral-sh/setup-uv@v8.0.0
|
|
127
|
+
with:
|
|
128
|
+
enable-cache: true
|
|
129
|
+
|
|
130
|
+
- name: Sync dependencies
|
|
131
|
+
run: uv sync --group dev --locked
|
|
132
|
+
|
|
133
|
+
- name: Run tests with coverage
|
|
134
|
+
run: uv run python -m pytest --junitxml=pytest.xml --cov=holded_tt --cov-report=term-missing --cov-report=markdown:coverage.md
|
|
135
|
+
|
|
136
|
+
- name: Write coverage and test summary
|
|
137
|
+
if: always()
|
|
138
|
+
shell: bash
|
|
139
|
+
run: |
|
|
140
|
+
uv run python - <<'PY'
|
|
141
|
+
import os
|
|
142
|
+
import xml.etree.ElementTree as ET
|
|
143
|
+
from pathlib import Path
|
|
144
|
+
|
|
145
|
+
summary_path = Path(os.environ["GITHUB_STEP_SUMMARY"])
|
|
146
|
+
report_path = Path("pytest.xml")
|
|
147
|
+
coverage_path = Path("coverage.md")
|
|
148
|
+
|
|
149
|
+
with summary_path.open("a", encoding="utf-8") as fh:
|
|
150
|
+
fh.write("## Test summary\n\n")
|
|
151
|
+
if report_path.exists():
|
|
152
|
+
root = ET.parse(report_path).getroot()
|
|
153
|
+
suites = [root] if root.tag == "testsuite" else root.findall(".//testsuite")
|
|
154
|
+
tests = sum(int(float(suite.attrib.get("tests", 0))) for suite in suites)
|
|
155
|
+
failures = sum(int(float(suite.attrib.get("failures", 0))) for suite in suites)
|
|
156
|
+
errors = sum(int(float(suite.attrib.get("errors", 0))) for suite in suites)
|
|
157
|
+
skipped = sum(int(float(suite.attrib.get("skipped", 0))) for suite in suites)
|
|
158
|
+
passed = tests - failures - errors - skipped
|
|
159
|
+
|
|
160
|
+
fh.write(f"- ✅ Passed: **{passed}**\n")
|
|
161
|
+
fh.write(f"- ❌ Failed: **{failures}**\n")
|
|
162
|
+
fh.write(f"- 🚨 Errors: **{errors}**\n")
|
|
163
|
+
fh.write(f"- ⏭️ Skipped: **{skipped}**\n")
|
|
164
|
+
fh.write(f"- 🧪 Total: **{tests}**\n")
|
|
165
|
+
else:
|
|
166
|
+
fh.write("❌ No `pytest.xml` report was generated.\n")
|
|
167
|
+
|
|
168
|
+
fh.write("\n## Coverage summary\n\n")
|
|
169
|
+
if coverage_path.exists():
|
|
170
|
+
fh.write(coverage_path.read_text(encoding="utf-8"))
|
|
171
|
+
else:
|
|
172
|
+
fh.write("❌ No coverage report was generated.\n")
|
|
173
|
+
PY
|
|
174
|
+
|
|
175
|
+
summary:
|
|
176
|
+
name: Workflow summary
|
|
177
|
+
runs-on: ubuntu-latest
|
|
178
|
+
if: always()
|
|
179
|
+
needs:
|
|
180
|
+
- lint
|
|
181
|
+
- test
|
|
182
|
+
- package
|
|
183
|
+
- coverage
|
|
184
|
+
steps:
|
|
185
|
+
- name: Write workflow summary
|
|
186
|
+
shell: bash
|
|
187
|
+
env:
|
|
188
|
+
LINT_RESULT: ${{ needs.lint.result }}
|
|
189
|
+
TEST_RESULT: ${{ needs.test.result }}
|
|
190
|
+
PACKAGE_RESULT: ${{ needs.package.result }}
|
|
191
|
+
COVERAGE_RESULT: ${{ needs.coverage.result }}
|
|
192
|
+
run: |
|
|
193
|
+
status_icon() {
|
|
194
|
+
case "$1" in
|
|
195
|
+
success) echo "✅" ;;
|
|
196
|
+
failure) echo "❌" ;;
|
|
197
|
+
cancelled) echo "⚪" ;;
|
|
198
|
+
skipped) echo "⏭️" ;;
|
|
199
|
+
*) echo "❔" ;;
|
|
200
|
+
esac
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
{
|
|
204
|
+
echo "## CI summary"
|
|
205
|
+
echo
|
|
206
|
+
echo "| Job | Result |"
|
|
207
|
+
echo "| --- | --- |"
|
|
208
|
+
echo "| Lint | $(status_icon "$LINT_RESULT") $LINT_RESULT |"
|
|
209
|
+
echo "| Test matrix | $(status_icon "$TEST_RESULT") $TEST_RESULT |"
|
|
210
|
+
echo "| Package smoke test | $(status_icon "$PACKAGE_RESULT") $PACKAGE_RESULT |"
|
|
211
|
+
echo "| Coverage | $(status_icon "$COVERAGE_RESULT") $COVERAGE_RESULT |"
|
|
212
|
+
} >> "$GITHUB_STEP_SUMMARY"
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.pyo
|
|
5
|
+
*.pyd
|
|
6
|
+
.Python
|
|
7
|
+
*.egg
|
|
8
|
+
*.egg-info/
|
|
9
|
+
dist/
|
|
10
|
+
build/
|
|
11
|
+
eggs/
|
|
12
|
+
parts/
|
|
13
|
+
var/
|
|
14
|
+
sdist/
|
|
15
|
+
wheels/
|
|
16
|
+
*.egg-link
|
|
17
|
+
.installed.cfg
|
|
18
|
+
MANIFEST
|
|
19
|
+
|
|
20
|
+
# Virtual environments
|
|
21
|
+
.venv/
|
|
22
|
+
venv/
|
|
23
|
+
env/
|
|
24
|
+
ENV/
|
|
25
|
+
|
|
26
|
+
# uv
|
|
27
|
+
.uv/
|
|
28
|
+
|
|
29
|
+
# Testing
|
|
30
|
+
.pytest_cache/
|
|
31
|
+
.coverage
|
|
32
|
+
htmlcov/
|
|
33
|
+
.tox/
|
|
34
|
+
|
|
35
|
+
# Type checkers
|
|
36
|
+
.mypy_cache/
|
|
37
|
+
.ruff_cache/
|
|
38
|
+
|
|
39
|
+
# IDE
|
|
40
|
+
.vscode/
|
|
41
|
+
.idea/
|
|
42
|
+
*.swp
|
|
43
|
+
*.swo
|
|
44
|
+
|
|
45
|
+
# OS
|
|
46
|
+
.DS_Store
|
|
47
|
+
Thumbs.db
|
|
48
|
+
|
|
49
|
+
# Planning
|
|
50
|
+
.planning/
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
<!-- GSD:project-start source:PROJECT.md -->
|
|
2
|
+
## Project
|
|
3
|
+
|
|
4
|
+
**Holded TT CLI**
|
|
5
|
+
|
|
6
|
+
A Python CLI tool (`holded-tt`) that automates time-tracking on [Holded](https://app.holded.com) — a Spanish HR SaaS. The primary use case is bulk-registering work days for a date range with a single command, skipping weekends and public holidays automatically. It handles the full authentication flow (email + 2FA) and persists session cookies for subsequent use.
|
|
7
|
+
|
|
8
|
+
**Core Value:** `holded-tt track --from YYYY-MM-DD --to YYYY-MM-DD` registers all working days in a range without manual effort — weekends and holidays filtered automatically.
|
|
9
|
+
|
|
10
|
+
### Constraints
|
|
11
|
+
|
|
12
|
+
- **Tech Stack**: Python 3.11+, Typer, httpx (sync), tomllib (stdlib) / tomli-w, rich — no alternatives
|
|
13
|
+
- **Platform**: Cross-platform (Linux, macOS, Windows); Unix file permissions best-effort
|
|
14
|
+
- **Security**: `session.json` contains sensitive cookies — never logged, permissions 0600 best-effort
|
|
15
|
+
- **API**: Holded internal API — no official SDK, no versioning guarantees; payloads must match browser behavior exactly
|
|
16
|
+
- **Testing**: Unit tests for business logic (date filtering, config parsing, session handling) — no HTTP mocking required
|
|
17
|
+
<!-- GSD:project-end -->
|
|
18
|
+
|
|
19
|
+
<!-- GSD:stack-start source:research/STACK.md -->
|
|
20
|
+
## Technology Stack
|
|
21
|
+
|
|
22
|
+
## Already-Decided Stack (do not re-research)
|
|
23
|
+
| Technology | Role |
|
|
24
|
+
|------------|------|
|
|
25
|
+
| Python 3.11+ | Runtime (stdlib `tomllib` requires 3.11) |
|
|
26
|
+
| Typer | CLI framework |
|
|
27
|
+
| httpx (sync) | HTTP client |
|
|
28
|
+
| tomllib (stdlib) | TOML config reads |
|
|
29
|
+
| tomli-w | TOML config writes |
|
|
30
|
+
| rich | Terminal output |
|
|
31
|
+
## Build Backend
|
|
32
|
+
| Backend | When to use | Verdict |
|
|
33
|
+
|---------|-------------|---------|
|
|
34
|
+
| `uv_build` | uv-only workflows, fastest builds, new projects | Good for pure-uv shops |
|
|
35
|
+
| `hatchling` | Any workflow (pip, pipx, uv), needs no special frontend, wider compatibility | Recommended here |
|
|
36
|
+
## pyproject.toml Structure
|
|
37
|
+
### Entry Point Declaration
|
|
38
|
+
### Src Layout Directory Structure
|
|
39
|
+
## Packaging for pipx / uv tool install
|
|
40
|
+
# Local development
|
|
41
|
+
# or
|
|
42
|
+
# Install as isolated tool (end users)
|
|
43
|
+
# Install from local directory (dev/testing)
|
|
44
|
+
## Cross-Platform Config Directory
|
|
45
|
+
| Platform | `user_config_dir("holded-tt-cli")` returns |
|
|
46
|
+
|----------|-----------------------------------------|
|
|
47
|
+
| Linux | `~/.config/holded-tt-cli` (respects `$XDG_CONFIG_HOME`) |
|
|
48
|
+
| macOS | `~/Library/Application Support/holded-tt-cli` |
|
|
49
|
+
| Windows | `C:\Users\<user>\AppData\Roaming\holded-tt-cli` |
|
|
50
|
+
## Cross-Platform File Permissions (0600 Best-Effort)
|
|
51
|
+
### Pattern
|
|
52
|
+
## pytest Setup for Typer CLI Testing
|
|
53
|
+
### Dependencies
|
|
54
|
+
### Core Pattern
|
|
55
|
+
# tests/conftest.py
|
|
56
|
+
### Test Examples
|
|
57
|
+
# tests/test_cli.py
|
|
58
|
+
### Key CliRunner Behaviors to Know
|
|
59
|
+
| Behavior | Default | How to change |
|
|
60
|
+
|----------|---------|---------------|
|
|
61
|
+
| stdout + stderr mixed | `True` | `CliRunner(mix_stderr=False)` |
|
|
62
|
+
| Filesystem isolation | None | Pass `isolated_filesystem=True` or use `tmp_path` |
|
|
63
|
+
| Exceptions swallowed | `True` | `runner.invoke(app, ..., catch_exceptions=False)` |
|
|
64
|
+
| stdin | Empty | Pass `input="text\n"` string |
|
|
65
|
+
### Testing with Env Vars
|
|
66
|
+
## httpx Sync Patterns
|
|
67
|
+
### Client Setup
|
|
68
|
+
### Cookie Persistence Pattern
|
|
69
|
+
### Redirect Handling
|
|
70
|
+
# requests: follows redirects by default
|
|
71
|
+
# httpx: does NOT follow redirects by default
|
|
72
|
+
# Wrong — will get 302 silently
|
|
73
|
+
# Correct — always set explicitly when redirecting is expected
|
|
74
|
+
### multipart/form-data for Auth
|
|
75
|
+
### Timeout Configuration
|
|
76
|
+
# Recommended: explicit timeout, never rely on default None (infinite wait)
|
|
77
|
+
### Exception Handling
|
|
78
|
+
## Typer + httpx Gotchas
|
|
79
|
+
### Gotcha 1: Exception Swallowing in CliRunner
|
|
80
|
+
### Gotcha 2: Typer Exit Codes
|
|
81
|
+
- `raise typer.Exit(code=0)` — clean exit
|
|
82
|
+
- `raise typer.Exit(code=1)` — error exit
|
|
83
|
+
- `raise typer.Abort()` — "Aborted!" message, exit code 1
|
|
84
|
+
- Unhandled exception — exit code 1, exception stored in `result.exception`
|
|
85
|
+
### Gotcha 3: `invoke_without_command` Default is False
|
|
86
|
+
### Gotcha 4: Rich + CliRunner Output Capture
|
|
87
|
+
### Gotcha 5: httpx Redirects Are Off by Default
|
|
88
|
+
### Gotcha 6: tomllib Opens Files in Binary Mode
|
|
89
|
+
# CORRECT
|
|
90
|
+
# WRONG — will raise TypeError
|
|
91
|
+
## Dependency Versions (as of 2025)
|
|
92
|
+
| Package | Minimum version | Notes |
|
|
93
|
+
|---------|-----------------|-------|
|
|
94
|
+
| typer | `>=0.12` | 0.12 added rich integration by default |
|
|
95
|
+
| httpx | `>=0.28` | Stable API; 0.28 is current stable |
|
|
96
|
+
| tomli-w | `>=1.1` | Stable; no breaking changes expected |
|
|
97
|
+
| rich | `>=13.0` | Stable; 13.x is current major |
|
|
98
|
+
| platformdirs | `>=4.2` | Current is 4.x; `user_config_path` available since 3.x |
|
|
99
|
+
| hatchling | `>=1.24` | Build-time only; not in runtime deps |
|
|
100
|
+
| pytest | `>=8.0` | Dev only |
|
|
101
|
+
| pytest-cov | `>=5.0` | Dev only |
|
|
102
|
+
## Installation Commands Reference
|
|
103
|
+
# Initialize project with uv
|
|
104
|
+
# Add runtime dependencies
|
|
105
|
+
# Add dev dependencies
|
|
106
|
+
# Local editable install (dev)
|
|
107
|
+
# Build distribution
|
|
108
|
+
# Install as tool (end users, isolated)
|
|
109
|
+
# Run tests
|
|
110
|
+
## Sources
|
|
111
|
+
- uv build backend: https://docs.astral.sh/uv/concepts/build-backend/
|
|
112
|
+
- uv project init: https://docs.astral.sh/uv/concepts/projects/init/
|
|
113
|
+
- uv tools: https://docs.astral.sh/uv/concepts/tools/
|
|
114
|
+
- PyPA pyproject.toml guide: https://packaging.python.org/en/latest/guides/writing-pyproject-toml/
|
|
115
|
+
- Typer testing: https://typer.tiangolo.com/tutorial/testing/
|
|
116
|
+
- platformdirs: https://platformdirs.readthedocs.io/
|
|
117
|
+
- httpx redirect behavior: https://www.python-httpx.org/compatibility/
|
|
118
|
+
- httpx cookie persistence discussion: https://github.com/encode/httpx/discussions/2229
|
|
119
|
+
- uv_build vs hatchling comparison: https://medium.com/@dynamicy/python-build-backends-in-2025-what-to-use-and-why-uv-build-vs-hatchling-vs-poetry-core-94dd6b92248f
|
|
120
|
+
- thisdavej CLI packaging walkthrough: https://thisdavej.com/packaging-python-command-line-apps-the-modern-way-with-uv/
|
|
121
|
+
<!-- GSD:stack-end -->
|
|
122
|
+
|
|
123
|
+
<!-- GSD:conventions-start source:CONVENTIONS.md -->
|
|
124
|
+
## Conventions
|
|
125
|
+
|
|
126
|
+
Conventions not yet established. Will populate as patterns emerge during development.
|
|
127
|
+
<!-- GSD:conventions-end -->
|
|
128
|
+
|
|
129
|
+
<!-- GSD:architecture-start source:ARCHITECTURE.md -->
|
|
130
|
+
## Architecture
|
|
131
|
+
|
|
132
|
+
Architecture not yet mapped. Follow existing patterns found in the codebase.
|
|
133
|
+
<!-- GSD:architecture-end -->
|
|
134
|
+
|
|
135
|
+
<!-- GSD:skills-start source:skills/ -->
|
|
136
|
+
## Project Skills
|
|
137
|
+
|
|
138
|
+
No project skills found. Add skills to any of: `.claude/skills/`, `.agents/skills/`, `.cursor/skills/`, or `.github/skills/` with a `SKILL.md` index file.
|
|
139
|
+
<!-- GSD:skills-end -->
|
|
140
|
+
|
|
141
|
+
<!-- GSD:workflow-start source:GSD defaults -->
|
|
142
|
+
## GSD Workflow Enforcement
|
|
143
|
+
|
|
144
|
+
Before using Edit, Write, or other file-changing tools, start work through a GSD command so planning artifacts and execution context stay in sync.
|
|
145
|
+
|
|
146
|
+
Use these entry points:
|
|
147
|
+
- `/gsd-quick` for small fixes, doc updates, and ad-hoc tasks
|
|
148
|
+
- `/gsd-debug` for investigation and bug fixing
|
|
149
|
+
- `/gsd-execute-phase` for planned phase work
|
|
150
|
+
|
|
151
|
+
Do not make direct repo edits outside a GSD workflow unless the user explicitly asks to bypass it.
|
|
152
|
+
<!-- GSD:workflow-end -->
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
<!-- GSD:profile-start -->
|
|
157
|
+
## Developer Profile
|
|
158
|
+
|
|
159
|
+
> Profile not yet configured. Run `/gsd-profile-user` to generate your developer profile.
|
|
160
|
+
> This section is managed by `generate-claude-profile` -- do not edit manually.
|
|
161
|
+
<!-- GSD:profile-end -->
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
<!-- GSD:project-start source:PROJECT.md -->
|
|
2
|
+
## Project
|
|
3
|
+
|
|
4
|
+
**Holded TT CLI**
|
|
5
|
+
|
|
6
|
+
A Python CLI tool (`holded-tt`) that automates time-tracking on [Holded](https://app.holded.com) — a Spanish HR SaaS. The primary use case is bulk-registering work days for a date range with a single command, skipping weekends and public holidays automatically. It handles the full authentication flow (email + 2FA) and persists session cookies for subsequent use.
|
|
7
|
+
|
|
8
|
+
**Core Value:** `holded-tt track --from YYYY-MM-DD --to YYYY-MM-DD` registers all working days in a range without manual effort — weekends and holidays filtered automatically.
|
|
9
|
+
|
|
10
|
+
### Constraints
|
|
11
|
+
|
|
12
|
+
- **Tech Stack**: Python 3.11+, Typer, httpx (sync), tomllib (stdlib) / tomli-w, rich — no alternatives
|
|
13
|
+
- **Platform**: Cross-platform (Linux, macOS, Windows); Unix file permissions best-effort
|
|
14
|
+
- **Security**: `session.json` contains sensitive cookies — never logged, permissions 0600 best-effort
|
|
15
|
+
- **API**: Holded internal API — no official SDK, no versioning guarantees; payloads must match browser behavior exactly
|
|
16
|
+
- **Testing**: Unit tests for business logic (date filtering, config parsing, session handling) — no HTTP mocking required
|
|
17
|
+
<!-- GSD:project-end -->
|
|
18
|
+
|
|
19
|
+
<!-- GSD:stack-start source:research/STACK.md -->
|
|
20
|
+
## Technology Stack
|
|
21
|
+
|
|
22
|
+
## Already-Decided Stack (do not re-research)
|
|
23
|
+
| Technology | Role |
|
|
24
|
+
|------------|------|
|
|
25
|
+
| Python 3.11+ | Runtime (stdlib `tomllib` requires 3.11) |
|
|
26
|
+
| Typer | CLI framework |
|
|
27
|
+
| httpx (sync) | HTTP client |
|
|
28
|
+
| tomllib (stdlib) | TOML config reads |
|
|
29
|
+
| tomli-w | TOML config writes |
|
|
30
|
+
| rich | Terminal output |
|
|
31
|
+
## Build Backend
|
|
32
|
+
| Backend | When to use | Verdict |
|
|
33
|
+
|---------|-------------|---------|
|
|
34
|
+
| `uv_build` | uv-only workflows, fastest builds, new projects | Good for pure-uv shops |
|
|
35
|
+
| `hatchling` | Any workflow (pip, pipx, uv), needs no special frontend, wider compatibility | Recommended here |
|
|
36
|
+
## pyproject.toml Structure
|
|
37
|
+
### Entry Point Declaration
|
|
38
|
+
### Src Layout Directory Structure
|
|
39
|
+
## Packaging for pipx / uv tool install
|
|
40
|
+
# Local development
|
|
41
|
+
# or
|
|
42
|
+
# Install as isolated tool (end users)
|
|
43
|
+
# Install from local directory (dev/testing)
|
|
44
|
+
## Cross-Platform Config Directory
|
|
45
|
+
| Platform | `user_config_dir("holded-tt-cli")` returns |
|
|
46
|
+
|----------|-----------------------------------------|
|
|
47
|
+
| Linux | `~/.config/holded-tt-cli` (respects `$XDG_CONFIG_HOME`) |
|
|
48
|
+
| macOS | `~/Library/Application Support/holded-tt-cli` |
|
|
49
|
+
| Windows | `C:\Users\<user>\AppData\Roaming\holded-tt-cli` |
|
|
50
|
+
## Cross-Platform File Permissions (0600 Best-Effort)
|
|
51
|
+
### Pattern
|
|
52
|
+
## pytest Setup for Typer CLI Testing
|
|
53
|
+
### Dependencies
|
|
54
|
+
### Core Pattern
|
|
55
|
+
# tests/conftest.py
|
|
56
|
+
### Test Examples
|
|
57
|
+
# tests/test_cli.py
|
|
58
|
+
### Key CliRunner Behaviors to Know
|
|
59
|
+
| Behavior | Default | How to change |
|
|
60
|
+
|----------|---------|---------------|
|
|
61
|
+
| stdout + stderr mixed | `True` | `CliRunner(mix_stderr=False)` |
|
|
62
|
+
| Filesystem isolation | None | Pass `isolated_filesystem=True` or use `tmp_path` |
|
|
63
|
+
| Exceptions swallowed | `True` | `runner.invoke(app, ..., catch_exceptions=False)` |
|
|
64
|
+
| stdin | Empty | Pass `input="text\n"` string |
|
|
65
|
+
### Testing with Env Vars
|
|
66
|
+
## httpx Sync Patterns
|
|
67
|
+
### Client Setup
|
|
68
|
+
### Cookie Persistence Pattern
|
|
69
|
+
### Redirect Handling
|
|
70
|
+
# requests: follows redirects by default
|
|
71
|
+
# httpx: does NOT follow redirects by default
|
|
72
|
+
# Wrong — will get 302 silently
|
|
73
|
+
# Correct — always set explicitly when redirecting is expected
|
|
74
|
+
### multipart/form-data for Auth
|
|
75
|
+
### Timeout Configuration
|
|
76
|
+
# Recommended: explicit timeout, never rely on default None (infinite wait)
|
|
77
|
+
### Exception Handling
|
|
78
|
+
## Typer + httpx Gotchas
|
|
79
|
+
### Gotcha 1: Exception Swallowing in CliRunner
|
|
80
|
+
### Gotcha 2: Typer Exit Codes
|
|
81
|
+
- `raise typer.Exit(code=0)` — clean exit
|
|
82
|
+
- `raise typer.Exit(code=1)` — error exit
|
|
83
|
+
- `raise typer.Abort()` — "Aborted!" message, exit code 1
|
|
84
|
+
- Unhandled exception — exit code 1, exception stored in `result.exception`
|
|
85
|
+
### Gotcha 3: `invoke_without_command` Default is False
|
|
86
|
+
### Gotcha 4: Rich + CliRunner Output Capture
|
|
87
|
+
### Gotcha 5: httpx Redirects Are Off by Default
|
|
88
|
+
### Gotcha 6: tomllib Opens Files in Binary Mode
|
|
89
|
+
# CORRECT
|
|
90
|
+
# WRONG — will raise TypeError
|
|
91
|
+
## Dependency Versions (as of 2025)
|
|
92
|
+
| Package | Minimum version | Notes |
|
|
93
|
+
|---------|-----------------|-------|
|
|
94
|
+
| typer | `>=0.12` | 0.12 added rich integration by default |
|
|
95
|
+
| httpx | `>=0.28` | Stable API; 0.28 is current stable |
|
|
96
|
+
| tomli-w | `>=1.1` | Stable; no breaking changes expected |
|
|
97
|
+
| rich | `>=13.0` | Stable; 13.x is current major |
|
|
98
|
+
| platformdirs | `>=4.2` | Current is 4.x; `user_config_path` available since 3.x |
|
|
99
|
+
| hatchling | `>=1.24` | Build-time only; not in runtime deps |
|
|
100
|
+
| pytest | `>=8.0` | Dev only |
|
|
101
|
+
| pytest-cov | `>=5.0` | Dev only |
|
|
102
|
+
## Installation Commands Reference
|
|
103
|
+
# Initialize project with uv
|
|
104
|
+
# Add runtime dependencies
|
|
105
|
+
# Add dev dependencies
|
|
106
|
+
# Local editable install (dev)
|
|
107
|
+
# Build distribution
|
|
108
|
+
# Install as tool (end users, isolated)
|
|
109
|
+
# Run tests
|
|
110
|
+
## Sources
|
|
111
|
+
- uv build backend: https://docs.astral.sh/uv/concepts/build-backend/
|
|
112
|
+
- uv project init: https://docs.astral.sh/uv/concepts/projects/init/
|
|
113
|
+
- uv tools: https://docs.astral.sh/uv/concepts/tools/
|
|
114
|
+
- PyPA pyproject.toml guide: https://packaging.python.org/en/latest/guides/writing-pyproject-toml/
|
|
115
|
+
- Typer testing: https://typer.tiangolo.com/tutorial/testing/
|
|
116
|
+
- platformdirs: https://platformdirs.readthedocs.io/
|
|
117
|
+
- httpx redirect behavior: https://www.python-httpx.org/compatibility/
|
|
118
|
+
- httpx cookie persistence discussion: https://github.com/encode/httpx/discussions/2229
|
|
119
|
+
- uv_build vs hatchling comparison: https://medium.com/@dynamicy/python-build-backends-in-2025-what-to-use-and-why-uv-build-vs-hatchling-vs-poetry-core-94dd6b92248f
|
|
120
|
+
- thisdavej CLI packaging walkthrough: https://thisdavej.com/packaging-python-command-line-apps-the-modern-way-with-uv/
|
|
121
|
+
<!-- GSD:stack-end -->
|
|
122
|
+
|
|
123
|
+
<!-- GSD:conventions-start source:CONVENTIONS.md -->
|
|
124
|
+
## Conventions
|
|
125
|
+
|
|
126
|
+
Conventions not yet established. Will populate as patterns emerge during development.
|
|
127
|
+
<!-- GSD:conventions-end -->
|
|
128
|
+
|
|
129
|
+
<!-- GSD:architecture-start source:ARCHITECTURE.md -->
|
|
130
|
+
## Architecture
|
|
131
|
+
|
|
132
|
+
Architecture not yet mapped. Follow existing patterns found in the codebase.
|
|
133
|
+
<!-- GSD:architecture-end -->
|
|
134
|
+
|
|
135
|
+
<!-- GSD:skills-start source:skills/ -->
|
|
136
|
+
## Project Skills
|
|
137
|
+
|
|
138
|
+
No project skills found. Add skills to any of: `.claude/skills/`, `.agents/skills/`, `.cursor/skills/`, or `.github/skills/` with a `SKILL.md` index file.
|
|
139
|
+
<!-- GSD:skills-end -->
|
|
140
|
+
|
|
141
|
+
<!-- GSD:workflow-start source:GSD defaults -->
|
|
142
|
+
## GSD Workflow Enforcement
|
|
143
|
+
|
|
144
|
+
Before using Edit, Write, or other file-changing tools, start work through a GSD command so planning artifacts and execution context stay in sync.
|
|
145
|
+
|
|
146
|
+
Use these entry points:
|
|
147
|
+
- `/gsd-quick` for small fixes, doc updates, and ad-hoc tasks
|
|
148
|
+
- `/gsd-debug` for investigation and bug fixing
|
|
149
|
+
- `/gsd-execute-phase` for planned phase work
|
|
150
|
+
|
|
151
|
+
Do not make direct repo edits outside a GSD workflow unless the user explicitly asks to bypass it.
|
|
152
|
+
<!-- GSD:workflow-end -->
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
<!-- GSD:profile-start -->
|
|
157
|
+
## Developer Profile
|
|
158
|
+
|
|
159
|
+
> Profile not yet configured. Run `/gsd-profile-user` to generate your developer profile.
|
|
160
|
+
> This section is managed by `generate-claude-profile` -- do not edit manually.
|
|
161
|
+
<!-- GSD:profile-end -->
|
holded_tt-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Villoh
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|