pyholded 0.1.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.
- pyholded-0.1.0/.gitattributes +6 -0
- pyholded-0.1.0/.github/workflows/ci.yml +93 -0
- pyholded-0.1.0/.gitignore +45 -0
- pyholded-0.1.0/LICENSE +21 -0
- pyholded-0.1.0/Makefile +49 -0
- pyholded-0.1.0/PKG-INFO +376 -0
- pyholded-0.1.0/README.md +339 -0
- pyholded-0.1.0/pyproject.toml +104 -0
- pyholded-0.1.0/requirements.lock +69 -0
- pyholded-0.1.0/sbom.cdx.json +1010 -0
- pyholded-0.1.0/scripts/_sbom_signing.py +29 -0
- pyholded-0.1.0/scripts/generate_sbom.py +347 -0
- pyholded-0.1.0/scripts/verify_sbom.py +57 -0
- pyholded-0.1.0/src/pyholded/__init__.py +55 -0
- pyholded-0.1.0/src/pyholded/_proxies.py +73 -0
- pyholded-0.1.0/src/pyholded/_registry.py +84 -0
- pyholded-0.1.0/src/pyholded/cli.py +329 -0
- pyholded-0.1.0/src/pyholded/client.py +159 -0
- pyholded-0.1.0/src/pyholded/config.py +196 -0
- pyholded-0.1.0/src/pyholded/endpoints.py +210 -0
- pyholded-0.1.0/src/pyholded/exceptions.py +43 -0
- pyholded-0.1.0/src/pyholded/multi.py +98 -0
- pyholded-0.1.0/src/pyholded/output.py +97 -0
- pyholded-0.1.0/src/pyholded/py.typed +0 -0
- pyholded-0.1.0/src/pyholded/transport.py +139 -0
- pyholded-0.1.0/tests/__init__.py +0 -0
- pyholded-0.1.0/tests/conftest.py +10 -0
- pyholded-0.1.0/tests/test_cli.py +231 -0
- pyholded-0.1.0/tests/test_client.py +192 -0
- pyholded-0.1.0/tests/test_config.py +126 -0
- pyholded-0.1.0/tests/test_multi_account.py +154 -0
- pyholded-0.1.0/tests/test_output.py +66 -0
- pyholded-0.1.0/tests/test_package.py +23 -0
- pyholded-0.1.0/tests/test_registry.py +59 -0
- pyholded-0.1.0/tests/test_sbom_script.py +39 -0
- pyholded-0.1.0/tests/test_transport.py +64 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
gate:
|
|
14
|
+
name: Static gate (lint/format/types/security)
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
- name: Set up Python 3.14
|
|
19
|
+
uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: "3.14"
|
|
22
|
+
- name: Install
|
|
23
|
+
run: |
|
|
24
|
+
python -m pip install --upgrade pip
|
|
25
|
+
pip install -e ".[dev]"
|
|
26
|
+
- name: Lint (ruff)
|
|
27
|
+
run: python -m ruff check .
|
|
28
|
+
- name: Format (black)
|
|
29
|
+
run: python -m black --check --diff .
|
|
30
|
+
- name: Types (mypy --strict)
|
|
31
|
+
run: python -m mypy
|
|
32
|
+
- name: Security (bandit)
|
|
33
|
+
run: python -m bandit -rq src -c pyproject.toml
|
|
34
|
+
- name: Dead code (vulture)
|
|
35
|
+
run: python -m vulture src/
|
|
36
|
+
- name: Dependency audit (pip-audit)
|
|
37
|
+
run: python -m pip_audit -r requirements.lock --strict
|
|
38
|
+
|
|
39
|
+
test:
|
|
40
|
+
name: Tests (${{ matrix.os }})
|
|
41
|
+
strategy:
|
|
42
|
+
fail-fast: false
|
|
43
|
+
matrix:
|
|
44
|
+
os: [ubuntu-latest, windows-latest, macos-latest]
|
|
45
|
+
runs-on: ${{ matrix.os }}
|
|
46
|
+
steps:
|
|
47
|
+
- uses: actions/checkout@v4
|
|
48
|
+
- name: Set up Python 3.14
|
|
49
|
+
uses: actions/setup-python@v5
|
|
50
|
+
with:
|
|
51
|
+
python-version: "3.14"
|
|
52
|
+
- name: Install
|
|
53
|
+
run: |
|
|
54
|
+
python -m pip install --upgrade pip
|
|
55
|
+
pip install -e ".[dev]"
|
|
56
|
+
- name: Tests (pytest)
|
|
57
|
+
run: python -m pytest
|
|
58
|
+
|
|
59
|
+
sbom:
|
|
60
|
+
name: SBOM + quality score
|
|
61
|
+
runs-on: ubuntu-latest
|
|
62
|
+
needs: [gate, test]
|
|
63
|
+
env:
|
|
64
|
+
SBOM_MIN_SCORE: "8.0"
|
|
65
|
+
steps:
|
|
66
|
+
- uses: actions/checkout@v4
|
|
67
|
+
- name: Set up Python 3.14
|
|
68
|
+
uses: actions/setup-python@v5
|
|
69
|
+
with:
|
|
70
|
+
python-version: "3.14"
|
|
71
|
+
- name: Install
|
|
72
|
+
run: |
|
|
73
|
+
python -m pip install --upgrade pip
|
|
74
|
+
pip install -e ".[dev]"
|
|
75
|
+
- name: Install sbomqs
|
|
76
|
+
run: |
|
|
77
|
+
go install github.com/interlynk-io/sbomqs@latest
|
|
78
|
+
echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH"
|
|
79
|
+
- name: Generate signed SBOM
|
|
80
|
+
run: python scripts/generate_sbom.py --sign --output sbom.cdx.json
|
|
81
|
+
- name: Verify SBOM signature
|
|
82
|
+
run: python scripts/verify_sbom.py sbom.cdx.json
|
|
83
|
+
- name: Score SBOM (fail below threshold)
|
|
84
|
+
run: |
|
|
85
|
+
sbomqs score sbom.cdx.json
|
|
86
|
+
score=$(sbomqs score sbom.cdx.json --basic | cut -f1)
|
|
87
|
+
awk -v s="$score" -v m="$SBOM_MIN_SCORE" 'BEGIN { exit (s+0 < m+0) }' \
|
|
88
|
+
|| { echo "SBOM score $score below $SBOM_MIN_SCORE"; exit 1; }
|
|
89
|
+
- name: Upload SBOM artifact
|
|
90
|
+
uses: actions/upload-artifact@v4
|
|
91
|
+
with:
|
|
92
|
+
name: sbom-cyclonedx
|
|
93
|
+
path: sbom.cdx.json
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Claude Code
|
|
2
|
+
CLAUDE.md
|
|
3
|
+
.claude/
|
|
4
|
+
|
|
5
|
+
# Signing — never commit private keys (the public key is embedded in the SBOM)
|
|
6
|
+
signing/signing-key.pem
|
|
7
|
+
signing/cosign.key
|
|
8
|
+
*.key
|
|
9
|
+
|
|
10
|
+
# Virtual environments
|
|
11
|
+
venv/
|
|
12
|
+
.venv/
|
|
13
|
+
env/
|
|
14
|
+
ENV/
|
|
15
|
+
|
|
16
|
+
# Python bytecode / caches
|
|
17
|
+
__pycache__/
|
|
18
|
+
*.py[cod]
|
|
19
|
+
*$py.class
|
|
20
|
+
*.so
|
|
21
|
+
|
|
22
|
+
# Packaging / build
|
|
23
|
+
build/
|
|
24
|
+
dist/
|
|
25
|
+
*.egg-info/
|
|
26
|
+
*.egg
|
|
27
|
+
.eggs/
|
|
28
|
+
pip-wheel-metadata/
|
|
29
|
+
|
|
30
|
+
# Test / coverage / tooling caches
|
|
31
|
+
.pytest_cache/
|
|
32
|
+
.mypy_cache/
|
|
33
|
+
.ruff_cache/
|
|
34
|
+
.tox/
|
|
35
|
+
.nox/
|
|
36
|
+
.coverage
|
|
37
|
+
.coverage.*
|
|
38
|
+
htmlcov/
|
|
39
|
+
coverage.xml
|
|
40
|
+
|
|
41
|
+
# Editors / OS
|
|
42
|
+
.idea/
|
|
43
|
+
.vscode/
|
|
44
|
+
*.swp
|
|
45
|
+
.DS_Store
|
pyholded-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Marc Rivero López
|
|
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.
|
pyholded-0.1.0/Makefile
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
.PHONY: install gate lint format types security deadcode audit test \
|
|
2
|
+
sbom sbom-score sbom-verify lock all
|
|
3
|
+
|
|
4
|
+
VENV ?= venv
|
|
5
|
+
PY := $(VENV)/bin/python
|
|
6
|
+
SBOM := sbom.cdx.json
|
|
7
|
+
SBOM_MIN_SCORE ?= 9.0
|
|
8
|
+
|
|
9
|
+
install:
|
|
10
|
+
$(PY) -m pip install -e ".[dev]"
|
|
11
|
+
|
|
12
|
+
lint:
|
|
13
|
+
$(PY) -m ruff check .
|
|
14
|
+
|
|
15
|
+
format:
|
|
16
|
+
$(PY) -m black --check --diff .
|
|
17
|
+
|
|
18
|
+
types:
|
|
19
|
+
$(PY) -m mypy
|
|
20
|
+
|
|
21
|
+
security:
|
|
22
|
+
$(PY) -m bandit -rq src -c pyproject.toml
|
|
23
|
+
|
|
24
|
+
deadcode:
|
|
25
|
+
$(PY) -m vulture src/
|
|
26
|
+
|
|
27
|
+
audit:
|
|
28
|
+
$(PY) -m pip_audit -r requirements.lock --strict
|
|
29
|
+
|
|
30
|
+
test:
|
|
31
|
+
$(PY) -m pytest
|
|
32
|
+
|
|
33
|
+
gate: lint format types security deadcode test
|
|
34
|
+
|
|
35
|
+
lock:
|
|
36
|
+
uv pip compile pyproject.toml --generate-hashes -o requirements.lock
|
|
37
|
+
|
|
38
|
+
sbom:
|
|
39
|
+
$(PY) scripts/generate_sbom.py --sign --output $(SBOM)
|
|
40
|
+
|
|
41
|
+
sbom-verify:
|
|
42
|
+
$(PY) scripts/verify_sbom.py $(SBOM)
|
|
43
|
+
|
|
44
|
+
sbom-score: sbom
|
|
45
|
+
sbomqs score $(SBOM)
|
|
46
|
+
@score=$$(sbomqs score $(SBOM) --basic | cut -f1); \
|
|
47
|
+
awk -v s=$$score -v m=$(SBOM_MIN_SCORE) 'BEGIN { if (s+0 < m+0) { printf "SBOM score %.1f below minimum %.1f\n", s, m; exit 1 } printf "SBOM score %.1f >= %.1f OK\n", s, m }'
|
|
48
|
+
|
|
49
|
+
all: gate sbom-score sbom-verify
|
pyholded-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyholded
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Modular Python client and CLI for the complete Holded API, with rich, JSON and TOON output.
|
|
5
|
+
Project-URL: Homepage, https://github.com/seifreed/pyholded
|
|
6
|
+
Project-URL: Documentation, https://developers.holded.com
|
|
7
|
+
Project-URL: Issues, https://github.com/seifreed/pyholded/issues
|
|
8
|
+
Author-email: Marc Rivero López <mriverolopez@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: accounting,api,cli,client,crm,holded,invoicing
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Classifier: Topic :: Office/Business :: Financial :: Accounting
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Python: <3.15,>=3.14
|
|
19
|
+
Requires-Dist: click>=8.1
|
|
20
|
+
Requires-Dist: httpx>=0.28
|
|
21
|
+
Requires-Dist: rich>=13.9
|
|
22
|
+
Requires-Dist: toons>=0.7
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: bandit; extra == 'dev'
|
|
25
|
+
Requires-Dist: black; extra == 'dev'
|
|
26
|
+
Requires-Dist: coverage; extra == 'dev'
|
|
27
|
+
Requires-Dist: cryptography; extra == 'dev'
|
|
28
|
+
Requires-Dist: cyclonedx-bom; extra == 'dev'
|
|
29
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
30
|
+
Requires-Dist: pip-audit; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest-httpx; extra == 'dev'
|
|
34
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
35
|
+
Requires-Dist: vulture; extra == 'dev'
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
<p align="center">
|
|
39
|
+
<img src="https://img.shields.io/badge/pyholded-Holded%20API%20v2%20client-blue?style=for-the-badge" alt="pyholded">
|
|
40
|
+
</p>
|
|
41
|
+
|
|
42
|
+
<h1 align="center">pyholded</h1>
|
|
43
|
+
|
|
44
|
+
<p align="center">
|
|
45
|
+
<strong>Modular Python client and CLI for the complete Holded API v2</strong>
|
|
46
|
+
</p>
|
|
47
|
+
|
|
48
|
+
<p align="center">
|
|
49
|
+
<a href="https://pypi.org/project/pyholded/"><img src="https://img.shields.io/pypi/v/pyholded?style=flat-square&logo=pypi&logoColor=white" alt="PyPI Version"></a>
|
|
50
|
+
<a href="https://pypi.org/project/pyholded/"><img src="https://img.shields.io/pypi/pyversions/pyholded?style=flat-square&logo=python&logoColor=white" alt="Python Versions"></a>
|
|
51
|
+
<a href="https://github.com/seifreed/pyholded/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="License"></a>
|
|
52
|
+
<a href="https://github.com/seifreed/pyholded/actions"><img src="https://img.shields.io/github/actions/workflow/status/seifreed/pyholded/ci.yml?style=flat-square&logo=github&label=CI" alt="CI Status"></a>
|
|
53
|
+
<a href="https://github.com/seifreed/pyholded"><img src="https://img.shields.io/badge/types-py.typed-brightgreen?style=flat-square" alt="Typed"></a>
|
|
54
|
+
<a href="https://github.com/seifreed/pyholded/blob/main/sbom.cdx.json"><img src="https://img.shields.io/badge/SBOM-CycloneDX%209.3%2FA%20signed-brightgreen?style=flat-square" alt="SBOM"></a>
|
|
55
|
+
</p>
|
|
56
|
+
|
|
57
|
+
<p align="center">
|
|
58
|
+
<a href="https://github.com/seifreed/pyholded/stargazers"><img src="https://img.shields.io/github/stars/seifreed/pyholded?style=flat-square" alt="GitHub Stars"></a>
|
|
59
|
+
<a href="https://github.com/seifreed/pyholded/issues"><img src="https://img.shields.io/github/issues/seifreed/pyholded?style=flat-square" alt="GitHub Issues"></a>
|
|
60
|
+
<a href="https://buymeacoffee.com/seifreed"><img src="https://img.shields.io/badge/Buy%20Me%20a%20Coffee-support-yellow?style=flat-square&logo=buy-me-a-coffee&logoColor=white" alt="Buy Me a Coffee"></a>
|
|
61
|
+
</p>
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Overview
|
|
66
|
+
|
|
67
|
+
**pyholded** is a Python toolkit to talk to the [Holded](https://developers.holded.com)
|
|
68
|
+
business-management API (v2). Every endpoint across all modules — sales/purchase
|
|
69
|
+
documents, contacts, products, CRM, projects and team — is described in a single
|
|
70
|
+
declarative registry from which both the typed client and the CLI are generated.
|
|
71
|
+
Results print as rich tables, JSON, or TOON.
|
|
72
|
+
|
|
73
|
+
### Key Features
|
|
74
|
+
|
|
75
|
+
| Feature | Description |
|
|
76
|
+
|---------|-------------|
|
|
77
|
+
| **Registry-driven** | Every endpoint is data; client and CLI share one source of truth |
|
|
78
|
+
| **Full v2 surface** | Documents, contacts, products, CRM, projects, team — plus a `raw` escape hatch |
|
|
79
|
+
| **Bearer auth** | Token from `--token`, environment variable, or TOML config file |
|
|
80
|
+
| **Cursor pagination** | `--all` (CLI) / `paginate=True` (library) merges every page |
|
|
81
|
+
| **Three outputs** | Rich tables, JSON, and TOON (token-efficient for LLMs) |
|
|
82
|
+
| **CLI + Library** | Use as a command-line tool or a typed Python package (`py.typed`) |
|
|
83
|
+
| **Strict quality gate** | ruff, black, mypy (strict), bandit, vulture, pip-audit — zero suppressions |
|
|
84
|
+
|
|
85
|
+
### Supported Outputs
|
|
86
|
+
|
|
87
|
+
```text
|
|
88
|
+
Records rich tables, JSON, TOON
|
|
89
|
+
Pagination cursor-based ({items, cursor, has_more}); --all merges pages
|
|
90
|
+
Auth Authorization: Bearer <PAT> via env var or config file
|
|
91
|
+
Binary PDF download for any document type (invoices, credit-notes, ...)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Installation
|
|
97
|
+
|
|
98
|
+
### From PyPI (Recommended)
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
pip install pyholded
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### From Source
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
git clone https://github.com/seifreed/pyholded.git
|
|
108
|
+
cd pyholded
|
|
109
|
+
python3 -m venv venv
|
|
110
|
+
source venv/bin/activate # Windows: venv\Scripts\activate
|
|
111
|
+
pip install -e .
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Optional Extras
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
pip install "pyholded[dev]" # ruff, black, mypy, bandit, vulture, pip-audit, pytest
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Authentication
|
|
123
|
+
|
|
124
|
+
Holded API v2 uses a Personal Access Token (`pat_…`) sent as `Authorization: Bearer`.
|
|
125
|
+
Generate one in Holded: **Settings → Developers → Credentials → Add API Token**.
|
|
126
|
+
|
|
127
|
+
The token is resolved in order of precedence:
|
|
128
|
+
|
|
129
|
+
1. An explicit value (`--token`, or `HoldedClient(token=...)`).
|
|
130
|
+
2. The `HOLDED_TOKEN` environment variable (`HOLDED_API_KEY` also accepted).
|
|
131
|
+
3. A TOML config file (`--config`, `HOLDED_CONFIG`, or `~/.config/pyholded/config.toml`).
|
|
132
|
+
|
|
133
|
+
```toml
|
|
134
|
+
# ~/.config/pyholded/config.toml
|
|
135
|
+
[holded]
|
|
136
|
+
token = "pat_xxx_yyy"
|
|
137
|
+
# base_url = "https://api.holded.com/api/v2/" # optional override
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Multiple accounts
|
|
141
|
+
|
|
142
|
+
Configure several Holded accounts and query one or all of them.
|
|
143
|
+
|
|
144
|
+
Environment variables — `HOLDED_TOKEN` is the `default` account; `HOLDED_TOKEN_<NAME>`
|
|
145
|
+
adds a named account:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
export HOLDED_TOKEN="pat_default"
|
|
149
|
+
export HOLDED_TOKEN_ACME="pat_acme"
|
|
150
|
+
export HOLDED_TOKEN_PERSONAL="pat_personal"
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Config file — per-account tables (env overrides the file for the same name):
|
|
154
|
+
|
|
155
|
+
```toml
|
|
156
|
+
# ~/.config/pyholded/config.toml
|
|
157
|
+
default_account = "acme" # optional; picks the account when none is given
|
|
158
|
+
|
|
159
|
+
[accounts.acme]
|
|
160
|
+
token = "pat_acme"
|
|
161
|
+
# base_url = "..." # optional, per account
|
|
162
|
+
|
|
163
|
+
[accounts.personal]
|
|
164
|
+
token = "pat_personal"
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Select with `--account <name>` (CLI) / `HoldedClient(account="acme")` (library), or fan
|
|
168
|
+
out to every account with `--all-accounts` / `MultiClient`. When several accounts are
|
|
169
|
+
configured and none is selected, set `default_account` or pass one explicitly.
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Quick Start
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
# List every resource and its operations
|
|
177
|
+
holded resources
|
|
178
|
+
|
|
179
|
+
# List records (pretty table by default)
|
|
180
|
+
holded contacts list --limit 5
|
|
181
|
+
|
|
182
|
+
# TOON output, ideal for LLM contexts
|
|
183
|
+
holded taxes list -o toon
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Usage
|
|
189
|
+
|
|
190
|
+
### Command Line Interface
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
# List a page, or follow the cursor and fetch all pages
|
|
194
|
+
holded invoices list --limit 50
|
|
195
|
+
holded expenses_accounts list --all -o json
|
|
196
|
+
|
|
197
|
+
# Get one record, in JSON
|
|
198
|
+
holded contacts get --id 0123456789abcdef01234567 -o json
|
|
199
|
+
|
|
200
|
+
# Download a document PDF (binary)
|
|
201
|
+
holded invoices get-pdf --id 89abcdef0123456789abcdef > invoice.pdf
|
|
202
|
+
|
|
203
|
+
# Create from inline JSON, a file, or key=value fields
|
|
204
|
+
holded contacts create --data '{"name": "ACME SL"}'
|
|
205
|
+
holded contacts create --data @contact.json
|
|
206
|
+
holded contacts create --field name=ACME --field code=B12345678
|
|
207
|
+
|
|
208
|
+
# Multiple accounts
|
|
209
|
+
holded accounts # list configured accounts
|
|
210
|
+
holded --account acme contacts list # one named account
|
|
211
|
+
holded --all-accounts contacts list -o json # fan out -> {account: result}
|
|
212
|
+
|
|
213
|
+
# Call any endpoint directly
|
|
214
|
+
holded raw GET taxes -o toon
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Main Commands
|
|
218
|
+
|
|
219
|
+
| Command | Description |
|
|
220
|
+
|--------|-------------|
|
|
221
|
+
| `holded resources` | List all resources and their operations |
|
|
222
|
+
| `holded accounts` | List configured accounts (names + base URLs; tokens never shown) |
|
|
223
|
+
| `holded <resource> list` | List records (cursor-paginated; `--all` fetches every page) |
|
|
224
|
+
| `holded <resource> get --id <id>` | Get a single record |
|
|
225
|
+
| `holded <resource> create --data <json>` | Create a record |
|
|
226
|
+
| `holded <resource> update --id <id> --data <json>` | Update a record |
|
|
227
|
+
| `holded <resource> delete --id <id>` | Delete a record |
|
|
228
|
+
| `holded invoices get-pdf --id <id>` | Download a document PDF (also `send`) |
|
|
229
|
+
| `holded raw <METHOD> <PATH>` | Call an arbitrary endpoint |
|
|
230
|
+
|
|
231
|
+
### Options
|
|
232
|
+
|
|
233
|
+
| Option | Description |
|
|
234
|
+
|--------|-------------|
|
|
235
|
+
| `-o, --output {rich,json,toon}` | Output format (global default or per-command override) |
|
|
236
|
+
| `-a, --account <name>` | Use a named account (env/config) |
|
|
237
|
+
| `--all-accounts` | Run the command on every configured account |
|
|
238
|
+
| `--all` | Follow the cursor and fetch every page (GET) |
|
|
239
|
+
| `--limit`, `--cursor` | Manual pagination controls |
|
|
240
|
+
| `--data <json\|@file>`, `--field k=v` | Request body for create/update |
|
|
241
|
+
| `--token`, `--config`, `--base-url`, `--timeout` | Connection options |
|
|
242
|
+
|
|
243
|
+
### Resources
|
|
244
|
+
|
|
245
|
+
| Group | Resources |
|
|
246
|
+
|-------|-----------|
|
|
247
|
+
| **Documents** (CRUD + `get-pdf`, `send`) | `invoices`, `credit_notes`, `sales_orders`, `estimates`, `proformas`, `waybills`, `sales_receipts`, `purchases`, `purchase_orders` |
|
|
248
|
+
| **Masters** (CRUD) | `contacts`, `contact_groups`, `products`, `services`, `warehouses`, `payments`, `sales_channels`, `expenses_accounts`, `taxes`, `payment_methods` |
|
|
249
|
+
| **CRM** | `funnels`, `leads` (+ `create-note`, `create-task`), `events`, `bookings`, `booking_locations` |
|
|
250
|
+
| **Projects / Team** | `projects`, `tasks`, `employees` |
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## Python Library
|
|
255
|
+
|
|
256
|
+
### Basic Usage
|
|
257
|
+
|
|
258
|
+
```python
|
|
259
|
+
from pyholded import HoldedClient
|
|
260
|
+
|
|
261
|
+
with HoldedClient() as client: # token from env or config file
|
|
262
|
+
page = client.invoices.list(params={"limit": 50})
|
|
263
|
+
everyone = client.contacts.list(paginate=True) # all pages, merged items list
|
|
264
|
+
contact = client.contacts.get(id="0123456789abcdef01234567")
|
|
265
|
+
pdf = client.invoices.getPdf(id="89abcdef0123456789abcdef") # raw bytes
|
|
266
|
+
|
|
267
|
+
new = client.contacts.create(data={"name": "ACME SL", "code": "B12345678"})
|
|
268
|
+
|
|
269
|
+
# Any endpoint, even one not modelled, is reachable directly:
|
|
270
|
+
raw = client.request("GET", "taxes", params={"limit": 5})
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Resources are attributes; operations are methods. Path parameters (`id`) are keyword
|
|
274
|
+
arguments, query parameters go in `params=`, and the request body in `data=`.
|
|
275
|
+
|
|
276
|
+
### Multiple accounts
|
|
277
|
+
|
|
278
|
+
```python
|
|
279
|
+
from pyholded import HoldedClient, MultiClient
|
|
280
|
+
|
|
281
|
+
# one named account
|
|
282
|
+
with HoldedClient(account="acme") as client:
|
|
283
|
+
invoices = client.invoices.list()
|
|
284
|
+
|
|
285
|
+
# every configured account at once -> {account: result}
|
|
286
|
+
with MultiClient.from_accounts() as multi: # or from_accounts(["acme", "personal"])
|
|
287
|
+
per_account = multi.contacts.list(params={"limit": 5})
|
|
288
|
+
# {"acme": {"items": [...]}, "personal": {"items": [...]}}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
A failure on one account is captured as `{"error": "..."}` for that account, so the
|
|
292
|
+
others still return their data.
|
|
293
|
+
|
|
294
|
+
### Output Helpers
|
|
295
|
+
|
|
296
|
+
```python
|
|
297
|
+
from pyholded import OutputFormat, render, to_json, to_toon
|
|
298
|
+
|
|
299
|
+
render(page, OutputFormat.TOON) # print in TOON
|
|
300
|
+
print(to_json(page)) # canonical JSON string
|
|
301
|
+
print(to_toon(page)) # TOON string
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## Supply Chain / SBOM
|
|
307
|
+
|
|
308
|
+
A CycloneDX 1.6 SBOM ([`sbom.cdx.json`](sbom.cdx.json)) is generated from real data —
|
|
309
|
+
package SHA-256 hashes come from a `uv`-compiled hashed lockfile
|
|
310
|
+
([`requirements.lock`](requirements.lock)), licenses and suppliers from installed
|
|
311
|
+
package metadata, a full dependency graph, and an **ECDSA P-256 (ES256) signature**
|
|
312
|
+
embedded as a CycloneDX JSF block (pure-Python, offline, no external tools). CI
|
|
313
|
+
regenerates, scores and verifies it on every push.
|
|
314
|
+
|
|
315
|
+
```bash
|
|
316
|
+
make sbom # generate + sign sbom.cdx.json
|
|
317
|
+
make sbom-score # generate + score with sbomqs (fails below 9.0)
|
|
318
|
+
make sbom-verify # verify the embedded signature
|
|
319
|
+
make lock # refresh the hashed lockfile (uv)
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Current [sbomqs](https://github.com/interlynk-io/sbomqs) score: **9.3 / 10 (Grade A)** —
|
|
323
|
+
Identification, Provenance, Integrity, Licensing, Vulnerability and Structural all at A.
|
|
324
|
+
Completeness (D) is capped by sbomqs's CycloneDX dependency-graph detection, not by
|
|
325
|
+
missing data (the `dependencies` and `compositions` are present and valid). A perfect
|
|
326
|
+
10/A is not attainable for a PyPI project (it also requires per-component CPEs, which
|
|
327
|
+
Python packages do not have).
|
|
328
|
+
|
|
329
|
+
The signing private key is never committed; the public key travels inside the SBOM
|
|
330
|
+
(`signature.publicKey` JWK), so `scripts/verify_sbom.py` verifies it with no extra files.
|
|
331
|
+
|
|
332
|
+
## Requirements
|
|
333
|
+
|
|
334
|
+
- Python 3.14+
|
|
335
|
+
- See [pyproject.toml](pyproject.toml) for dependencies and extras
|
|
336
|
+
|
|
337
|
+
---
|
|
338
|
+
|
|
339
|
+
## Contributing
|
|
340
|
+
|
|
341
|
+
Contributions are welcome.
|
|
342
|
+
|
|
343
|
+
1. Fork the repository
|
|
344
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
345
|
+
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
|
346
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
347
|
+
5. Open a Pull Request
|
|
348
|
+
|
|
349
|
+
All changes must keep the full quality gate green (`ruff`, `black`, `mypy --strict`,
|
|
350
|
+
`bandit`, `vulture`, `pip-audit`, `pytest`) with zero in-line suppressions.
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
## Support the Project
|
|
355
|
+
|
|
356
|
+
If this project is useful in your workflows, you can support development:
|
|
357
|
+
|
|
358
|
+
<a href="https://buymeacoffee.com/seifreed" target="_blank">
|
|
359
|
+
<img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50">
|
|
360
|
+
</a>
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
## License
|
|
365
|
+
|
|
366
|
+
This project is licensed under the MIT license. See [LICENSE](LICENSE).
|
|
367
|
+
|
|
368
|
+
**Attribution**
|
|
369
|
+
- Author: **Marc Rivero López** | [@seifreed](https://github.com/seifreed)
|
|
370
|
+
- Repository: [github.com/seifreed/pyholded](https://github.com/seifreed/pyholded)
|
|
371
|
+
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
<p align="center">
|
|
375
|
+
<sub>Built for practical Holded automation and business-data workflows</sub>
|
|
376
|
+
</p>
|