ThailandHosting Object Storage — Python SDK
Python client for ThailandHosting Object Storage: buckets, objects, large files and share links over the ThailandHosting API.
pip install thailandhosting-object-storage
Python 3.9+. One dependency: urllib3.
Quick start
import os
from thailandhosting_objectstorage import Client, NotFoundError
client = Client(
endpoint="https://objects.thailandhosting.com/th-bkk-1",
access_key_id=os.environ["TH_ACCESS_KEY_ID"],
secret_access_key=os.environ["TH_SECRET_ACCESS_KEY"],
)
bucket = client.bucket("my-bucket")
# Any size: large files go up as parallel parts.
bucket.upload_file("backups/db.dump", "/var/backups/db.dump")
# Large objects come down as parallel ranges.
bucket.download_file("backups/db.dump", "/tmp/db.dump")
# Small objects
bucket.put("notes/hello.txt", "สวัสดี", metadata={"author": "me"})
print(bucket.get_bytes("notes/hello.txt").decode())
# Streaming download
with bucket.get("videos/intro.mp4") as stream:
for chunk in stream.iter_chunks():
...
# Every object under a prefix
for obj in bucket.iter_objects("backups/"):
print(obj.key, obj.size)
# A link that works without a key for one hour
print(bucket.create_link("backups/db.dump", expires_in=3600).url)
try:
bucket.head("missing")
except NotFoundError as e:
print(e.code) # object_not_found
Create access keys in the Console (Object Storage → Access keys). Reuse one
Client for the life of your program: it is thread-safe and keeps
connections open.
Authentication
Every request is signed with HTTP Message Signatures (RFC 9421), HMAC-SHA256 keyed with your secret access key. The secret never leaves your program: requests carry only the access key ID and the signature.
Content-Digest: sha-256=:<base64 SHA-256 of the body>:
Signature-Input: th=("@method" "@authority" "@path" "@query" "content-digest");created=<unix time>;keyid="<access key id>";alg="hmac-sha256"
Signature: th=:<base64 HMAC-SHA256>:
The signature covers the method, host, path and query, and the body's
Content-Digest (RFC 9530) whenever there is a body; the service checks the
digest against the bytes as they arrive. Each attempt is signed afresh, and
created must be within 5 minutes of the service's clock, so keep the
machine's time in sync (NTP). Share links (create_link) work without a key,
as before.
Conditional writes and reads
from thailandhosting_objectstorage import NotModifiedError, PreconditionFailedError
# Create only if the key is free (also on upload_file / upload_fileobj).
try:
bucket.put("locks/job-42", b"me", if_none_match="*")
except PreconditionFailedError:
print("someone else holds it")
# Replace or delete only the version you read (optimistic locking).
info = bucket.head("config.json")
bucket.put("config.json", new_config, if_match=info.etag)
bucket.delete("config.json", if_match=info.etag)
# Download only if it changed since your cached copy.
try:
data = bucket.get_bytes("config.json", if_none_match=cached_etag)
except NotModifiedError:
data = cached_data
The service checks if_match / if_none_match atomically with the write; a
large upload applies them when its parts are joined. get, get_bytes and
head also take if_modified_since / if_unmodified_since.
Public buckets and folders
from thailandhosting_objectstorage import Client, PublicRule
# Anyone may read images/ (and nothing else) without a key; needs a key
# allowed to manage buckets. [] makes the bucket private again.
bucket.set_public_access([PublicRule("images/")])
print(bucket.public_url("images/logo.png"))
# https://objects.thailandhosting.com/th-bkk-1/my-bucket/images/logo.png
# A client without a key reads (and, where allowed, lists) public objects.
anon = Client("https://objects.thailandhosting.com/th-bkk-1")
data = anon.bucket("my-bucket").get_bytes("images/logo.png")
What a key may do (buckets, folders, read / list / write / delete, managing buckets, an expiry date) is set on the key in the Console.
Performance
| Connections | urllib3 pool, keep-alive; pool_maxsize (default 2 × concurrency) |
| Uploads | upload_file reads parts straight from the file, one handle per thread, and sends them in parallel; upload_fileobj streams any binary stream (pipe, socket) holding at most concurrency + 1 parts in memory |
| Downloads | download_file fetches parallel ranges into the file, each pinned to the object's ETag; get streams |
| Small data | one request; a stream that turns out small is one request too |
| Tuning | part_size (default 16 MiB), concurrency (default 8), multipart_threshold (default 32 MiB) |
Hashing and socket I/O release the GIL, so parts really move in parallel.
Reliability
- Requests that are safe to repeat are retried with exponential backoff and
jitter (
max_retries, default 4) on 429, 500, 502, 503, 504 and connection errors, waiting at least as long asRetry-Afterasks. - Every body (each single upload, each part, each JSON request) carries its
SHA-256
Content-Digest; the service refuses a body damaged on the way (bad_digest) and stores nothing. A stream that cannot seek is hashed from the buffer each part is read into. Every upload and part is also checked against the MD5 the service reports; damaged data raisesIntegrityErrorand is never accepted. - Objects uploaded in one request keep their digest, which
getandheadreturn asinfo.digest(Repr-Digest). Whole-object downloads (get,get_bytes,download_file) are checked against it and raiseDigestMismatchError(anIntegrityError) on a mismatch. Ranges and objects uploaded in parts have no digest to check. - A failed multipart upload is aborted, so its parts are not billed.
download_filewrites topath + ".download"and renames it into place; a range whose connection breaks resumes where it stopped.
API
Client |
|
|---|---|
list_buckets(), create_bucket(name), get_bucket(name), delete_bucket(name) |
buckets |
bucket(name) |
a Bucket handle |
Bucket |
|
|---|---|
put, upload_file, upload_fileobj |
upload (content_type, metadata, cache_control, content_disposition, if_not_exists, if_match, if_none_match) |
get (stream; offset/length), get_bytes, download_file |
download (if_none_match / if_modified_since → NotModifiedError, if_match / if_unmodified_since → PreconditionFailedError) |
head, exists, delete (if_match, if_none_match), delete_many, copy |
object operations |
list (one page), iter_objects |
listing |
create_link(key, expires_in=, upload=) |
a link that needs no key |
public_access(), set_public_access(rules), public_url(key) |
make the bucket or some folders readable by anyone, and their URLs |
create_multipart_upload, resume_multipart_upload → upload_part, parts, complete (if_match, if_none_match), abort |
do-it-yourself multipart |
Errors from the service are Problem Details (RFC 9457,
application/problem+json) and raise ObjectStorageError with status,
code (for example object_not_found, bucket_already_exists,
invalid_signature, bad_digest, busy) and message (the title).
NotFoundError is the 404 case and PreconditionFailedError the 412 case
(precondition_failed, never retried).
Testing
python -m unittest -v tests/test_client.py
runs against an in-memory fake of the service. Set
TH_OBJECT_STORAGE_ENDPOINT, TH_ACCESS_KEY_ID and TH_SECRET_ACCESS_KEY
to also run a round trip against a real endpoint (it creates and deletes a
bucket).
Metadata
Release files for thailandhosting-object-storage 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| thailandhosting_object_storage-1.0.0.tar.gz | 24.9 kB | Without provenance | Details for thailandhosting_object_storage-1.0.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| thailandhosting_object_storage-1.0.0-py3-none-any.whl | Python 3 | none | any | Details for thailandhosting_object_storage-1.0.0-py3-none-any.whl |
Total release size: 43.0 kB
Release files / thailandhosting_object_storage-1.0.0.tar.gz
| Download URL | thailandhosting_object_storage-1.0.0.tar.gz |
|---|---|
| Size | 24.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f77c17f171eaebf5228e5349a4f3385940c924865c90d7a5c637073a1e119731
|
|
BLAKE2b-256 checksum How to use checksums |
b1f692ebdbb5a2b519f43e9abaccb4339888c5697b2b0681b4e99d07aeda3462
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / thailandhosting_object_storage-1.0.0-py3-none-any.whl
| Download URL | thailandhosting_object_storage-1.0.0-py3-none-any.whl |
|---|---|
| Size | 18.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
49efe75fb901d440669aa2e21fe6cfac026640b1cfa4aaade240984157c2278f
|
|
BLAKE2b-256 checksum How to use checksums |
58ed8e1cb044c6a68cf739df7bb36336dd06a1bb3dc3e8eec22f2447e91a243b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Security for thailandhosting-object-storage 1.0.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.