kompressr-sdk 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.
@@ -0,0 +1,7 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .mypy_cache/
4
+ .venv/
5
+ dist/
6
+ build/
7
+ *.egg-info/
@@ -0,0 +1,109 @@
1
+ # Python API reference
2
+
3
+ The Python SDK follows JS SDK 1.0.0 and the shared API contract. Import `KompressrClient`, `PlatformClient`, `AsyncKompressrClient`, `AsyncPlatformClient`, `KompressrError`, `KompressrCancelledError` and `CancellationToken` from `kompressr`. Public response types are exported from that module too. Sync and async methods accept the same arguments and return the same typed dictionaries; async methods must be awaited. Async iteration uses `async for`.
4
+
5
+ ## Configuration
6
+
7
+ Construct clients with keyword arguments:
8
+
9
+ | Argument | Default | Meaning |
10
+ | --- | --- | --- |
11
+ | `api_key` | Required | Explicit project or scoped key; never persisted |
12
+ | `project_id` | Omitted | File client only; UUID default for upload listing |
13
+ | `api_base_url` | `https://kompressr.com` | Management origin |
14
+ | `upload_base_url` | Derived | `https://uploads.kompressr.com` for the production management origin; otherwise the custom management origin |
15
+ | `request_timeout` | `30` | Total management request budget in seconds |
16
+ | `upload_timeout` | `600` | Total upload request budget in seconds |
17
+ | `max_read_retries` | `2` | Additional eligible GET attempts, integer 0–5 |
18
+
19
+ Origins must be absolute HTTP(S) origins without credentials, path, query or fragment. File clients expose resolved `api_base_url`, `upload_base_url` and `platform`. `resolve_endpoints(...)` and `ENDPOINTS` allow inspecting configuration without a request. For an alternate deployment, explicitly supply its management and upload origins. There is no environment-name switch.
20
+
21
+ HTTPX verifies TLS certificates, does not follow redirects, has transport retries disabled and does not automatically read proxy settings from the environment. The SDK creates and closes a connection pool for each operation, including all retries. There is no persistent connection pool or client lifecycle method. No caller-supplied transport is accepted.
22
+
23
+ DNS resolution shares a process-wide limit of eight daemon workers with no queued resolver jobs. Cancelling or timing out stops the caller promptly and prevents a late connection/write, even while an operating-system lookup remains blocked. That lookup keeps its slot until the OS returns; if all eight slots remain occupied, later hostname requests can time out waiting for admission. Numeric IP origins bypass DNS. A separate limit permits 64 live connections per process; waiting for either limit remains inside the request deadline.
24
+
25
+ Resolved TCP addresses are tried sequentially in system order. Each receives a fair share of the **remaining** request budget divided by the remaining addresses, so a stalled first address leaves time for fallback. There is only one connection attempt per request at a time. This can take longer than Happy Eyeballs, which races address families, but avoids extra background connection attempts. DNS time is deducted before allocating those shares. The original hostname remains the HTTP Host and TLS SNI/certificate identity. Scoped IPv6 zone identifiers are unsupported.
26
+
27
+ Every HTTP method supports keyword-only `timeout: float | None` and `cancel: CancellationToken | None`. Positive finite timeouts use seconds (maximum 2147483.647); constructor budgets apply when omitted. Deadlines include connection setup, upload writes, response reads and retry waits. File opening, validation and the snapshot of an in-memory buffer happen before the request budget. Standard local file reads occur in bounded chunks on the calling thread; filesystem I/O itself is subject to operating-system behavior. Arbitrary/custom stream readers, pipes and sockets are rejected.
28
+
29
+ ## Uploads and delivery
30
+
31
+ | Method | Required arguments | Result |
32
+ | --- | --- | --- |
33
+ | `upload_file` | `path: str | PathLike[str]` | `UploadAccepted`; opens and closes a regular binary file |
34
+ | `upload_bytes` | `content: bytes | bytearray | memoryview`, `filename` | `UploadAccepted`; snapshots the selected bytes when the operation starts |
35
+ | `upload_stream` | `stream: BinaryIO`, `filename` | `UploadAccepted`; standard regular `FileIO`/`BufferedReader` or `BytesIO` only; rewinds to zero and leaves it open |
36
+ | `get_upload_status` | `asset_id: int` | `UploadAsset` |
37
+ | `wait_until_ready` | `asset_id: int` | Active, processed `UploadAsset` |
38
+ | `generate_private_url` | `asset_id: int` | `PrivateUrl` |
39
+
40
+ Upload keyword arguments: `visibility='public'|'private'` (server default if omitted), `quality='480p'|'720p'|'1080p'|'4k'` (video only), `data: CustomerData`, `private_url_expires_in: int` (60–604800 seconds). Metadata fields always precede the multipart file. Filenames must be nonempty, contain valid UTF-8 and have no control characters; directory components are removed. Stream subclasses/custom wrappers are unsupported because their reads can block cancellation or misreport length. File source contents must remain unchanged during upload.
41
+
42
+ `CustomerData` is a dictionary of up to five scalar strings, finite numbers, booleans or `None`. Keys are trimmed, unique after trimming, nonempty and at most 64 UTF-8 bytes. String values support 512 UTF-8 bytes. Encoded upload data is limited to 4096 bytes; platform metadata to 8192.
43
+
44
+ `wait_until_ready` accepts `timeout=120`, `poll_interval=2` (positive seconds, maximum 30) and `cancel`. A timeout raises `WAIT_TIMEOUT`; cancellation raises `WAIT_CANCELLED`. Both preserve `asset_id`. Failed, unprocessable or unknown processing raises `PROCESSING_FAILED`; nonactive deletion raises `ASSET_DELETED`. A processed public asset must have a delivery URL; a private one must be eligible to generate a link. Waiting never reuploads or signs automatically. The total wait deadline also bounds any in-flight status request and its retries. An earlier per-request deadline can surface as `REQUEST_TIMEOUT`, still with the saved asset ID.
45
+
46
+ `generate_private_url` accepts `expires_in=3600` (60–604800 integer seconds), `variant='webp'|'avif'` (default `webp`), plus request options. The server rejects unprocessed/public assets or unavailable variants. Private URLs authorize access: do not place them in ordinary logs.
47
+
48
+ `UploadAccepted` requires `id`, `status`, `deletion_status`, `visibility`, `original_name`, `original_size`, `asset_type`. The wire `asset_id` is normalized to `id`. Optional fields include delivery URLs, `private_url_expires_at`, `data`, `external_id`, `options`, `quality`, `warning`, `deletion_requested_at`.
49
+
50
+ `UploadAsset` adds `kompressd_size`, `savings_percent`, `can_generate_private_url`; `original_name` is optional or empty for older records. Optional `input_format`, `output_format` and `created_at` may be returned. Sizes are bytes. Processing states are `pending`, `processing`, `processed`, `failed`, `unprocessable`, `unknown`; deletion is independent (`active`, `deleting`, `deleted`, `legacy_deleted`). Private assets cannot contain public URLs; nonactive assets cannot contain delivery URLs. A pending public URL does not prove readiness.
51
+
52
+ `PrivateUrl` contains `asset_id`, `variant`, `private_url`, `expires_at`, `expires_in`.
53
+
54
+ ## Listing and lifecycle
55
+
56
+ | Method | Arguments | Result |
57
+ | --- | --- | --- |
58
+ | `list_uploads` | Upload filters below | `Page[UploadAsset]` |
59
+ | `iterate_uploads` | Same filters | Lazy sync iterator / async iterator of `UploadAsset` |
60
+ | `list_assets` | `page=1`, `limit=20`, optional `deletion_status`, request options | `Page[AssetState]` |
61
+ | `get_asset` | `asset_id`, request options | `AssetState` |
62
+ | `delete_asset` | `asset_id`, request options | Accepted or completed deletion `AssetState` |
63
+ | `bulk_delete_assets` | List of 1–100 explicit numeric IDs, request options | `BulkDeletion` |
64
+
65
+ Upload filters are keyword-only `project_id` (required from constructor or call), `page=1`, `limit=20` (1–100), `visibility`, `external_id` (at most 512 UTF-8 bytes), `data` (up to five string-valued filters), `status` (pending/processing/processed/failed/unprocessable or integer 1–6), `deletion_status`, `timeout`, `cancel`. The server defaults deletion filtering to active. Status names are mapped to the numeric wire filter.
66
+
67
+ `Page[T]` contains `data: list[T]`, `page`, `limit`, `total`. Iteration fetches only as needed, checks page progression and stops on an empty page or the reported total. Each page has a separate finite request deadline. Breaking iteration prevents further requests. Offset-based listings can change while files are added or deleted.
68
+
69
+ `AssetState` requires `id`, `project_id`, `kind`, `source` (`upload`/`origin`), numeric `processing_status`, `deletion_status`, `storage_credited_mb`. Optional fields: `deletion_requested_at`, `accounting_status` (`pending`/`settled`/`unavailable`), `last_failure: {code, message, at}`. This lifecycle shape does not contain upload delivery metadata. List credits are zero; inspect detail for accounting.
70
+
71
+ `BulkDeletion` preserves `results` (each `id`, HTTP-like `status`, `accepted`, optional `state`, `code`, `error`) and `counts` (`total`, `accepted`, `failed`). Duplicate input IDs count toward the input limit and are sent unchanged; response validation expects first-seen deduplication. Success of the HTTP request does not mean every item succeeded. Results and counts are validated together.
72
+
73
+ Asset IDs must be positive integers no greater than 9007199254740991, retaining compatibility with the JS baseline. Booleans, strings and Python floats are rejected as ID inputs. Deletion is permanent and asynchronous; acceptance does not prove storage cleanup or cache expiry. There is no delete-by-URL, restore, implicit batch expansion or automatic mutation retry.
74
+
75
+ ## Platform administration
76
+
77
+ Use `client.platform` or a standalone `PlatformClient`/`AsyncPlatformClient` with project authority. A scoped key cannot gain administration access by constructing a different client.
78
+
79
+ | Method | Arguments | Result |
80
+ | --- | --- | --- |
81
+ | `list_users` | `page=1`, `limit=20`, request options | `ProjectUserPage` |
82
+ | `create_user` | `customer_user_id`, optional `display_name`, `metadata`, request options | `{created, project_user}` |
83
+ | `update_user` | `project_user_id`, optional `display_name`, `metadata`, request options | `ProjectUser` |
84
+ | `suspend_user` | `project_user_id`, request options | `ProjectUser` |
85
+ | `reactivate_user` | `project_user_id`, request options | `ProjectUser` |
86
+ | `list_upload_keys` | `project_user_id`, `page=1`, `limit=20`, request options | `Page[UploadKey]` |
87
+ | `create_upload_key` | `project_user_id`, `name`, optional policy below, request options | `IssuedKey` |
88
+ | `rotate_upload_key` | `project_user_id`, key-record `key_id`, request options | `IssuedKey` |
89
+ | `revoke_upload_key` | `project_user_id`, key-record `key_id`, request options | `None` |
90
+
91
+ Project/user/key-record identifiers are UUID strings. Stable `customer_user_id` supports 1–256 UTF-8 bytes; the server finds or creates that identity. Display names support 120 bytes and can be cleared with `''`; omitted or `None` fields are not sent. Use `{}` to clear metadata.
92
+
93
+ `create_upload_key` policy: optional `scopes: list[UploadScope]` drawn from `upload`, `status`, `list`, `private_url`, `delete`; `allowed_visibility='public'|'private'|'both'`; `allowed_asset_types: list[int]` (positive IDs; empty means all supported); `expires_at` (future ISO timestamp with timezone). Key names require 1–120 UTF-8 bytes. Omitted scopes (or `None`) use server defaults excluding delete; a nonempty explicit list replaces defaults. An explicitly empty list raises `INVALID_ARGUMENT` before any request: do not create a key when your permission filter selects no scopes. Issuing another key is not idempotent, and the SDK invents no spending limits.
94
+
95
+ `ProjectUser` contains identity/project IDs, `customer_user_id`, `status` (`active`/`suspended`) and optional `display_name`, metadata, usage counters and activity timestamps. `ProjectUserPage` adds `project_active_count`, `org_active_count`, optional `usage_period: {start,end}` date strings. `UploadKey` contains `id`, `key_id`, project/user IDs, `name`, scopes, `allowed_visibility`, optional asset types and expiry/revocation/activity timestamps.
96
+
97
+ `IssuedKey` contains `id`, `key_id`, `api_key` and optional `upload_key`. The raw key is returned once and must be stored or delivered securely; list methods do not recover it. Suspension revokes keys without deleting files. Reactivation does not restore revoked keys; issue a new key. Rotation replaces the secret.
98
+
99
+ ## Error and cancellation contract
100
+
101
+ `KompressrError` exposes `kind`, `code`, optional `status`, `retry_after_ms`, `asset_id`, and boolean `may_have_succeeded`. `to_dict()` returns these safe fields and the message. Kinds: `validation`, `api`, `network`, `timeout`, `cancelled`, `invalid_response`, `processing`. Known credentials, bearer values, URLs and credential assignments are redacted from error messages; raw response bodies and one-time secrets are not attached. Do not log successful key or signed-URL result dictionaries.
102
+
103
+ Eligible GET network errors and HTTP 429/502/503/504 retry with bounded backoff; `Retry-After` seconds/date or body `retry_after` is respected inside the total deadline. Other statuses, malformed successful responses and all writes have no automatic replay. Failed envelopes are handled even under HTTP 200. Responses above 2 MiB or malformed shapes raise `INVALID_RESPONSE`. Requests advertise `Accept-Encoding: identity`; other response content encodings are rejected before decompression. Successful compressed responses raise `INVALID_RESPONSE`; error responses retain their HTTP status and header retry delay without parsing the compressed body. Request timeouts and cancellation preserve the last known response status and retry delay, including while reading a body or waiting to retry. Starting a new attempt clears that prior response context.
104
+
105
+ If a server rejects a large upload before reading its body and closes the connection, HTTPX can report a write/network error before exposing the HTTP response. The SDK then returns `NETWORK_ERROR` with `may_have_succeeded=True`, rather than the server's rejection status. Inspect the upload outcome before retrying; the SDK does not replay it automatically.
106
+
107
+ A dispatched mutation can have succeeded despite timeout, network failure, task cancellation, rejected redirect, server failure, or invalid successful data. These errors set `may_have_succeeded=True`. Pre-dispatch cancellation and local validation set it false. A false value does not certify that replaying an entire application workflow is safe. Single-asset writes retain the requested ID. Upload recovery only trusts a positive safe numeric `data.asset_id` from a parsed successful envelope; broken JSON has no recoverable ID. Bulk errors never choose one arbitrary ID.
108
+
109
+ `CancellationToken.cancel()` is thread-safe; pass the token as `cancel=`. Cancellation is checked before dispatch and during network activity/backoff with a small polling interval. Token cancellation raises `KompressrError`; native asyncio task cancellation raises `KompressrCancelledError`, retaining `asyncio.CancelledError` semantics and the same metadata. Catch native cancellation only for cleanup, then re-raise. Native task cancellation before a coroutine starts cannot attach SDK metadata because no SDK code has run. Standard local file errors may occur before a request and remain native Python exceptions.
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ - Reject explicitly empty scoped-key permissions before dispatch; omission still uses server defaults.
6
+ - Reject nonidentity response encodings before decompression to preserve bounded response memory and write-error context.
7
+ - Retain known retry delays on request timeout and cancellation, clearing prior response metadata before another attempt.
8
+
9
+ - Add typed sync and asyncio clients for uploads, readiness, private signing, listing and asset lifecycle operations.
10
+ - Add complete project identity and scoped-key administration.
11
+ - Bound DNS worker and connection resources, preserve TLS hostname verification and ensure slow resolver calls cannot delay cancellation or cause late writes.
12
+ - Preserve bounded deadlines, read-only retries, cancellation, ambiguous-write context and credential-safe errors.
13
+ - Add local HTTP regressions, package consumer verification, runnable examples and publishing guidance.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kompressr
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.
@@ -0,0 +1,36 @@
1
+ # Behavior and verification map
2
+
3
+ Baseline: JS SDK 1.0.0 and the shared API contract. `tests/test_sdk.py` uses real loopback HTTP servers and synthetic credentials. The lifecycle fixture is copied from `contracts/fixtures/file-lifecycle.json` so source distributions can test independently.
4
+
5
+ | JS surface / guarantee | Python surface | Regression evidence in `tests/test_sdk.py` |
6
+ | --- | --- | --- |
7
+ | uploadFile / uploadBytes / uploadBlob | upload_file / upload_bytes / upload_stream | real_file_multipart_separate_origins_and_polling; bytes_view_boundaries_and_snapshot; stream_reads_are_bounded_and_caller_retains_ownership; large_file_streaming_has_bounded_memory (32 MiB file, under 8 MiB traced allocations) |
8
+ | getUploadStatus / waitUntilReady | get_upload_status / wait_until_ready | private_lifecycle_and_signing; optional_original_name; terminal_processing_states_with_url; readiness_requires_active_correct_delivery |
9
+ | generatePrivateUrl | generate_private_url | private_lifecycle_and_signing; mutations_invalid_json_and_incomplete_shapes_keep_ambiguity |
10
+ | listUploads / iterateUploads | list_uploads / iterate_uploads | pagination_filters_lazy_progression; invalid_pagination_and_empty_stop |
11
+ | listAssets / getAsset / deleteAsset / bulkDeleteAssets | list_assets / get_asset / delete_asset / bulk_delete_assets | asset_listing_needs_no_project; deletion_states_and_per_item_bulk_results; mismatched_single_asset_response_retains_requested_id |
12
+ | All nine platform methods | Same names in snake_case on platform | platform_all_routes_one_time_secrets_and_no_delete_default; platform_explicit_restrictions_and_counter_validation; empty_scopes_rejected_while_omission_keeps_defaults; empty_scopes_rejected_before_async_dispatch |
13
+ | Total request and waiting budgets | timeout seconds | slow_response_body_has_total_request_deadline; slow_body_wait_deadline_preserves_id; slow_write_deadline_ambiguity_and_no_replay; retry_after_never_shortened_and_deadline_includes_delay |
14
+ | Slow DNS, resolver saturation and TLS origins | Bounded shared DNS workers; sequential deadline-aware address fallback | tests/test_dns.py: sync_dns_timeout_returns_before_os_lookup_finishes_without_late_write; asyncio_run_exits_before_blocked_dns_finishes; dns_capacity_is_bounded_and_waiters_are_cancellable; stalled_first_address_leaves_time_for_healthy_fallback; https_keeps_original_host_sni_and_certificate_validation |
15
+ | Cancellation before/after dispatch | CancellationToken and asyncio task cancellation | pre_cancel_sends_nothing; cancel_during_write_retains_sync_metadata; native_task_cancel_keeps_cancelled_semantics_and_write_context; native_wait_cancel_has_wait_code |
16
+ | No blind write replay | Structured may_have_succeeded and asset_id | network_read_retries_but_writes_do_not; mutations_invalid_json_and_incomplete_shapes_keep_ambiguity (all eleven mutation families, broken/incomplete responses); unsafe_upload_ids_are_never_recovered |
17
+ | Retry permissions and bounds | GET-only retries, no key substitution | eligible_read_retries_and_bounded_attempts; permission_and_non_retryable_errors_preserve_scope |
18
+ | Retry metadata through interruptions | Preserve known delay; clear at the next attempt | retry_delay_survives_deadline_and_token_cancellation; native_cancellation_retains_retry_delay; new_attempt_clears_previous_retry_metadata |
19
+ | Credential isolation | Redirect refusal, redacted structured errors | redirects_never_follow_or_leak_credentials; error_redaction_and_html_exclusion |
20
+ | Input and response validation | Strict typed results and validation errors | invalid_inputs_rejected_before_network; response_size_limit_and_json_nonfinite; compression_bomb_is_rejected_before_expansion; compressed_responses_are_rejected_with_write_context; identity_response_and_compressed_http_error_metadata; readiness_requires_active_correct_delivery |
21
+ | Installable distribution | Wheel and sdist, py.typed | scripts/verify.py installs a fresh consumer, checks types and repeats the HTTP suite |
22
+
23
+ Test method names have the `test_` prefix. Parameterized scenarios use unittest subtests, so a test count does not equal the number of cases.
24
+
25
+ ## Intentional Python differences
26
+
27
+ - Python uses snake_case methods/options and typed dictionaries. All deadlines and polling intervals use seconds; URL lifetimes remain integer seconds and retry metadata remains milliseconds.
28
+ - Both sync and asyncio clients are provided. The sync adapter cannot be used inside an already running asyncio event loop. Native task cancellation remains a subclass of `asyncio.CancelledError`; cooperative cancellation tokens yield `KompressrError`.
29
+ - Python has no Blob. `upload_stream` accepts standard regular binary files and BytesIO, rewinds to zero and retains caller ownership. Custom readers/subclasses, pipes and sockets are rejected before reading; arbitrary source code must not block the cancellation loop. File reads are bounded but ordinary filesystem operations can block at the operating-system level. `upload_file` opens/closes its own regular file.
30
+ - Mutable bytes/memoryview inputs are snapshotted when the async operation begins executing. Merely creating a coroutine does not execute it. Keep source files unchanged until the upload completes.
31
+ - DNS uses at most eight daemon workers; blocked OS lookups retain slots until completion while cancelled callers return promptly. Live connections are capped at 64. Address fallback is sequential, sharing the remaining deadline among remaining addresses, so fallback can be slower than Happy Eyeballs. Admission waiting is part of the request budget.
32
+ - Request timeout options allow fractional seconds. Integer IDs and lifetimes reject Python booleans/floats; asset IDs retain the JS safe-integer ceiling. API keys require printable ASCII. Expiry input requires an explicit timezone. Filename validation rejects all control characters.
33
+ - Optional response fields receive stricter validation when present (quality, warning, key timestamps and permitted asset types). Future incompatible shapes fail visibly rather than entering typed results.
34
+ - HTTP pools live for one operation and all its retries, then close. This simplifies sync/async ownership and cross-thread use but does not reuse connections between separate operations. Environment proxy settings and caller-supplied HTTP transports are not supported.
35
+
36
+ The suite establishes wire behavior against controlled HTTP fixtures. It does not establish current account entitlements, production scope enforcement, service availability or publishing ownership. Those are separate live/release checks.
@@ -0,0 +1,133 @@
1
+ Metadata-Version: 2.4
2
+ Name: kompressr-sdk
3
+ Version: 0.1.0
4
+ Summary: Typed synchronous and asynchronous Python clients for Kompressr
5
+ Project-URL: Homepage, https://kompressr.com
6
+ Project-URL: Repository, https://github.com/kompressrcdn/clients
7
+ Project-URL: Issues, https://github.com/kompressrcdn/clients/issues
8
+ Author: Kompressr
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Programming Language :: Python :: 3.15
18
+ Classifier: Typing :: Typed
19
+ Requires-Python: >=3.11
20
+ Requires-Dist: httpcore[asyncio]<2,>=1.0.9
21
+ Requires-Dist: httpx<1,>=0.28.1
22
+ Provides-Extra: dev
23
+ Requires-Dist: build<2,>=1.2; extra == 'dev'
24
+ Requires-Dist: cryptography<51,>=45; extra == 'dev'
25
+ Requires-Dist: mypy<2,>=1.15; extra == 'dev'
26
+ Requires-Dist: twine<7,>=6; extra == 'dev'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # Kompressr for Python
30
+
31
+ Upload and manage files from Python backends, scripts and asyncio applications. Requires Python 3.11 or later.
32
+
33
+ ## Install
34
+
35
+ ```sh
36
+ python -m pip install kompressr-sdk
37
+ ```
38
+
39
+ Use `KompressrClient` for synchronous applications, including Django views and background workers. Use `AsyncKompressrClient` in FastAPI async handlers or Django async views. No framework adapter is required.
40
+
41
+ ## Upload a file
42
+
43
+ Create a Kompressr project and put its API key in your server's `KOMPRESSR_API_KEY` environment variable. Keep project credentials on your backend. API uploads and file types depend on your [plan](https://kompressr.com/pricing).
44
+
45
+ ```python
46
+ import os
47
+ from kompressr import KompressrClient
48
+
49
+ files = KompressrClient(api_key=os.environ['KOMPRESSR_API_KEY'])
50
+ accepted = files.upload_file('image.png', visibility='public')
51
+ # Persist accepted['id'] in your application's database before waiting.
52
+ ready = files.wait_until_ready(accepted['id'])
53
+ print(ready['cdn_url'])
54
+ ```
55
+
56
+ Acceptance means the server has the upload; processing may still be running. Save the numeric asset ID before waiting. If a wait times out, use that saved ID to resume status checks. Reuploading can create another asset.
57
+
58
+ For private files, use `visibility='private'`, wait for readiness, then call `files.generate_private_url(asset_id)`. The result's `private_url` is an expiring authorization link. Return it only to an authorized user and keep it out of logs. Private readiness does not automatically create a fresh link.
59
+
60
+ The runnable [upload example](https://github.com/kompressrcdn/clients/blob/main/sdk-python/examples/upload.py) saves a receipt before waiting and supports `--resume` with no second upload:
61
+
62
+ ```sh
63
+ python sdk-python/examples/upload.py image.png --receipt receipt.json
64
+ python sdk-python/examples/upload.py --resume --receipt receipt.json
65
+ ```
66
+
67
+ The example prints a delivery link to your terminal when successful. Treat terminal history and captured output accordingly. Choose `--public` only for files intended for public delivery.
68
+
69
+ ## Asyncio
70
+
71
+ Use `AsyncKompressrClient` in async applications. Methods and results match the synchronous client; await each call and use `async for` with `iterate_uploads`.
72
+
73
+ ```python
74
+ import asyncio
75
+ import os
76
+ from kompressr import AsyncKompressrClient
77
+
78
+ async def main():
79
+ files = AsyncKompressrClient(api_key=os.environ['KOMPRESSR_API_KEY'])
80
+ accepted = await files.upload_file('image.png', visibility='public')
81
+ # Save accepted['id'] durably here before waiting.
82
+ ready = await files.wait_until_ready(accepted['id'])
83
+ print(ready['cdn_url'])
84
+
85
+ asyncio.run(main())
86
+ ```
87
+
88
+ The synchronous client cannot run inside an active asyncio event loop; use the async client there. Clients create and close an HTTP connection pool per operation, so no `close()` is required and a sync client can be reused across threads. Async clients require asyncio; Trio is not supported. Regular files are read in bounded chunks; brief local file reads happen on the calling event-loop thread.
89
+
90
+ ## Failures and recovery
91
+
92
+ ```python
93
+ from kompressr import KompressrError
94
+
95
+ try:
96
+ ready = files.wait_until_ready(saved_asset_id, timeout=60)
97
+ except KompressrError as error:
98
+ if error.code == 'WAIT_TIMEOUT':
99
+ # Keep error.asset_id and resume checking it later.
100
+ pass
101
+ else:
102
+ raise
103
+ ```
104
+
105
+ All timeout values are **seconds**. Management requests default to 30 seconds, uploads to 600 seconds and readiness waiting to 120 seconds. A request deadline includes body reads and retry delays. Eligible GET failures may retry twice; uploads, signing, deletion and administration writes are never automatically replayed. When a write fails with `may_have_succeeded=True`, inspect the saved asset or operation outcome before deciding what to do next. A safely recovered `error.asset_id` may be available even when an upload acceptance response is malformed. Unknown outcomes can require application reconciliation.
106
+
107
+ Pass a `CancellationToken` using `cancel=` to stop sync or async work cooperatively. Calling `token.cancel()` is thread-safe. Native async task cancellation raises `KompressrCancelledError`, a subclass of `asyncio.CancelledError`, with the same recovery metadata. Catch it only to save necessary state, then re-raise so structured cancellation works. Cancelling an operation cannot roll back server work.
108
+
109
+ ## Scoped keys and administration
110
+
111
+ A regular project key can operate across that project and administer identities. A scoped key is bound to an identity, allowed actions, visibility and file types. Kompressr enforces those restrictions; the SDK never substitutes a stronger key. Your application still authenticates its users, binds their keys to the correct session, stores keys securely and authorizes conversation access.
112
+
113
+ Use `files.platform` with a project key to create identities and issue scoped keys. The [platform example](https://github.com/kompressrcdn/clients/blob/main/sdk-python/examples/platform.py) explains secure one-time key storage and explicit key rotation/revocation. Scoped keys require a plan and project type supporting them. Delete permission is opt-in; the SDK leaves omitted scopes to the server's non-destructive defaults. See the [chatbot guide](https://github.com/kompressrcdn/clients/blob/main/guides/chatbot-integration.md) for the application flow.
114
+
115
+ ## More operations
116
+
117
+ See [API.md](https://github.com/kompressrcdn/clients/blob/main/sdk-python/API.md) for all file and platform methods, options and response fields; [PARITY.md](https://github.com/kompressrcdn/clients/blob/main/sdk-python/PARITY.md) maps behavior to tests. Results are typed dictionaries with API snake_case keys. File uploads stream from regular files; `upload_bytes` supports bytes, bytearray and selected memoryview slices. `upload_stream` supports a standard open regular binary file or BytesIO (custom readers/subclasses are rejected), rewinds to the beginning and leaves ownership with the caller.
118
+
119
+ List uploads with `project_id` supplied to the constructor or call. `iterate_uploads` fetches pages lazily. Asset list/detail do not require a project ID: the server derives the project from the key. Deletion is permanent and asynchronous; an accepted deletion is not proof that storage cleanup or cache expiry has finished. Bulk deletion keeps each item's outcome even under HTTP 200.
120
+
121
+ ## Development
122
+
123
+ ```sh
124
+ cd sdk-python
125
+ python -m venv .venv
126
+ . .venv/bin/activate
127
+ python -m pip install -e '.[dev]'
128
+ python scripts/verify.py
129
+ ```
130
+
131
+ Verification typechecks, runs local HTTP tests, builds wheel/source distributions, checks archive contents, installs the wheel in a fresh environment and repeats HTTP tests against that installed package. It uses synthetic credentials and no Kompressr account.
132
+
133
+ MIT licensed. Report bugs through the repository's issue tracker and security concerns through [SECURITY.md](https://github.com/kompressrcdn/clients/blob/main/SECURITY.md).
@@ -0,0 +1,105 @@
1
+ # Kompressr for Python
2
+
3
+ Upload and manage files from Python backends, scripts and asyncio applications. Requires Python 3.11 or later.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ python -m pip install kompressr-sdk
9
+ ```
10
+
11
+ Use `KompressrClient` for synchronous applications, including Django views and background workers. Use `AsyncKompressrClient` in FastAPI async handlers or Django async views. No framework adapter is required.
12
+
13
+ ## Upload a file
14
+
15
+ Create a Kompressr project and put its API key in your server's `KOMPRESSR_API_KEY` environment variable. Keep project credentials on your backend. API uploads and file types depend on your [plan](https://kompressr.com/pricing).
16
+
17
+ ```python
18
+ import os
19
+ from kompressr import KompressrClient
20
+
21
+ files = KompressrClient(api_key=os.environ['KOMPRESSR_API_KEY'])
22
+ accepted = files.upload_file('image.png', visibility='public')
23
+ # Persist accepted['id'] in your application's database before waiting.
24
+ ready = files.wait_until_ready(accepted['id'])
25
+ print(ready['cdn_url'])
26
+ ```
27
+
28
+ Acceptance means the server has the upload; processing may still be running. Save the numeric asset ID before waiting. If a wait times out, use that saved ID to resume status checks. Reuploading can create another asset.
29
+
30
+ For private files, use `visibility='private'`, wait for readiness, then call `files.generate_private_url(asset_id)`. The result's `private_url` is an expiring authorization link. Return it only to an authorized user and keep it out of logs. Private readiness does not automatically create a fresh link.
31
+
32
+ The runnable [upload example](https://github.com/kompressrcdn/clients/blob/main/sdk-python/examples/upload.py) saves a receipt before waiting and supports `--resume` with no second upload:
33
+
34
+ ```sh
35
+ python sdk-python/examples/upload.py image.png --receipt receipt.json
36
+ python sdk-python/examples/upload.py --resume --receipt receipt.json
37
+ ```
38
+
39
+ The example prints a delivery link to your terminal when successful. Treat terminal history and captured output accordingly. Choose `--public` only for files intended for public delivery.
40
+
41
+ ## Asyncio
42
+
43
+ Use `AsyncKompressrClient` in async applications. Methods and results match the synchronous client; await each call and use `async for` with `iterate_uploads`.
44
+
45
+ ```python
46
+ import asyncio
47
+ import os
48
+ from kompressr import AsyncKompressrClient
49
+
50
+ async def main():
51
+ files = AsyncKompressrClient(api_key=os.environ['KOMPRESSR_API_KEY'])
52
+ accepted = await files.upload_file('image.png', visibility='public')
53
+ # Save accepted['id'] durably here before waiting.
54
+ ready = await files.wait_until_ready(accepted['id'])
55
+ print(ready['cdn_url'])
56
+
57
+ asyncio.run(main())
58
+ ```
59
+
60
+ The synchronous client cannot run inside an active asyncio event loop; use the async client there. Clients create and close an HTTP connection pool per operation, so no `close()` is required and a sync client can be reused across threads. Async clients require asyncio; Trio is not supported. Regular files are read in bounded chunks; brief local file reads happen on the calling event-loop thread.
61
+
62
+ ## Failures and recovery
63
+
64
+ ```python
65
+ from kompressr import KompressrError
66
+
67
+ try:
68
+ ready = files.wait_until_ready(saved_asset_id, timeout=60)
69
+ except KompressrError as error:
70
+ if error.code == 'WAIT_TIMEOUT':
71
+ # Keep error.asset_id and resume checking it later.
72
+ pass
73
+ else:
74
+ raise
75
+ ```
76
+
77
+ All timeout values are **seconds**. Management requests default to 30 seconds, uploads to 600 seconds and readiness waiting to 120 seconds. A request deadline includes body reads and retry delays. Eligible GET failures may retry twice; uploads, signing, deletion and administration writes are never automatically replayed. When a write fails with `may_have_succeeded=True`, inspect the saved asset or operation outcome before deciding what to do next. A safely recovered `error.asset_id` may be available even when an upload acceptance response is malformed. Unknown outcomes can require application reconciliation.
78
+
79
+ Pass a `CancellationToken` using `cancel=` to stop sync or async work cooperatively. Calling `token.cancel()` is thread-safe. Native async task cancellation raises `KompressrCancelledError`, a subclass of `asyncio.CancelledError`, with the same recovery metadata. Catch it only to save necessary state, then re-raise so structured cancellation works. Cancelling an operation cannot roll back server work.
80
+
81
+ ## Scoped keys and administration
82
+
83
+ A regular project key can operate across that project and administer identities. A scoped key is bound to an identity, allowed actions, visibility and file types. Kompressr enforces those restrictions; the SDK never substitutes a stronger key. Your application still authenticates its users, binds their keys to the correct session, stores keys securely and authorizes conversation access.
84
+
85
+ Use `files.platform` with a project key to create identities and issue scoped keys. The [platform example](https://github.com/kompressrcdn/clients/blob/main/sdk-python/examples/platform.py) explains secure one-time key storage and explicit key rotation/revocation. Scoped keys require a plan and project type supporting them. Delete permission is opt-in; the SDK leaves omitted scopes to the server's non-destructive defaults. See the [chatbot guide](https://github.com/kompressrcdn/clients/blob/main/guides/chatbot-integration.md) for the application flow.
86
+
87
+ ## More operations
88
+
89
+ See [API.md](https://github.com/kompressrcdn/clients/blob/main/sdk-python/API.md) for all file and platform methods, options and response fields; [PARITY.md](https://github.com/kompressrcdn/clients/blob/main/sdk-python/PARITY.md) maps behavior to tests. Results are typed dictionaries with API snake_case keys. File uploads stream from regular files; `upload_bytes` supports bytes, bytearray and selected memoryview slices. `upload_stream` supports a standard open regular binary file or BytesIO (custom readers/subclasses are rejected), rewinds to the beginning and leaves ownership with the caller.
90
+
91
+ List uploads with `project_id` supplied to the constructor or call. `iterate_uploads` fetches pages lazily. Asset list/detail do not require a project ID: the server derives the project from the key. Deletion is permanent and asynchronous; an accepted deletion is not proof that storage cleanup or cache expiry has finished. Bulk deletion keeps each item's outcome even under HTTP 200.
92
+
93
+ ## Development
94
+
95
+ ```sh
96
+ cd sdk-python
97
+ python -m venv .venv
98
+ . .venv/bin/activate
99
+ python -m pip install -e '.[dev]'
100
+ python scripts/verify.py
101
+ ```
102
+
103
+ Verification typechecks, runs local HTTP tests, builds wheel/source distributions, checks archive contents, installs the wheel in a fresh environment and repeats HTTP tests against that installed package. It uses synthetic credentials and no Kompressr account.
104
+
105
+ MIT licensed. Report bugs through the repository's issue tracker and security concerns through [SECURITY.md](https://github.com/kompressrcdn/clients/blob/main/SECURITY.md).
@@ -0,0 +1,45 @@
1
+ """Provision an identity and store its one-time key in a new private file."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import os
7
+ import sys
8
+
9
+ from kompressr import KompressrClient, KompressrError
10
+
11
+
12
+ def main():
13
+ parser = argparse.ArgumentParser(description=__doc__)
14
+ parser.add_argument('customer_user_id', help='Stable identity from your authenticated application')
15
+ parser.add_argument('--key-file', required=True, help='New private output file outside the repository')
16
+ args = parser.parse_args()
17
+ key = os.environ.get('KOMPRESSR_API_KEY')
18
+ if not key:
19
+ parser.error('Set KOMPRESSR_API_KEY to your project credential on a trusted backend.')
20
+ files = KompressrClient(api_key=key, api_base_url=os.environ.get('KOMPRESSR_API_BASE_URL', 'https://kompressr.com'),
21
+ upload_base_url=os.environ.get('KOMPRESSR_UPLOAD_BASE_URL'))
22
+ # Reserve the secret destination before issuing a nonrecoverable raw key.
23
+ descriptor = os.open(args.key_file, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
24
+ with os.fdopen(descriptor, 'w') as destination:
25
+ user = files.platform.create_user(args.customer_user_id)['project_user']
26
+ issued = files.platform.create_upload_key(user['id'], 'Application runtime',
27
+ scopes=['upload', 'status', 'list', 'private_url'], allowed_visibility='private')
28
+ json.dump({'project_user_id': user['id'], 'key_record_id': issued['id'], 'api_key': issued['api_key']}, destination)
29
+ destination.flush()
30
+ os.fsync(destination.fileno())
31
+ print('Scoped key saved. Give it only to the authenticated runtime for this identity.')
32
+ # In production use an encrypted credential store instead of a local file.
33
+ # Later: rotated = files.platform.rotate_upload_key(user['id'], issued['id'])
34
+ # Persist rotated['api_key'] securely before switching the runtime to it.
35
+ # To revoke: files.platform.revoke_upload_key(user['id'], issued['id'])
36
+ # To suspend all keys: files.platform.suspend_user(user['id'])
37
+ # Reactivation does not restore revoked keys; create a new key afterwards.
38
+
39
+
40
+ if __name__ == '__main__':
41
+ try:
42
+ main()
43
+ except KompressrError as error:
44
+ print(json.dumps(error.to_dict()), file=sys.stderr)
45
+ sys.exit(1)
@@ -0,0 +1,63 @@
1
+ """Upload privately by default, save the ID, then print an authorized delivery link."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+ import sys
9
+
10
+ from kompressr import KompressrClient, KompressrError
11
+
12
+
13
+ def save(handle, client, asset_id, state):
14
+ handle.seek(0)
15
+ json.dump({'asset_id': asset_id, 'api_base_url': client.api_base_url, 'state': state}, handle)
16
+ handle.truncate()
17
+ handle.flush()
18
+ os.fsync(handle.fileno())
19
+
20
+
21
+ def main():
22
+ parser = argparse.ArgumentParser(description=__doc__)
23
+ parser.add_argument('file', nargs='?', type=Path)
24
+ parser.add_argument('--receipt', required=True, type=Path, help='New receipt path for upload, existing path for resume')
25
+ parser.add_argument('--resume', action='store_true', help='Check the saved ID without uploading')
26
+ parser.add_argument('--public', action='store_true', help='Upload for public delivery instead of private delivery')
27
+ args = parser.parse_args()
28
+ if args.resume == bool(args.file):
29
+ parser.error('Supply a file for a new upload, or --resume with no file.')
30
+ key = os.environ.get('KOMPRESSR_API_KEY')
31
+ if not key:
32
+ parser.error('Set KOMPRESSR_API_KEY in your server environment.')
33
+ files = KompressrClient(api_key=key, api_base_url=os.environ.get('KOMPRESSR_API_BASE_URL', 'https://kompressr.com'),
34
+ upload_base_url=os.environ.get('KOMPRESSR_UPLOAD_BASE_URL'))
35
+ if args.resume:
36
+ receipt = json.loads(args.receipt.read_text())
37
+ asset_id = receipt.get('asset_id')
38
+ if type(asset_id) is not int or asset_id < 1 or receipt.get('api_base_url') != files.api_base_url:
39
+ parser.error('The receipt needs a saved asset ID and the same configured management origin.')
40
+ else:
41
+ # Reserve storage before a write so a receipt cannot overwrite an older upload.
42
+ descriptor = os.open(args.receipt, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
43
+ with os.fdopen(descriptor, 'w') as handle:
44
+ save(handle, files, None, 'not_accepted')
45
+ try:
46
+ accepted = files.upload_file(args.file, visibility='public' if args.public else 'private')
47
+ except KompressrError as error:
48
+ save(handle, files, error.asset_id, 'uncertain' if error.may_have_succeeded else 'failed')
49
+ raise
50
+ asset_id = accepted['id']
51
+ save(handle, files, asset_id, 'accepted')
52
+ ready = files.wait_until_ready(asset_id)
53
+ # This is intentional terminal output; do not route private links into routine logs.
54
+ link = files.generate_private_url(asset_id)['private_url'] if ready['visibility'] == 'private' else ready['cdn_url']
55
+ print(link)
56
+
57
+
58
+ if __name__ == '__main__':
59
+ try:
60
+ main()
61
+ except KompressrError as error:
62
+ print(json.dumps(error.to_dict()), file=sys.stderr)
63
+ sys.exit(1)
@@ -0,0 +1,45 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27,<2"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "kompressr-sdk"
7
+ version = "0.1.0"
8
+ description = "Typed synchronous and asynchronous Python clients for Kompressr"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{name = "Kompressr"}]
14
+ dependencies = ["httpx>=0.28.1,<1", "httpcore[asyncio]>=1.0.9,<2"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Programming Language :: Python :: 3.13",
21
+ "Programming Language :: Python :: 3.14",
22
+ "Programming Language :: Python :: 3.15",
23
+ "Typing :: Typed",
24
+ ]
25
+
26
+ [project.urls]
27
+ Homepage = "https://kompressr.com"
28
+ Repository = "https://github.com/kompressrcdn/clients"
29
+ Issues = "https://github.com/kompressrcdn/clients/issues"
30
+
31
+ [project.optional-dependencies]
32
+ dev = ["build>=1.2,<2", "mypy>=1.15,<2", "twine>=6,<7", "cryptography>=45,<51"]
33
+
34
+ [tool.hatch.build.targets.wheel]
35
+ core-metadata-version = "2.4"
36
+ packages = ["src/kompressr"]
37
+
38
+ [tool.hatch.build.targets.sdist]
39
+ core-metadata-version = "2.4"
40
+ include = ["/src", "/tests", "/examples", "/scripts", "/README.md", "/API.md", "/PARITY.md", "/CHANGELOG.md", "/LICENSE", "/pyproject.toml"]
41
+
42
+ [tool.mypy]
43
+ python_version = "3.11"
44
+ strict = true
45
+ files = ["src/kompressr"]