ippx 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.
- ippx-0.1.0/.github/workflows/ci.yml +26 -0
- ippx-0.1.0/.github/workflows/release.yml +24 -0
- ippx-0.1.0/.gitignore +6 -0
- ippx-0.1.0/CHANGELOG.md +27 -0
- ippx-0.1.0/LICENSE +21 -0
- ippx-0.1.0/PKG-INFO +196 -0
- ippx-0.1.0/README.md +167 -0
- ippx-0.1.0/pyproject.toml +59 -0
- ippx-0.1.0/src/ippx/__init__.py +43 -0
- ippx-0.1.0/src/ippx/_async.py +184 -0
- ippx-0.1.0/src/ippx/_base.py +69 -0
- ippx-0.1.0/src/ippx/_codec.py +311 -0
- ippx-0.1.0/src/ippx/_models.py +144 -0
- ippx-0.1.0/src/ippx/_operations.py +277 -0
- ippx-0.1.0/src/ippx/_sync.py +177 -0
- ippx-0.1.0/src/ippx/_tls.py +143 -0
- ippx-0.1.0/src/ippx/exceptions.py +77 -0
- ippx-0.1.0/src/ippx/py.typed +0 -0
- ippx-0.1.0/tests/__init__.py +0 -0
- ippx-0.1.0/tests/helpers.py +61 -0
- ippx-0.1.0/tests/test_async_client.py +170 -0
- ippx-0.1.0/tests/test_codec.py +200 -0
- ippx-0.1.0/tests/test_operations.py +93 -0
- ippx-0.1.0/tests/test_sync_client.py +86 -0
- ippx-0.1.0/tests/test_tls.py +169 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
test:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
strategy:
|
|
15
|
+
matrix:
|
|
16
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
- uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: ${{ matrix.python-version }}
|
|
22
|
+
- run: pip install -e .[dev]
|
|
23
|
+
- run: ruff check src tests
|
|
24
|
+
- run: ruff format --check src tests
|
|
25
|
+
- run: mypy
|
|
26
|
+
- run: pytest -q
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
publish:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
environment: pypi
|
|
14
|
+
permissions:
|
|
15
|
+
id-token: write # required for PyPI Trusted Publishing (OIDC)
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
- uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: "3.12"
|
|
21
|
+
- run: pip install --upgrade build twine
|
|
22
|
+
- run: python -m build
|
|
23
|
+
- run: twine check dist/*
|
|
24
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
ippx-0.1.0/.gitignore
ADDED
ippx-0.1.0/CHANGELOG.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-06-13
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Initial release.
|
|
14
|
+
- Synchronous `IppClient` and asynchronous `AsyncIppClient`.
|
|
15
|
+
- Sans-IO IPP/IPPS codec (RFC 8010) with full request encoding and response
|
|
16
|
+
decoding, including nested collections.
|
|
17
|
+
- IPP operation set (RFC 8011) with the IANA operation registry through 0x46.
|
|
18
|
+
- Print-Job submission, verified end to end against real hardware
|
|
19
|
+
(HP Color LaserJet Pro M283fdw over IPPS).
|
|
20
|
+
- TLS support with certificate fingerprint pinning, configurable verification,
|
|
21
|
+
and legacy-cipher (SECLEVEL) handling for older printers.
|
|
22
|
+
- Distinct exception taxonomy (`IppDecodeError`, `IppResponseError`,
|
|
23
|
+
`IppHttpError`, `FingerprintMismatch`).
|
|
24
|
+
- Typed API shipping `py.typed`, checked under strict mypy.
|
|
25
|
+
|
|
26
|
+
[Unreleased]: https://github.com/smck83/ippx/compare/v0.1.0...HEAD
|
|
27
|
+
[0.1.0]: https://github.com/smck83/ippx/releases/tag/v0.1.0
|
ippx-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright(c) 2026 smck83
|
|
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.
|
ippx-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ippx
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Sync and async IPP/IPPS client: send print jobs and monitor network printers
|
|
5
|
+
Project-URL: Homepage, https://github.com/smck83/ippx
|
|
6
|
+
Project-URL: Repository, https://github.com/smck83/ippx
|
|
7
|
+
Project-URL: Issues, https://github.com/smck83/ippx/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/smck83/ippx/blob/main/CHANGELOG.md
|
|
9
|
+
Author: Scott McKenzie
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: ipp,ipps,print-job,printer,printing
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Printing
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Requires-Dist: httpx>=0.27
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
26
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
27
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# ippx
|
|
31
|
+
|
|
32
|
+
Sync and async IPP/IPPS client for Python. Send print jobs and monitor network
|
|
33
|
+
printers, including printers exposed over the internet behind TLS with source
|
|
34
|
+
IP allowlisting.
|
|
35
|
+
|
|
36
|
+
Built on [httpx](https://www.python-httpx.org/) with a pure sans-IO codec for
|
|
37
|
+
the IPP binary protocol (RFC 8010) and the RFC 8011 required operation set.
|
|
38
|
+
|
|
39
|
+
ippx is a plain library: the only runtime dependency is httpx. No framework,
|
|
40
|
+
no server, no Docker required. Use it from a script, a CLI, a cron job, a
|
|
41
|
+
serverless function, or an async web app.
|
|
42
|
+
|
|
43
|
+
## Why
|
|
44
|
+
|
|
45
|
+
[pyipp](https://pypi.org/project/pyipp/) is monitoring only. ippx can actually
|
|
46
|
+
print, in both async (FastAPI-native) and sync code, with first-class support
|
|
47
|
+
for the realities of network printers: self-signed certificates, legacy cipher
|
|
48
|
+
suites, HTTP Basic auth, and flaky WAN links.
|
|
49
|
+
|
|
50
|
+
## Install
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
pip install ippx
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Python 3.11+. Single runtime dependency: httpx. Fully typed (PEP 561).
|
|
57
|
+
|
|
58
|
+
## Quick start (async, e.g. inside FastAPI)
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from ippx import AsyncIppClient, BasicAuth, TlsConfig
|
|
62
|
+
|
|
63
|
+
async def print_invoice(pdf_bytes: bytes) -> None:
|
|
64
|
+
async with AsyncIppClient(
|
|
65
|
+
"ipps://printer.example.com:631/ipp/print",
|
|
66
|
+
auth=BasicAuth("user", "password"),
|
|
67
|
+
tls=TlsConfig(fingerprint="sha256:AB:CD:..."),
|
|
68
|
+
) as printer:
|
|
69
|
+
job = await printer.print_job(
|
|
70
|
+
pdf_bytes,
|
|
71
|
+
document_format="application/pdf",
|
|
72
|
+
job_name="invoice-123",
|
|
73
|
+
job_attributes={"copies": 1, "sides": "two-sided-long-edge"},
|
|
74
|
+
)
|
|
75
|
+
result = await printer.wait_for_job(job.job_id, timeout=120)
|
|
76
|
+
print(result.state, result.state_reasons)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Quick start (sync)
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from ippx import IppClient
|
|
83
|
+
|
|
84
|
+
with IppClient("ipps://203.0.113.10:631/ipp/print") as printer:
|
|
85
|
+
caps = printer.get_printer_attributes()
|
|
86
|
+
print(caps.make_and_model, caps.state, caps.document_formats)
|
|
87
|
+
job = printer.print_job(pdf_bytes, document_format="application/pdf")
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Operations
|
|
91
|
+
|
|
92
|
+
The RFC 8011 required operation set, supported by every conformant printer:
|
|
93
|
+
|
|
94
|
+
| Method | IPP operation |
|
|
95
|
+
|---|---|
|
|
96
|
+
| `print_job(document, ...)` | Print-Job |
|
|
97
|
+
| `validate_job(...)` | Validate-Job (pre-flight without printing) |
|
|
98
|
+
| `cancel_job(job_id)` | Cancel-Job |
|
|
99
|
+
| `get_printer_attributes(...)` | Get-Printer-Attributes |
|
|
100
|
+
| `get_job_attributes(job_id, ...)` | Get-Job-Attributes |
|
|
101
|
+
| `get_jobs(...)` | Get-Jobs |
|
|
102
|
+
| `wait_for_job(job_id, timeout=...)` | polling helper over Get-Job-Attributes |
|
|
103
|
+
|
|
104
|
+
`wait_for_job` polls with exponential backoff (1s doubling to a 15s cap by
|
|
105
|
+
default) until the job reaches a terminal state (completed, canceled,
|
|
106
|
+
aborted) and raises `JobTimeoutError` otherwise.
|
|
107
|
+
|
|
108
|
+
A printer's full `operations-supported` list decodes to the named `Operation`
|
|
109
|
+
enum (the complete IANA IPP registry, not just the implemented set), so you
|
|
110
|
+
can introspect capabilities like `Operation.IDENTIFY_PRINTER`.
|
|
111
|
+
|
|
112
|
+
Note that for `get_jobs` most printers only return `job-id` and `job-uri` by
|
|
113
|
+
default, per RFC 8011; pass
|
|
114
|
+
`requested_attributes=["job-id", "job-state", "job-name"]` to get more.
|
|
115
|
+
|
|
116
|
+
## Authentication
|
|
117
|
+
|
|
118
|
+
- **HTTP Basic**: `auth=ippx.BasicAuth("user", "pw")` (or a plain
|
|
119
|
+
`("user", "pw")` tuple)
|
|
120
|
+
- **HTTP Digest**: `auth=ippx.DigestAuth("user", "pw")`
|
|
121
|
+
- **TLS client certificate**: `tls=TlsConfig(client_cert=("cert.pem", "key.pem"))`
|
|
122
|
+
- **requesting-user-name**: set via `requesting_user_name=`, sent on every
|
|
123
|
+
request (identification only, not authentication)
|
|
124
|
+
|
|
125
|
+
## TLS
|
|
126
|
+
|
|
127
|
+
Printers almost universally ship self-signed certificates. `TlsConfig` gives
|
|
128
|
+
you four options, strongest first:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
TlsConfig() # system CA validation (default)
|
|
132
|
+
TlsConfig(verify="/path/to/printer-ca.pem") # custom CA bundle
|
|
133
|
+
TlsConfig(fingerprint="sha256:AB:CD:...") # pin the exact certificate
|
|
134
|
+
TlsConfig(verify=False) # no validation (last resort)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Fingerprint pinning verifies the SHA-256 digest of the server certificate
|
|
138
|
+
during the TLS handshake itself, so there is no window between checking and
|
|
139
|
+
using the connection. Get a printer's fingerprint with:
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
openssl s_client -connect printer:631 < /dev/null 2>/dev/null \
|
|
143
|
+
| openssl x509 -fingerprint -sha256 -noout
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
For an internet-exposed printer (port forward locked to a source WAN IP),
|
|
147
|
+
the recommended setup is fingerprint pinning plus HTTP Basic auth.
|
|
148
|
+
|
|
149
|
+
Two behaviours worth knowing:
|
|
150
|
+
|
|
151
|
+
- Printers very commonly offer only plain-RSA key exchange (no ECDHE), which
|
|
152
|
+
stock OpenSSL policy rejects with a bare handshake-failure alert. Every
|
|
153
|
+
context ippx builds therefore uses `DEFAULT:@SECLEVEL=1` ciphers so real
|
|
154
|
+
hardware is reachable and genuine certificate problems surface as truthful
|
|
155
|
+
certificate errors; override with `TlsConfig(ciphers=...)` to tighten or
|
|
156
|
+
loosen.
|
|
157
|
+
- With a custom CA bundle, hostname verification is disabled (printers are
|
|
158
|
+
usually addressed by IP), so any certificate issued by that CA is accepted.
|
|
159
|
+
|
|
160
|
+
## Job attributes
|
|
161
|
+
|
|
162
|
+
Pass common Job Template attributes as a plain dict; tags are inferred for
|
|
163
|
+
`copies`, `sides`, `media`, `print-quality`, `print-color-mode`,
|
|
164
|
+
`orientation-requested`, `output-bin`, `number-up`, `printer-resolution` and
|
|
165
|
+
others. For anything else, pass `ippx.Attribute` objects with explicit tags.
|
|
166
|
+
|
|
167
|
+
## Verified hardware
|
|
168
|
+
|
|
169
|
+
| Printer | Print-Job | Status | Notes |
|
|
170
|
+
|---|---|---|---|
|
|
171
|
+
| HP Color LaserJet Pro M283fdw | yes | verified | Full IPPS path with fingerprint pinning: Get-Printer-Attributes (128 attrs incl. nested media-col collections), Validate-Job, Print-Job to completion, job polling, Get-Jobs, sync and async. Offers RSA-key-exchange ciphers only, handled by the default `TlsConfig` cipher policy. |
|
|
172
|
+
|
|
173
|
+
Also verified end-to-end against a live CUPS 2.4 server (Get-Printer-Attributes,
|
|
174
|
+
Validate-Job, Print-Job, job polling to completion, Get-Jobs, sync and async
|
|
175
|
+
clients). Contributions to this table are welcome.
|
|
176
|
+
|
|
177
|
+
## Not in scope (yet)
|
|
178
|
+
|
|
179
|
+
- Document conversion: bring your own PDF/PCL/PWG raster bytes
|
|
180
|
+
- Create-Job / Send-Document multi-document jobs
|
|
181
|
+
- IPP event subscriptions (RFC 3995): printer firmware support is too rare
|
|
182
|
+
to rely on; use `wait_for_job` polling
|
|
183
|
+
- Encoding IPP collections in requests (decoding is fully supported)
|
|
184
|
+
- mDNS/driverless discovery
|
|
185
|
+
|
|
186
|
+
## Development
|
|
187
|
+
|
|
188
|
+
```
|
|
189
|
+
pip install -e .[dev]
|
|
190
|
+
ruff check src tests
|
|
191
|
+
pytest
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## License
|
|
195
|
+
|
|
196
|
+
MIT
|
ippx-0.1.0/README.md
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# ippx
|
|
2
|
+
|
|
3
|
+
Sync and async IPP/IPPS client for Python. Send print jobs and monitor network
|
|
4
|
+
printers, including printers exposed over the internet behind TLS with source
|
|
5
|
+
IP allowlisting.
|
|
6
|
+
|
|
7
|
+
Built on [httpx](https://www.python-httpx.org/) with a pure sans-IO codec for
|
|
8
|
+
the IPP binary protocol (RFC 8010) and the RFC 8011 required operation set.
|
|
9
|
+
|
|
10
|
+
ippx is a plain library: the only runtime dependency is httpx. No framework,
|
|
11
|
+
no server, no Docker required. Use it from a script, a CLI, a cron job, a
|
|
12
|
+
serverless function, or an async web app.
|
|
13
|
+
|
|
14
|
+
## Why
|
|
15
|
+
|
|
16
|
+
[pyipp](https://pypi.org/project/pyipp/) is monitoring only. ippx can actually
|
|
17
|
+
print, in both async (FastAPI-native) and sync code, with first-class support
|
|
18
|
+
for the realities of network printers: self-signed certificates, legacy cipher
|
|
19
|
+
suites, HTTP Basic auth, and flaky WAN links.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
pip install ippx
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Python 3.11+. Single runtime dependency: httpx. Fully typed (PEP 561).
|
|
28
|
+
|
|
29
|
+
## Quick start (async, e.g. inside FastAPI)
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from ippx import AsyncIppClient, BasicAuth, TlsConfig
|
|
33
|
+
|
|
34
|
+
async def print_invoice(pdf_bytes: bytes) -> None:
|
|
35
|
+
async with AsyncIppClient(
|
|
36
|
+
"ipps://printer.example.com:631/ipp/print",
|
|
37
|
+
auth=BasicAuth("user", "password"),
|
|
38
|
+
tls=TlsConfig(fingerprint="sha256:AB:CD:..."),
|
|
39
|
+
) as printer:
|
|
40
|
+
job = await printer.print_job(
|
|
41
|
+
pdf_bytes,
|
|
42
|
+
document_format="application/pdf",
|
|
43
|
+
job_name="invoice-123",
|
|
44
|
+
job_attributes={"copies": 1, "sides": "two-sided-long-edge"},
|
|
45
|
+
)
|
|
46
|
+
result = await printer.wait_for_job(job.job_id, timeout=120)
|
|
47
|
+
print(result.state, result.state_reasons)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Quick start (sync)
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from ippx import IppClient
|
|
54
|
+
|
|
55
|
+
with IppClient("ipps://203.0.113.10:631/ipp/print") as printer:
|
|
56
|
+
caps = printer.get_printer_attributes()
|
|
57
|
+
print(caps.make_and_model, caps.state, caps.document_formats)
|
|
58
|
+
job = printer.print_job(pdf_bytes, document_format="application/pdf")
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Operations
|
|
62
|
+
|
|
63
|
+
The RFC 8011 required operation set, supported by every conformant printer:
|
|
64
|
+
|
|
65
|
+
| Method | IPP operation |
|
|
66
|
+
|---|---|
|
|
67
|
+
| `print_job(document, ...)` | Print-Job |
|
|
68
|
+
| `validate_job(...)` | Validate-Job (pre-flight without printing) |
|
|
69
|
+
| `cancel_job(job_id)` | Cancel-Job |
|
|
70
|
+
| `get_printer_attributes(...)` | Get-Printer-Attributes |
|
|
71
|
+
| `get_job_attributes(job_id, ...)` | Get-Job-Attributes |
|
|
72
|
+
| `get_jobs(...)` | Get-Jobs |
|
|
73
|
+
| `wait_for_job(job_id, timeout=...)` | polling helper over Get-Job-Attributes |
|
|
74
|
+
|
|
75
|
+
`wait_for_job` polls with exponential backoff (1s doubling to a 15s cap by
|
|
76
|
+
default) until the job reaches a terminal state (completed, canceled,
|
|
77
|
+
aborted) and raises `JobTimeoutError` otherwise.
|
|
78
|
+
|
|
79
|
+
A printer's full `operations-supported` list decodes to the named `Operation`
|
|
80
|
+
enum (the complete IANA IPP registry, not just the implemented set), so you
|
|
81
|
+
can introspect capabilities like `Operation.IDENTIFY_PRINTER`.
|
|
82
|
+
|
|
83
|
+
Note that for `get_jobs` most printers only return `job-id` and `job-uri` by
|
|
84
|
+
default, per RFC 8011; pass
|
|
85
|
+
`requested_attributes=["job-id", "job-state", "job-name"]` to get more.
|
|
86
|
+
|
|
87
|
+
## Authentication
|
|
88
|
+
|
|
89
|
+
- **HTTP Basic**: `auth=ippx.BasicAuth("user", "pw")` (or a plain
|
|
90
|
+
`("user", "pw")` tuple)
|
|
91
|
+
- **HTTP Digest**: `auth=ippx.DigestAuth("user", "pw")`
|
|
92
|
+
- **TLS client certificate**: `tls=TlsConfig(client_cert=("cert.pem", "key.pem"))`
|
|
93
|
+
- **requesting-user-name**: set via `requesting_user_name=`, sent on every
|
|
94
|
+
request (identification only, not authentication)
|
|
95
|
+
|
|
96
|
+
## TLS
|
|
97
|
+
|
|
98
|
+
Printers almost universally ship self-signed certificates. `TlsConfig` gives
|
|
99
|
+
you four options, strongest first:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
TlsConfig() # system CA validation (default)
|
|
103
|
+
TlsConfig(verify="/path/to/printer-ca.pem") # custom CA bundle
|
|
104
|
+
TlsConfig(fingerprint="sha256:AB:CD:...") # pin the exact certificate
|
|
105
|
+
TlsConfig(verify=False) # no validation (last resort)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Fingerprint pinning verifies the SHA-256 digest of the server certificate
|
|
109
|
+
during the TLS handshake itself, so there is no window between checking and
|
|
110
|
+
using the connection. Get a printer's fingerprint with:
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
openssl s_client -connect printer:631 < /dev/null 2>/dev/null \
|
|
114
|
+
| openssl x509 -fingerprint -sha256 -noout
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
For an internet-exposed printer (port forward locked to a source WAN IP),
|
|
118
|
+
the recommended setup is fingerprint pinning plus HTTP Basic auth.
|
|
119
|
+
|
|
120
|
+
Two behaviours worth knowing:
|
|
121
|
+
|
|
122
|
+
- Printers very commonly offer only plain-RSA key exchange (no ECDHE), which
|
|
123
|
+
stock OpenSSL policy rejects with a bare handshake-failure alert. Every
|
|
124
|
+
context ippx builds therefore uses `DEFAULT:@SECLEVEL=1` ciphers so real
|
|
125
|
+
hardware is reachable and genuine certificate problems surface as truthful
|
|
126
|
+
certificate errors; override with `TlsConfig(ciphers=...)` to tighten or
|
|
127
|
+
loosen.
|
|
128
|
+
- With a custom CA bundle, hostname verification is disabled (printers are
|
|
129
|
+
usually addressed by IP), so any certificate issued by that CA is accepted.
|
|
130
|
+
|
|
131
|
+
## Job attributes
|
|
132
|
+
|
|
133
|
+
Pass common Job Template attributes as a plain dict; tags are inferred for
|
|
134
|
+
`copies`, `sides`, `media`, `print-quality`, `print-color-mode`,
|
|
135
|
+
`orientation-requested`, `output-bin`, `number-up`, `printer-resolution` and
|
|
136
|
+
others. For anything else, pass `ippx.Attribute` objects with explicit tags.
|
|
137
|
+
|
|
138
|
+
## Verified hardware
|
|
139
|
+
|
|
140
|
+
| Printer | Print-Job | Status | Notes |
|
|
141
|
+
|---|---|---|---|
|
|
142
|
+
| HP Color LaserJet Pro M283fdw | yes | verified | Full IPPS path with fingerprint pinning: Get-Printer-Attributes (128 attrs incl. nested media-col collections), Validate-Job, Print-Job to completion, job polling, Get-Jobs, sync and async. Offers RSA-key-exchange ciphers only, handled by the default `TlsConfig` cipher policy. |
|
|
143
|
+
|
|
144
|
+
Also verified end-to-end against a live CUPS 2.4 server (Get-Printer-Attributes,
|
|
145
|
+
Validate-Job, Print-Job, job polling to completion, Get-Jobs, sync and async
|
|
146
|
+
clients). Contributions to this table are welcome.
|
|
147
|
+
|
|
148
|
+
## Not in scope (yet)
|
|
149
|
+
|
|
150
|
+
- Document conversion: bring your own PDF/PCL/PWG raster bytes
|
|
151
|
+
- Create-Job / Send-Document multi-document jobs
|
|
152
|
+
- IPP event subscriptions (RFC 3995): printer firmware support is too rare
|
|
153
|
+
to rely on; use `wait_for_job` polling
|
|
154
|
+
- Encoding IPP collections in requests (decoding is fully supported)
|
|
155
|
+
- mDNS/driverless discovery
|
|
156
|
+
|
|
157
|
+
## Development
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
pip install -e .[dev]
|
|
161
|
+
ruff check src tests
|
|
162
|
+
pytest
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## License
|
|
166
|
+
|
|
167
|
+
MIT
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ippx"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Sync and async IPP/IPPS client: send print jobs and monitor network printers"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.11"
|
|
13
|
+
authors = [{ name = "Scott McKenzie" }]
|
|
14
|
+
keywords = ["ipp", "ipps", "printing", "printer", "print-job"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Topic :: Printing",
|
|
22
|
+
"Typing :: Typed",
|
|
23
|
+
]
|
|
24
|
+
dependencies = ["httpx>=0.27"]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/smck83/ippx"
|
|
28
|
+
Repository = "https://github.com/smck83/ippx"
|
|
29
|
+
Issues = "https://github.com/smck83/ippx/issues"
|
|
30
|
+
Changelog = "https://github.com/smck83/ippx/blob/main/CHANGELOG.md"
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
dev = [
|
|
34
|
+
"pytest>=8",
|
|
35
|
+
"pytest-asyncio>=0.23",
|
|
36
|
+
"respx>=0.21",
|
|
37
|
+
"ruff>=0.4",
|
|
38
|
+
"mypy>=1.10",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[tool.hatch.build.targets.wheel]
|
|
42
|
+
packages = ["src/ippx"]
|
|
43
|
+
|
|
44
|
+
[tool.ruff]
|
|
45
|
+
line-length = 100
|
|
46
|
+
target-version = "py311"
|
|
47
|
+
src = ["src", "tests"]
|
|
48
|
+
|
|
49
|
+
[tool.ruff.lint]
|
|
50
|
+
select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
|
|
51
|
+
|
|
52
|
+
[tool.pytest.ini_options]
|
|
53
|
+
asyncio_mode = "auto"
|
|
54
|
+
testpaths = ["tests"]
|
|
55
|
+
|
|
56
|
+
[tool.mypy]
|
|
57
|
+
python_version = "3.11"
|
|
58
|
+
strict = true
|
|
59
|
+
files = ["src/ippx"]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""ippx: sync and async IPP/IPPS client for sending print jobs and
|
|
2
|
+
monitoring network printers."""
|
|
3
|
+
|
|
4
|
+
from httpx import BasicAuth, DigestAuth
|
|
5
|
+
|
|
6
|
+
from ._async import AsyncIppClient
|
|
7
|
+
from ._codec import Attribute, IppMessage, Resolution, Tag, decode, encode
|
|
8
|
+
from ._models import Job, JobState, Operation, Printer, PrinterState
|
|
9
|
+
from ._sync import IppClient
|
|
10
|
+
from ._tls import TlsConfig
|
|
11
|
+
from .exceptions import (
|
|
12
|
+
IppDecodeError,
|
|
13
|
+
IppError,
|
|
14
|
+
IppHttpError,
|
|
15
|
+
IppResponseError,
|
|
16
|
+
JobTimeoutError,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__version__ = "0.1.0"
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"AsyncIppClient",
|
|
23
|
+
"Attribute",
|
|
24
|
+
"BasicAuth",
|
|
25
|
+
"DigestAuth",
|
|
26
|
+
"IppClient",
|
|
27
|
+
"IppDecodeError",
|
|
28
|
+
"IppError",
|
|
29
|
+
"IppHttpError",
|
|
30
|
+
"IppMessage",
|
|
31
|
+
"IppResponseError",
|
|
32
|
+
"Job",
|
|
33
|
+
"JobState",
|
|
34
|
+
"JobTimeoutError",
|
|
35
|
+
"Operation",
|
|
36
|
+
"Printer",
|
|
37
|
+
"PrinterState",
|
|
38
|
+
"Resolution",
|
|
39
|
+
"Tag",
|
|
40
|
+
"TlsConfig",
|
|
41
|
+
"decode",
|
|
42
|
+
"encode",
|
|
43
|
+
]
|