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.
Files changed (53) hide show
  1. holded_tt-1.0.0/.github/workflows/cd.yml +88 -0
  2. holded_tt-1.0.0/.github/workflows/ci.yml +212 -0
  3. holded_tt-1.0.0/.gitignore +50 -0
  4. holded_tt-1.0.0/AGENTS.md +161 -0
  5. holded_tt-1.0.0/CLAUDE.md +161 -0
  6. holded_tt-1.0.0/LICENSE +21 -0
  7. holded_tt-1.0.0/PKG-INFO +367 -0
  8. holded_tt-1.0.0/README.md +331 -0
  9. holded_tt-1.0.0/pyproject.toml +48 -0
  10. holded_tt-1.0.0/pytest.xml +1 -0
  11. holded_tt-1.0.0/src/holded_tt/__init__.py +3 -0
  12. holded_tt-1.0.0/src/holded_tt/auth.py +289 -0
  13. holded_tt-1.0.0/src/holded_tt/cli.py +120 -0
  14. holded_tt-1.0.0/src/holded_tt/commands/__init__.py +26 -0
  15. holded_tt-1.0.0/src/holded_tt/commands/clock.py +200 -0
  16. holded_tt-1.0.0/src/holded_tt/commands/config.py +123 -0
  17. holded_tt-1.0.0/src/holded_tt/commands/employee.py +248 -0
  18. holded_tt-1.0.0/src/holded_tt/commands/employees.py +74 -0
  19. holded_tt-1.0.0/src/holded_tt/commands/export.py +292 -0
  20. holded_tt-1.0.0/src/holded_tt/commands/login.py +38 -0
  21. holded_tt-1.0.0/src/holded_tt/commands/session.py +74 -0
  22. holded_tt-1.0.0/src/holded_tt/commands/track.py +939 -0
  23. holded_tt-1.0.0/src/holded_tt/commands/workplaces.py +40 -0
  24. holded_tt-1.0.0/src/holded_tt/config.py +43 -0
  25. holded_tt-1.0.0/src/holded_tt/console.py +29 -0
  26. holded_tt-1.0.0/src/holded_tt/dates.py +40 -0
  27. holded_tt-1.0.0/src/holded_tt/errors.py +31 -0
  28. holded_tt-1.0.0/src/holded_tt/exit_codes.py +5 -0
  29. holded_tt-1.0.0/src/holded_tt/holded_client.py +306 -0
  30. holded_tt-1.0.0/src/holded_tt/holidays.py +97 -0
  31. holded_tt-1.0.0/src/holded_tt/paths.py +13 -0
  32. holded_tt-1.0.0/src/holded_tt/renderers.py +26 -0
  33. holded_tt-1.0.0/src/holded_tt/session.py +72 -0
  34. holded_tt-1.0.0/src/holded_tt/state.py +31 -0
  35. holded_tt-1.0.0/tests/conftest.py +39 -0
  36. holded_tt-1.0.0/tests/test_auth_core.py +399 -0
  37. holded_tt-1.0.0/tests/test_cli_surface.py +218 -0
  38. holded_tt-1.0.0/tests/test_clock_command.py +265 -0
  39. holded_tt-1.0.0/tests/test_clock_helpers.py +226 -0
  40. holded_tt-1.0.0/tests/test_dates.py +71 -0
  41. holded_tt-1.0.0/tests/test_employee_command.py +270 -0
  42. holded_tt-1.0.0/tests/test_employees_command.py +96 -0
  43. holded_tt-1.0.0/tests/test_export_command.py +481 -0
  44. holded_tt-1.0.0/tests/test_holded_client.py +541 -0
  45. holded_tt-1.0.0/tests/test_holidays.py +263 -0
  46. holded_tt-1.0.0/tests/test_login_command.py +159 -0
  47. holded_tt-1.0.0/tests/test_output_contract.py +74 -0
  48. holded_tt-1.0.0/tests/test_package_metadata.py +34 -0
  49. holded_tt-1.0.0/tests/test_runtime_storage.py +143 -0
  50. holded_tt-1.0.0/tests/test_session_command.py +121 -0
  51. holded_tt-1.0.0/tests/test_track_command.py +1480 -0
  52. holded_tt-1.0.0/tests/test_workplaces_command.py +49 -0
  53. 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 -->
@@ -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.