Skip to main content

Application-agnostic Holded API client for MisterPato projects.

Project description

misterpato-holded-client

Application-agnostic Python client package for Holded Invoice API v1 and v2.

The package exposes a small synchronous SDK around Holded's v1 and v2 Invoice API. It is intentionally independent of Django, Celery, databases, or any app-specific idempotency layer.

Installation

Install from a checkout in editable mode:

python3 -m pip install -e .

Install with test dependencies for development:

python3 -m pip install -e .[test]
python3 -m pytest -q

Runtime dependency: requests.

Supported Python: >=3.9.

Development environment

A local virtual environment can be created with:

python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -e .[dev]
python -m pytest -q

This checkout includes .python-version set to 3.10.12 for pyenv-compatible tooling. The package itself supports Python >=3.9.

Publishing

Build and upload instructions are documented in PUBLISHING.md. Short version:

source .venv/bin/activate
python -m pytest -q
rm -rf dist build *.egg-info src/*.egg-info
python -m build
python -m twine check dist/*
python -m twine upload dist/*

Do not commit real PyPI/TestPyPI tokens. .pypirc.example contains only placeholders.

Basic usage

Choose the Holded API version once at client construction. Resources are wired directly on the client instance for both versions.

v1

from misterpato_holded import HoldedClient, VERSION_1

client = HoldedClient(api_key="holded-api-key", version=VERSION_1)

HoldedClient wires v1 resources directly on the client instance:

  • Generic resources: client.documents, client.contacts, client.treasuries, client.payment_methods
  • Typed document resources: client.invoices, client.credit_notes, client.sales_receipts, client.sales_orders, client.proformas, client.waybills, client.estimates, client.purchases, client.purchase_orders, client.purchase_refunds

MVP document resources support the full essential document action set for every document type, especially client.invoices and client.credit_notes: list, iter_all, list_all, create, get, update, approve, pay, delete, get_pdf_base64, and get_pdf.

v2

from misterpato_holded import HoldedClient, VERSION_2

client = HoldedClient(api_key="holded-api-key", version=VERSION_2)

v2 currently wires only:

  • client.contacts
  • client.invoices
  • client.credit_notes
  • client.proformas
  • client.treasuries

Other v2 document types (purchases and so on) are not implemented yet. Use v1 if you need those resources today.

v2 invoice actions implemented today: list, iter_all, list_all, create, get, update, approve, bulk_approve, pay, delete, get_pdf, and get_pdf_base64.

v2 credit note actions implemented today: list, iter_all, list_all, create, get, update, approve, pay, delete, get_pdf, and get_pdf_base64.

v2 proforma actions implemented today: list, iter_all, list_all, create, get, update, approve, delete, get_pdf, and get_pdf_base64 (no pay — proformas have no payments in the Holded v2 API).

v2 contacts support list, iter_all, list_all, create, get, update, and delete.

v2 treasury account actions implemented today: list, iter_all, list_all, create, and get (Holded documents no update, delete, or archive endpoint for treasury accounts).

Configuration

client = HoldedClient(
    api_key="holded-api-key",
    version=VERSION_1,  # or VERSION_2
    base_url="https://api.holded.com/api",
    timeout=30,
    timezone="Europe/Madrid",  # v1 only; ignored for v2 wiring
)

Notes:

  • Blank API keys raise HoldedConfigError.
  • Unknown version values raise HoldedConfigError.
  • v1 auth: the client sends key: <api_key> automatically.
  • v2 auth: the client sends Authorization: Bearer <api_key> automatically and does not send the v1 key header.
  • URLs: both versions use the same default base_url (https://api.holded.com/api). v2 resource paths are /v2/... (for example /v2/invoices, /v2/contacts). You do not need a special v2 base_url for normal use.
  • v1 timezone: timezone controls how naive date and datetime values are serialized to Holded Unix timestamps. v2 date serialization uses ISO/date strings instead (see below).

Holded API v2

v2 is a separate surface under misterpato_holded.v2 with its own dataclasses, pagination, date formats, and invoice status vocabulary. v1 behavior is unchanged.

v2 models and .raw

Import v2 models from misterpato_holded.v2.models (or misterpato_holded.v2 for the public subset):

from misterpato_holded.v2.models import (
    CreateContactRequest,
    CreateInvoiceRequest,
    Invoice,
    InvoiceStatus,
    PayInvoiceRequest,
    UpdateContactRequest,
)

v2 response dataclasses are separate from v1 models. Every parsed response preserves the original decoded payload on .raw.

v2 invoice listing and InvoiceStatus

v2 invoice listing uses cursor pagination and a native status= filter. Supported values are validated locally before HTTP:

from misterpato_holded.v2.models import InvoiceStatus

pending = client.invoices.list(status=InvoiceStatus.PENDING)
# or: client.invoices.list(status="pending")

Allowed status values: pending, completed, partial, cancelled, failed, overdue.

v2 does not support v1's boolean paid= filter. Passing paid= raises TypeError because the argument is not part of the v2 API. Use explicit status= values instead.

v2 list methods also do not accept v1's page= argument. Use cursor= on list() or iter_all() / list_all() for multi-page reads.

v2 invoice creation

Create with an existing contact id:

from datetime import date

created = client.invoices.create({
    "contact_id": "contact-id",
    "date": date(2026, 6, 29),
    "items": [{"name": "Service", "units": 1, "subtotal": "120.00"}],
})

print(created.id, created.raw)

Or supply contact creation fields when you do not have a contact_id. The client creates the contact first, then creates the invoice with the returned id. It does not search existing contacts first:

created = client.invoices.create({
    "contactName": "Cliente Demo",
    "contactEmail": "cliente@example.com",
    "date": "2026-06-29",
    "items": [{"name": "Service", "units": 1, "subtotal": "120.00"}],
})

Recognized contact auto-creation aliases: contactName / contact_name and contactEmail / contact_email. The caller payload is copied internally; your original mapping is not mutated.

Contact auto-creation requires both contact-write and invoice-write permissions on the Holded API key. If contact creation succeeds but invoice creation fails, the new contact remains in Holded; callers that need idempotency must handle that at the application layer.

v2 invoice create does not emulate v1's approve=True. Passing approve=True raises HoldedRequestValidationError locally with no HTTP request. Approve invoices explicitly after creation:

created = client.invoices.create({...})
approved = client.invoices.approve(created.id)

Create responses usually return only {id}; call get() when you need the full invoice body.

v2 invoice actions

invoice = client.invoices.get("invoice-id")
updated = client.invoices.update("invoice-id", {"notes": "Updated notes"})
approved = client.invoices.approve("invoice-id")
paid = client.invoices.pay("invoice-id", {"date": "2026-06-29", "amount": "120.00"})
deleted = client.invoices.delete("invoice-id")
pdf_bytes = client.invoices.get_pdf("invoice-id")
pdf_base64 = client.invoices.get_pdf_base64("invoice-id")

update() is documented by Holded as full replacement. Send a complete payload or risk clearing omitted fields.

get_pdf() requests the binary PDF endpoint and returns raw bytes. get_pdf_base64() base64-encodes those bytes locally for compatibility with v1-style callers.

delete() handles Holded's 204 no-content response.

v2 invoice bulk approve

Known Holded-side issue (checked 2026-07-02): the live /v2/invoices/bulk/approve endpoint currently returns 422 {"message": "None of the invoices could be approved"} for every request, including freshly created, individually approvable draft invoices on an account with Verifactu disabled — reproduced with this client, with raw requests, and with curl. Until Holded fixes the endpoint, bulk_approve() will raise HoldedValidationError; approve invoices one by one with approve() instead. The method is kept because it matches the documented API contract and needs no client change once Holded resolves it.

bulk_approve() posts {"ids": [...]} to /v2/invoices/bulk/approve. Holded processes each invoice independently:

result = client.invoices.bulk_approve(["invoice-id-1", "invoice-id-2"])
print(result.status_code, result.message, result.raw)
  • 204: all invoices approved; message is empty and raw is {}.
  • 207: some invoices could not be approved; message carries Holded's explanation and raw preserves the response body.
  • 422 (none approved) and other errors raise the usual typed exceptions (HoldedValidationError, ...).

An empty sequence or blank ids raise HoldedRequestValidationError locally with no HTTP request.

v2 credit notes

client.credit_notes targets /v2/credit-notes (Holded's facturas rectificativas) with the same action set as invoices except bulk_approve:

created = client.credit_notes.create({
    "contact_id": "contact-id",
    "date": date(2026, 6, 29),
    "items": [{"name": "Refund", "units": 1, "price": "10.00"}],
})

credit_note = client.credit_notes.get(created.id)
approved = client.credit_notes.approve(created.id)
paid = client.credit_notes.pay(created.id, {"amount": "10.00", "date": "2026-06-29"})
pdf_bytes = client.credit_notes.get_pdf(created.id)
deleted = client.credit_notes.delete(created.id)

Differences from invoices:

  • No contact auto-creation. create() requires an existing contact_id; contactName / contactEmail aliases are not recognized, and a payload without contact_id raises HoldedRequestValidationError locally with no HTTP request.
  • Mapping payloads only. There are no typed request dataclasses for credit notes; pass plain mappings. Dates and money fields are serialized with the same ISO/Decimal rules as invoices.
  • List filters are limit, cursor, contact_id, status, start_date, end_date, sort, and approval_status. The invoice-only due_date_start, due_date_end, and accounting_account_num filters are not accepted (TypeError). Status values reuse the invoice vocabulary (pending, completed, partial, cancelled, failed, overdue).

Responses parse into CreditNote and the generic CreateDocumentResponse / UpdateDocumentResponse / ApproveDocumentResponse / PayDocumentResponse / DeleteDocumentResponse dataclasses, all preserving .raw.

v2 proformas

client.proformas targets /v2/proformas with the same action set as credit notes minus pay — proformas carry no payment fields in the Holded v2 API, so there is no pay method at all:

created = client.proformas.create({
    "contact_id": "contact-id",
    "date": date(2026, 6, 29),
    "items": [{"name": "Line", "units": 1, "price": "10.00"}],
})

proforma = client.proformas.get(created.id)
approved = client.proformas.approve(created.id)
pdf_bytes = client.proformas.get_pdf(created.id)
deleted = client.proformas.delete(created.id)

Proforma create() supports the same contact auto-creation as invoices: when the payload has no contact_id but includes contactName / contact_name (optionally contactEmail / contact_email), the contact is created first via POST /v2/contacts, the returned id is used as contact_id, and the alias keys are stripped from the proforma payload. There is no contact search or deduplication — an existing contact with the same name is not reused. Without contact_id and without alias fields, create() raises HoldedRequestValidationError locally with no HTTP request:

created = client.proformas.create({
    "contactName": "New Client SL",
    "contactEmail": "billing@newclient.example",
    "items": [{"name": "Line", "units": 1, "price": "10.00"}],
})

List filters are limit, cursor, contact_id, status, start_date, end_date, and sort — no approval_status and no due-date filters (TypeError). Payloads are plain mappings, serialized with the same ISO/Decimal rules as invoices. Responses parse into Proforma (no payment fields) and the same generic document response dataclasses, all preserving .raw.

v2 treasuries

client.treasuries targets /v2/treasury/accounts. Holded documents only create, list, and get — there is no update, delete, or archive endpoint, so the resource has no such methods:

created = client.treasuries.create({"name": "Main bank", "type": "bank", "currency": "EUR"})

account = client.treasuries.get(created.id)
print(account.balance)  # decimal string, e.g. "1250.55" — parse with Decimal in your app

banks = client.treasuries.list(type="bank", archived=False)
for account in client.treasuries.iter_all():
    print(account.id, account.name, account.type)

create() validates locally (no HTTP) that name is present and type is one of bank, card, gateway, or cash. List filters are cursor, limit, type (same allowed values), and archivedarchived=True returns only archived accounts, archived=False only active ones (sent as lowercase true/false on the wire), and omitting it returns both. Accounts parse into Treasury with balance kept as a decimal string (no float conversion) and .raw preserved; create() returns the generic CreateDocumentResponse.

v2 contacts

from misterpato_holded.v2.models import CreateContactRequest, UpdateContactRequest

contact = client.contacts.create(CreateContactRequest(
    name="Cliente Demo",
    email="cliente@example.com",
))

same_contact = client.contacts.get(contact.id)
updated = client.contacts.update(contact.id, UpdateContactRequest(name="Cliente Demo", email="cliente@example.com"))
deleted = client.contacts.delete(contact.id)

Raw mapping payloads are also supported. Contact update() is full replacement, same caveat as invoices.

Contact list() parses the v2 cursor envelope (items, has_more, cursor). iter_all() / list_all() follow cursors with the same semantics as invoices, preserving filters on every page:

for contact in client.contacts.iter_all(limit=100):
    print(contact.id, contact.name)

all_matching = client.contacts.list_all(email="cliente@example.com", max_pages=20)

v2 pagination

v2 invoice pagination is cursor-based, not page-number based:

first_page = client.invoices.list(status="pending", limit=50)

for invoice in client.invoices.iter_all(status="pending", start_date="2026-01-01"):
    print(invoice.id, invoice.document_number, invoice.raw)

all_pending = client.invoices.list_all(status="pending", max_pages=20)

iter_all() follows cursor values until has_more is false. Filters passed to the first call are preserved on later cursor requests. If max_pages is reached before pagination finishes, HoldedPaginationLimitError is raised instead of silently returning partial data.

For a single page, pass cursor= to list() directly.

v2 date and money serialization

v2 sends ISO date strings (for example 2026-06-29) rather than v1 Unix timestamp strings. List date filters reject Unix timestamp ints or numeric strings.

Money values still use Decimal(str(amount)), are quantized to 0.01 with ROUND_HALF_UP, then sent as JSON numbers.

client.invoices.create({
    "contact_id": "contact-id",
    "date": date(2026, 6, 29),
    "items": [{"name": "Service", "units": 1, "subtotal": "10.235"}],  # sends 10.24
})

Migrating from v1 to v2

Topic v1 v2
Client init version=VERSION_1 version=VERSION_2
Auth header key: <api_key> Authorization: Bearer <api_key>
Resources documents, contacts, treasuries, payment methods, all document types contacts, invoices, credit notes, proformas, and treasuries only
Models misterpato_holded.v1.models misterpato_holded.v2.models
Invoice paid filter paid=True/False not supported; use status=
Pagination page=N, empty page stops cursor, has_more envelope
Dates on wire Unix timestamp strings ISO/date strings
Create + approve create(..., approve=True) create, then explicit approve()
Contact on create inline contact_name on document payload contact_id or auto-create via contact fields
PDF base64 JSON field and/or binary helper binary PDF bytes; base64 helper encodes locally

Webhooks

misterpato_holded.webhooks provides framework-agnostic helpers for receiving Holded webhooks: HMAC-SHA256 signature verification, header parsing, typed payload dataclasses, and event-name constants. There is no HTTP server, routing, or persistence — plug the helpers into whatever framework the app uses.

from misterpato_holded import webhooks
from misterpato_holded.exceptions import HoldedWebhookSignatureError

def receive(raw_body: bytes, request_headers: dict, secret: str):
    try:
        event = webhooks.parse_event(raw_body, request_headers, secret)
    except HoldedWebhookSignatureError:
        return 401  # reject: missing or invalid signature

    if event.event in webhooks.DOCUMENT_EVENTS:
        handle_document(event.payload, dedupe_key=event.idempotency_key)
    elif event.event in webhooks.CONTACT_EVENTS:
        handle_contact(event.payload, dedupe_key=event.idempotency_key)
    # unknown events arrive with event.payload = None; ignore or log them
    return 204

Notes:

  • parse_event verifies the signature against the exact raw body bytes before parsing anything, and raises HoldedWebhookSignatureError on failure. Always pass the raw bytes as received, never re-serialized JSON.
  • Header lookup is case-insensitive; all x-holded-webhook-* metadata is exposed on the returned HoldedWebhookEvent (event, webhook_id, account_id, delivered_at, webhook_version).
  • Delivery is at-least-once: deduplicate on event.idempotency_key ((account_id, webhook_id)). Dedup storage, queuing, and retry handling are the consuming app's responsibility.
  • Payload dataclasses (WebhookDocumentPayload, WebhookDeletedDocumentPayload, WebhookContactPayload, WebhookDeletedContactPayload) keep nullable fields as None, keep total/subtotal as strings (parse with Decimal), and preserve the original payload on .raw.
  • webhooks.verify_signature(raw_body, signature_header, secret) is available standalone when you only need the boolean check.

The full local reference for events, headers, payload schemas, and receiver guidance is docs/holded-v2-documentation/webhooks.md.

Documents (v1)

Create an invoice with the typed resource:

from datetime import date
from misterpato_holded import HoldedClient, VERSION_1
from misterpato_holded.v1.models import CreateDocumentRequest, DocumentItem

client = HoldedClient(api_key="holded-api-key", version=VERSION_1)

invoice = client.invoices.create(CreateDocumentRequest(
    date=date(2026, 1, 15),
    contact_name="Cliente Demo",
    currency="EUR",
    items=[DocumentItem(name="Service", units=1, subtotal="120.00")],
))

print(invoice.id, invoice.doc_number, invoice.raw)

The same operation can be done through the generic documents resource:

from misterpato_holded.v1.documents import DocumentType

invoice = client.documents.create(
    DocumentType.INVOICE,
    CreateDocumentRequest(date=date.today(), contact_name="Cliente Demo"),
)

Pass approve=True to approve a document during creation, which sends Holded's approveDoc: true field. Pass custom_fields=[...] to send Holded's customFields array:

custom_fields = [{"field": "project", "value": "MisterPato"}]

proforma = client.proformas.create(
    CreateDocumentRequest(date=date.today(), contact_name="Cliente Demo"),
    approve=True,
    custom_fields=custom_fields,
)

Other document operations:

document = client.invoices.get("document-id")
updated = client.invoices.update("document-id", {"notes": "Updated notes"})
paid = client.invoices.pay("document-id", {"date": date.today(), "amount": "120.00"})
approved = client.invoices.approve("document-id")
pdf_base64 = client.invoices.get_pdf_base64("document-id")
pdf_bytes = client.invoices.get_pdf("document-id")
deleted = client.invoices.delete("document-id")

Raw mappings are accepted for pragmatic use. Known money and timestamp fields are serialized before sending, and locally known required fields are validated before the HTTP request.

Contacts (v1)

from misterpato_holded.v1.models import CreateContactRequest, UpdateContactRequest

contact = client.contacts.create(CreateContactRequest(
    name="Cliente Demo",
    email="cliente@example.com",
))

same_contact = client.contacts.get(contact.id)
updated_contact = client.contacts.update(contact.id, UpdateContactRequest(phone="+34 600 000 000"))
deleted = client.contacts.delete(contact.id)

Raw mapping payloads are also supported:

contact = client.contacts.create({"name": "Cliente Demo", "email": "cliente@example.com"})

Treasuries and payment methods (v1)

These resources are list-only in this v1 package:

treasuries = client.treasuries.list()
payment_methods = client.payment_methods.list()

Pagination (v1)

List resources support one-page list(page=N), lazy iter_all(...), and materialized list_all(...).

page_2 = client.invoices.list(page=2)

for invoice in client.invoices.iter_all(starttmp=date(2026, 1, 1), endtmp=date(2026, 1, 31)):
    print(invoice.id, invoice.doc_number)

all_contacts = client.contacts.list_all(max_pages=20)

iter_all() starts at page 1 by default and stops when Holded returns an empty list. If max_pages is reached before an empty page is observed, HoldedPaginationLimitError is raised instead of silently returning partial data.

Date and datetime serialization (v1)

Holded v1 expects string Unix timestamps for document/payment dates and starttmp/endtmp filters. This package accepts int, str, date, and datetime values and emits canonical decimal strings on the wire.

Rules:

  • Default timezone for naive date/datetime: Europe/Madrid.
  • Configure it with HoldedClient(..., timezone="UTC") or another IANA timezone name.
  • starttmp=date(...) uses local 00:00:00.
  • endtmp=date(...) uses local 23:59:59.
  • Naive datetime is interpreted in the client timezone.
  • Aware datetime preserves its instant.
from datetime import date, datetime, timezone

client = HoldedClient(api_key="holded-api-key", timezone="Europe/Madrid")

client.invoices.list(starttmp=date(2026, 1, 1), endtmp=date(2026, 1, 31))
client.invoices.create(CreateDocumentRequest(date=datetime(2026, 1, 15, 10, 30), contact_name="Demo"))
client.invoices.pay("document-id", {"date": datetime.now(timezone.utc), "amount": "120.00"})

Money serialization

Money values use Decimal(str(amount)), are quantized to 0.01 with ROUND_HALF_UP, then sent as JSON numbers.

DocumentItem(name="Service", units=1, subtotal="10.235")  # sends 10.24

Error handling

All package exceptions inherit from HoldedError.

from misterpato_holded.exceptions import (
    HoldedAuthError,
    HoldedConfigError,
    HoldedError,
    HoldedNotFoundError,
    HoldedPaginationLimitError,
    HoldedRequestValidationError,
    HoldedServerError,
    HoldedValidationError,
)

try:
    invoice = client.invoices.create(CreateDocumentRequest(contact_name="Missing date"))
except HoldedRequestValidationError:
    # Local validation failed before any HTTP request was sent.
    raise
except HoldedValidationError as exc:
    # Holded returned a remote 400/422 validation error.
    print(exc.status_code, exc.response_json)
except HoldedAuthError:
    print("Invalid or unauthorized Holded API key")
except HoldedNotFoundError:
    print("Document/contact not found")
except HoldedServerError as exc:
    if exc.retryable:
        print("Holded server error; safe for caller-controlled retry policy")
except HoldedPaginationLimitError:
    print("Pagination reached max_pages before an empty page")
except HoldedConfigError:
    print("Invalid local client configuration")
except HoldedError as exc:
    print(f"Holded client failure: {exc}")

Status mapping:

  • 400 and 422: HoldedValidationError
  • 401 and 403: HoldedAuthError
  • 404: HoldedNotFoundError
  • 409: HoldedConflictError
  • 5xx: HoldedServerError with retryable=True
  • Invalid successful JSON or unexpected response shape: HoldedResponseError

Live v2 tests

The live suite under tests/live/ hits the real Holded v2 API at api.holded.com. It is opt-in only: default python -m pytest -q does not collect or run these tests.

Only human developers run the live suite (unless he explicitly delegates otherwise). Agents diagnose from shared logs and verify with the mocked suite only (python -m pytest -q).

export HOLDED_V2_API_KEY="your-v2-api-key"
pytest tests/live > live.log 2>&1

If a live run fails, share live.log with an agent — never the API key. The agent edits code/tests and re-verifies with python -m pytest -q; the human developer re-runs pytest tests/live.

Without HOLDED_V2_API_KEY exported, pytest tests/live skips every test with a clear reason and makes no network calls.

Cleanup caveat: an interrupted run (Ctrl-C, crash) may leave live-test- contacts or invoices in the Holded account. Identify them by the live-test- prefix and delete manually in the Holded UI.

Residual risk: invoice lifecycle tests that call pay() may leave a real payment record on the account even after the throwaway invoice is deleted.

Development verification

python3 -m pytest -q
python3 -m compileall -q src tests
python3 - <<'PY'
from misterpato_holded import HoldedClient, VERSION_1, VERSION_2

v1 = HoldedClient(api_key="x", version=VERSION_1)
print(v1.version, v1.invoices.doc_type.value)

v2 = HoldedClient(api_key="x", version=VERSION_2)
print(v2.version, type(v2.invoices).__name__, type(v2.contacts).__name__)
PY

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

misterpato_holded_client-0.2.3.tar.gz (44.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

misterpato_holded_client-0.2.3-py3-none-any.whl (40.4 kB view details)

Uploaded Python 3

File details

Details for the file misterpato_holded_client-0.2.3.tar.gz.

File metadata

  • Download URL: misterpato_holded_client-0.2.3.tar.gz
  • Upload date:
  • Size: 44.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.12

File hashes

Hashes for misterpato_holded_client-0.2.3.tar.gz
Algorithm Hash digest
SHA256 55d8b1b675a1d586e52a346ace9477f6f50a9974179eb8644986bf77c662167c
MD5 90d7d5141c271b075a14af0e7f7a1fdc
BLAKE2b-256 275e7a13520420b8c65529be29d43b708be1215b97a31abe3bd043440a177c96

See more details on using hashes here.

File details

Details for the file misterpato_holded_client-0.2.3-py3-none-any.whl.

File metadata

File hashes

Hashes for misterpato_holded_client-0.2.3-py3-none-any.whl
Algorithm Hash digest
SHA256 4cc1c94671d83e80002d0d660404f2a6189b35fff140343ee98130b7a2cb8350
MD5 3eabbfc15669f986872236d5b20daa3d
BLAKE2b-256 4230ede959c5d69c77623613b1f574d3fec2870a7985ebfd1ecbb8f0e8aa89b0

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page