Khipu Python SDK
Community-maintained Python SDK for the Khipu API. This project is independently maintained and is not affiliated with, endorsed by, or maintained by Khipu. "Khipu" refers to the payment service whose public API this SDK calls. For questions about your Khipu account, contract or API credentials, contact Khipu directly; for problems with this SDK, use this repository's issues.
High-level Python SDK for integrating with Khipu Instant Payments (v3) and Khipu Automatic Payments / PAC (v1), generated from the OpenAPI specifications Khipu publishes on its documentation site.
The SDK is a thin, typed boundary around what Khipu actually publishes. It validates money before anything is serialised, maps every failure onto a precise exception, and promotes the fields you need to verify a payment onto stable result types. It deliberately does not paper over the guarantees Khipu does not offer — see Idempotency.
Key principles
- All hand-written source code is in English.
- Never edit generated code under
generated/. - Regeneration is repeatable and automated (
make generate-all). - Your application imports only
khipu_sdk, nevergenerated.*. - Money is
Decimalend to end;floatappears only at the final wire boundary. - No retries anywhere. Retry policy belongs to the caller.
Repository structure
khipu-python-sdk/
openapi/
khipu-instant-payments.yaml
khipu-automatic-payments.yaml
generated/
__init__.py # makes generated/ a real package so it ships in the wheel
instant_payments/ # OpenAPI generated client (do not edit)
automatic_payments/ # OpenAPI generated client (do not edit)
khipu_sdk/
client.py # KhipuClient (high-level wrapper)
config.py # KhipuConfig
errors.py # SDK exception taxonomy
money.py # Decimal handling + currency minor-unit validation
types.py # stable result dataclasses
py.typed # PEP 561 marker
tools/
check_dist.py # wheel/sdist content and metadata check (pre-publish gate)
smoke_installed.py # smoke test run against the *installed* wheel or sdist
tests/
unit/
integration/
Makefile
pyproject.toml
Khipu API documentation
Khipu's own API documentation: https://docs.khipu.com/
This repository uses the OpenAPI specifications Khipu publishes there,
downloaded directly from its documentation site and pinned by sha256 in
OPENAPI_SPECS.lock.json. It is not the source of truth for Khipu's API; when
this README and Khipu's documentation disagree, Khipu's documentation wins.
Requirements
- Python 3.10+
- (for regeneration only)
openapi-python-client
Runtime dependencies are httpx, attrs and python-dateutil; they are
installed automatically.
Installation
Pre-release status.
0.3.3is the next planned release and the first planned public PyPI release. It is not yet published: it is on neither PyPI nor TestPyPI, and tagv0.3.3does not yet exist. The latest existing tag isv0.3.2. The PyPI and Git install commands below are for use after 0.3.3 is released; until then they fail.
From PyPI (after 0.3.3 is released)
The SDK will be published to PyPI as
khipu-python-sdk, starting with
0.3.3. The import name is khipu_sdk. After 0.3.3 is released:
pip install "khipu-python-sdk==0.3.3"
In requirements.txt:
khipu-python-sdk==0.3.3
Or in pyproject.toml:
[project]
dependencies = [
"khipu-python-sdk==0.3.3",
]
Pin an exact version so an upstream release cannot silently alter payment behaviour in your application, and read the changelog before moving the pin.
From source with Git (after 0.3.3 is released)
Each PyPI release corresponds to a vX.Y.Z tag in this repository. The wheel
built from a tag contains both khipu_sdk and the generated clients, so a Git
install is equally functional. After tag v0.3.3 is created by the release:
khipu-python-sdk @ git+https://github.com/yaselc/khipu-python-sdk.git@v0.3.3
Pin a tag rather than @master: master is not a release.
From a local checkout (SDK development)
python3 -m venv .venv
source .venv/bin/activate
make install-dev
Verifying an install
make smoke-dist builds the wheel and sdist, checks their contents with
tools/check_dist.py, installs each into a throwaway virtualenv and creates and
retrieves a payment against a local mock server. It fails if a distribution is
missing the generated clients or ships anything outside its allowlist. CI runs
it on every push.
Configuration
Environment variables:
KHIPU_API_KEY(required)KHIPU_BASE_URL(optional, defaulthttps://payment-api.khipu.com)KHIPU_TIMEOUT(optional, default10.0seconds)
from khipu_sdk import KhipuClient, KhipuConfig
client = KhipuClient(KhipuConfig.from_env())
Or explicitly, which is what you want when the values come from your framework's settings rather than the process environment:
import httpx
from khipu_sdk import KhipuClient, KhipuConfig
client = KhipuClient(
KhipuConfig(
api_key=settings.KHIPU_API_KEY,
base_url=settings.KHIPU_BASE_URL,
# A float applies to connect/read/write/pool alike. Pass an
# httpx.Timeout to control the phases independently.
timeout=httpx.Timeout(connect=3.0, read=10.0, write=10.0, pool=3.0),
)
)
KhipuConfig.api_key is excluded from repr(), so the credential cannot leak
through logging, Sentry local-variable capture or a debug page. The timeout is
never None: an unbounded default would let a stalled request pin a worker.
Money
Decimal is the canonical representation. The SDK performs no financial
arithmetic in binary floating point.
from decimal import Decimal
client.create_instant_payment(amount=Decimal("10000"), currency="CLP", ...)
amount accepts Decimal, int, str or float. float is converted through
Decimal(str(value)) so the SDK adds no floating-point noise of its own, but any
imprecision already present in your float survives — pass Decimal when
exactness matters.
Amounts returned by get_instant_payment() are Decimal, so you never have to
compare a Decimal against a float.
Currency minor units
Every amount is validated against the currency's ISO-4217 minor-unit scale before serialisation, and amounts are never silently rounded:
| Currency | Decimals | Currency | Decimals | |
|---|---|---|---|---|
| CLP | 0 | EUR | 2 | |
| CLF | 4 | MXN | 2 | |
| ARS | 2 | PEN | 2 | |
| BOB | 2 | USD | 2 | |
| COP | 2 |
client.create_instant_payment(amount=Decimal("10000"), currency="CLP", ...) # ok
client.create_instant_payment(amount=Decimal("10000.0000"), currency="CLP", ...) # ok
client.create_instant_payment(amount=Decimal("9700.5"), currency="CLP", ...) # KhipuValidationError
A rejected amount raises KhipuValidationError and no HTTP request is made.
Errors
All exceptions derive from KhipuError and are picklable, so they survive Celery
result backends.
| Exception | Meaning | Reached Khipu? |
|---|---|---|
KhipuConfigurationError |
Generated clients missing / SDK unusable as configured | No |
KhipuValidationError |
Invalid input rejected locally (currency, fractional CLP, unserialisable payload) | No |
KhipuTimeoutError |
Request timed out | Unknown — see below |
KhipuConnectionError |
DNS / TCP / TLS / broken stream | Usually no; check the flag |
KhipuApiError |
Khipu answered with a non-success status | Yes |
KhipuResponseError |
Success status, but the body was malformed or incomplete | Yes |
KhipuTimeoutError and KhipuConnectionError share a base,
KhipuTransportError, and carry:
except KhipuTransportError as exc:
if exc.request_may_have_been_delivered:
... # ambiguous: Khipu may have processed the request
else:
... # the connection was never established; nothing reached Khipu
That flag is derived from httpx's own exception taxonomy. It is False only
when the transport can prove the connection was never established. The SDK never
claims certainty httpx does not provide.
KhipuApiError preserves Khipu's diagnostics instead of discarding them:
except KhipuApiError as exc:
exc.status_code # 400
exc.message # "Monto inválido" (derived from Khipu's body)
exc.payload # {"message": "Monto inválido", "code": 42} (parsed JSON, or None)
exc.body # bounded raw body text
Bodies are truncated before they reach a message, and request headers are never touched, so the API key cannot leak through an exception.
Idempotency (read this before you retry)
create_instant_paymentis NOT idempotent.
- Khipu publishes no idempotency key for payment creation.
transaction_idis a merchant correlation field. Khipu documents no deduplication on it. Do not treat it as a uniqueness guarantee.- Khipu publishes no endpoint to look a payment up by
transaction_id—get_instant_payment()takes Khipu'spayment_idonly.- The SDK performs no automatic retries and keeps no duplicate-request cache.
A
KhipuTimeoutErroron create-payment is an ambiguous outcome: Khipu may have created a live, payable payment. Blindly retrying can produce a second live payment for the same order.
What a consuming application should do:
- Persist a payment-intent row (your reference, amount, currency, state) and
commit it before calling
create_instant_payment. - Enforce uniqueness on your own reference locally — Khipu will not.
- On an ambiguous failure, mark the intent unknown, not failed, and reconcile. Do not auto-retry.
- If you must create again after an ambiguous attempt, use a new
transaction_idso both attempts stay distinguishable. - Before recognising money, re-fetch with
get_instant_payment()and verifytransaction_id,amount,currency,statusandstatus_detailagainst your own record. Do not trust a notification body alone.
Correlation
Two opaque merchant fields are sent on creation and echoed back by
get_instant_payment():
transaction_id— your own reference (invoice, order or contribution id).custom— free-form extra payload.
created = client.create_instant_payment(
amount=Decimal("10000"),
currency="CLP",
subject="Order 123",
transaction_id="ORDER-123",
custom="ORDER-123-extra",
return_url="https://your-app.example.com/payments/return/",
cancel_url="https://your-app.example.com/payments/cancel/",
notify_url="https://your-app.example.com/payments/webhook/khipu/",
)
fetched = client.get_instant_payment(payment_id=created.payment_id)
assert fetched.transaction_id == "ORDER-123"
Again: correlation, not idempotency.
Usage
Instant Payments (v3)
from decimal import Decimal
from khipu_sdk import KhipuClient, KhipuConfig
client = KhipuClient(KhipuConfig.from_env())
created = client.create_instant_payment(
amount=Decimal("10000"),
currency="CLP",
subject="Test payment",
transaction_id="ORDER-123",
body="Order #123",
return_url="https://your-app.example.com/payments/return/",
cancel_url="https://your-app.example.com/payments/cancel/",
notify_url="https://your-app.example.com/payments/webhook/khipu/",
)
create_instant_payment returns InstantPaymentCreateResult with exactly what
Khipu's create response contains — no more:
| Field | Notes |
|---|---|
payment_id |
12-char Khipu identifier |
payment_url |
main redirect URL |
simplified_transfer_url |
|
transfer_url |
|
app_url |
Khipu mobile app deep link |
ready_for_terminal |
|
raw |
escape hatch; excluded from repr |
Khipu's create response carries no status, amount, currency, expiry or merchant reference. Those come from a follow-up fetch:
payment = client.get_instant_payment(payment_id=created.payment_id)
InstantPaymentStatus promotes every field needed for verification, so you
never need to touch raw:
| Field | Type |
|---|---|
payment_id |
str |
transaction_id |
Optional[str] |
custom |
Optional[str] |
status |
Optional[str] — pending, verifying, done |
status_detail |
Optional[str] — see the table below |
amount |
Optional[Decimal] — Khipu publishes it as a decimal string; the facade converts |
currency |
Optional[str] |
receiver_id |
Optional[int] |
notification_token |
Optional[str] |
expires_date |
Optional[datetime] |
conciliation_date |
Optional[datetime] |
out_of_date_conciliation |
Optional[bool] |
raw |
escape hatch; excluded from repr |
raw is kept out of repr on purpose: the underlying Khipu model carries payer
PII (name, email, personal identifier, bank account number).
status_detail vocabulary
The values Khipu currently publishes, with its own semantics:
| Value | Khipu's meaning |
|---|---|
pending |
the payer has not started paying |
normal |
the payment was verified and settled through a standard payment method |
marked-paid-by-receiver |
the collector marked the charge as paid by other means |
rejected-by-payer |
the payer declared they will not pay |
marked-as-abuse |
the payer declared they will not pay and that the charge was unsolicited |
reversed |
the payment was voided by the merchant and the amount was marked for return to the payer |
partially-refunded |
part of the amount was marked for return to the payer through partial refunds |
fully-refunded |
the full amount was marked for return to the payer through one or more refunds |
Two points of precision:
- Khipu's wording for the last three is marked for return to the payer, not
returned.
status_detaildoes not tell you the money moved. status == "done"is not final forever. A payment that readsdone/normaltoday can later readdone/partially-refundedordone/fully-refunded, because a payment can be reversed after it settles. Treatingdoneas a terminal state is unsafe.
Whether a refund actually paid out
Khipu's 2026-09 revision added three optional fields to the payment body that
answer the settlement question status_detail cannot:
| Field | Meaning |
|---|---|
refunds[] |
each refund on the payment, with status of pending, authorized, paid or expired, plus request_date / authorization_date / payout_date / expiration_date |
total_refunded |
total returned to the payer. Expired refunds are not counted. |
void_payout_date |
when a voided payment's funds actually reached the payer |
A refund that reads expired was never confirmed within 15 business days and
will not be paid. So a payment can read status_detail: partially-refunded
while some of that amount never left — which is exactly why
total_refunded and refunds[].status exist.
These three are optional, and the facade does not promote them onto
InstantPaymentStatus yet: refunds is a list of objects and deserves a
stable result type rather than leaking generated models. Read them through
raw for now:
status = client.get_instant_payment(payment_id=payment_id)
# Khipu omits these fields entirely when a payment has no refunds, so the
# generated model reports them as UNSET rather than as a value.
refunds = getattr(status.raw, "refunds", None)
if isinstance(refunds, list):
paid_out = [r for r in refunds if r.status.value == "paid"]
status == "done" alone is therefore not sufficient to recognise money. The SDK
reports provider state verbatim and makes no business decision about it:
deciding what a refund or reversal means for your own records is the consuming
application's job.
This vocabulary is Khipu's and can grow. The facade passes status_detail
through as a plain string rather than an SDK enum, so a value Khipu adds later
reaches you unchanged instead of raising.
payment_method and PAC charges
A PAC charge (create_charge) is itself a Khipu payment, read back with
get_instant_payment(payment_id=...). For those, the live API returns
payment_method: "direct_debit", a value Khipu's published Instant Payments
document does not declare: it lists only regular_transfer,
simplified_transfer and not_available. A client generated verbatim from that
document could not read a PAC payment at all
(#15).
The SDK therefore carries one deliberate, narrow compatibility allowance:
direct_debit is accepted for payment_method on GET /v3/payments/{id} and
nowhere else. payment_method is not promoted onto InstantPaymentStatus; it is
reachable through raw as the typed PaymentGetPaymentPaymentMethod enum, which
now includes DIRECT_DEBIT. Any other undeclared payment_method value, and
any undeclared value in status, status_detail or funds_source, still
raises KhipuResponseError, because that is provider-contract drift someone
should look at. See Compatibility overlay for how
the allowance is built and when it goes away.
Automatic Payments (PAC)
subscription = client.create_subscription(
name="Jane Doe",
email="jane.doe@example.com",
max_amount=Decimal("50000"), # monthly cap
max_amount_per_charge=Decimal("20000"), # per-charge cap; required by Khipu
currency="CLP",
notify_url="https://your-app.example.com/pac/notify/",
return_url="https://your-app.example.com/pac/return/",
cancel_url="https://your-app.example.com/pac/cancel/",
)
status = client.get_subscription(subscription_id=subscription.subscription_id)
charge = client.create_charge(
subscription_id=subscription.subscription_id,
amount=Decimal("20000"),
subject="Monthly fee",
# Required by Khipu's ChargeRequest schema:
error_response_url="https://your-app.example.com/pac/error/",
notify_url="https://your-app.example.com/pac/notify/",
# Optional in Khipu's schema; omitted fields are left out of the request:
body="Subscription monthly charge",
transaction_id="INVOICE-2026-0001",
custom="INVOICE-2026-0001",
# Optional; selects Khipu's notification contract. No SDK default:
notify_api_version="3.0",
# Optional, local validation only; never sent to Khipu:
currency="CLP",
)
The charge payload carries no currency — it is inherited from the subscription —
so pass currency if you want minor-unit validation applied to amount.
body, custom and transaction_id are optional because Khipu's schema makes
them optional. Omitting one leaves the field out of the request entirely: the
SDK never sends null and never invents a value. They are still the only
merchant-side fields Khipu echoes back, so if you reconcile charges against your
own records, require them in your service layer — that is your policy to
enforce, not one the SDK imposes on every consumer.
notify_api_version selects the notification contract Khipu uses when it
calls notify_url for this charge. It is optional, as in Khipu's schema, and
the SDK does not choose a default: a value is sent exactly as given, and
omitting it (or passing None) leaves the field out of the request, so Khipu
applies whatever it does for an unspecified version. Khipu's schema types it as
a free string, so the SDK does not restrict the value either. If your
application relies on signed JSON notifications (Khipu's notification API
3.0), request "3.0" explicitly rather than depending on the unspecified
behaviour, and verify the callbacks your own merchant account receives.
transaction_id is not an idempotency key. Khipu's ChargeError vocabulary
mentions IDEMPOTENCY_KEY_REUSED, but the published ChargeRequest defines no
idempotency field or header, so the SDK sends none and treats no field as one.
The no-retry policy described above is unchanged.
Subscription ids are opaque
subscription_id is an opaque provider string. Khipu's schema example shows
a UUID, but the live API issues short mixed-case alphanumeric tokens such as
7Kp2R9xTmQ4b. The SDK imposes no syntax on it, and neither should you: store
it as text, and never validate, normalise or parse it as a UUID.
Subscription lifecycle
get_subscription returns Khipu's own status as a plain string:
status |
Meaning |
|---|---|
PENDING |
Created; the customer has not signed the bank mandate yet. |
SIGNED |
The mandate was signed. |
ENABLED |
The mandate is active. Only this state permits a charge. |
DISABLED |
The mandate is not usable. |
DISCARDED |
The subscription was unsubscribed. Not reversible. |
status = client.get_subscription(subscription_id=subscription.subscription_id)
if status.status == "ENABLED":
... # safe to charge
Two things to know about this endpoint:
service_referencecomes back empty until the customer signs. Khipu returns"", notnulland not a missing key. The SDK passes that through unchanged:""is a value Khipu sent, and is never rewritten toNone.- Khipu's
notify_urlcallback is not authoritative. The POST Khipu sends when the customer signs — or when a subscription is unsubscribed — carriessubscription_idand a lowercasestatus("enabled"/"discarded"), with no signature, no token and no provider-authenticated field. Anyone can send it. Treat it as a hint to callget_subscription, which is authenticated, and decide on that result.
Charge rejections
create_charge succeeds on Khipu's documented 201 Created, and also on a
200 carrying a usable ChargeResult — the contract Khipu published until
2026-09, kept for compatibility. That allowance is exactly those two statuses:
any other 2xx, 202 Accepted above all, raises KhipuResponseError rather
than being read as a created charge.
A refused charge comes back as 422 with Khipu's own reason, raised as
KhipuApiError:
try:
charge = client.create_charge(...)
except KhipuApiError as exc:
if exc.status_code == 422:
code = exc.payload["error_code"] # e.g. MAX_AMOUNT_PER_CHARGE_EXCEEDED
Khipu documents MAX_AMOUNT_PER_CHARGE_EXCEEDED, MAX_MONTHLY_AMOUNT_EXCEEDED,
SUBSCRIPTION_NOT_CHARGEABLE, CURRENCY_MISMATCH, INVALID_AMOUNT,
IDEMPOTENCY_KEY_REUSED, INVALID_PAYLOAD and MERCHANT_ID_NOT_RESOLVED.
exc.message carries Khipu's error_message text.
Implemented operations
| Method | HTTP | Endpoint |
|---|---|---|
create_instant_payment |
POST | /v3/payments |
get_instant_payment |
GET | /v3/payments/{id} |
create_subscription |
POST | /v1/automatic-payment/subscription |
get_subscription |
GET | /v1/automatic-payment/subscription/{id} |
create_charge |
POST | /v1/automatic-payment/charge-intent |
Authentication is a single static API key sent as the x-api-key header on
every request. Khipu API v3 uses no request signing.
The generated clients under generated/ cover more of Khipu's surface than the
facade deliberately exposes: unsubscribing a PAC subscription
(DELETE /v1/automatic-payment/subscription/{id}, an irreversible operation),
cancelling a payment, POST /v3/payments/{id}/void,
POST /v3/refunds, GET /v3/refund-wallet/balance, GET /v3/banks,
GET /v3/merchants/{id}/paymentMethods, GET /v3/predict,
GET /v3/predict/all, and the receiver endpoints. They are regenerated and kept
in sync, but are not part of the supported API and may change shape when the
specs are refreshed. Promoting any of them onto KhipuClient is a deliberate
decision, not something to do because the generated code exists.
Note that Khipu marks POST /v3/payments/{id}/refunds as obsolete in favour of
POST /v3/payments/{id}/void, and that it now refunds the full amount only.
Out of scope
These are deliberate boundaries, not gaps waiting to be filled:
- Webhooks. The SDK sends
notify_urlbut does not receive, verify or parse inbound Khipu notifications. That is framework-specific work (routing, CSRF, raw-body access, replay handling across Khipu's two-day retry window) and belongs in the consuming application. - Settlement / reconciliation. Neither vendored spec exposes a settlement,
report or payout endpoint. Khipu delivers settlement out of band. The closest
per-payment evidence is
conciliation_date/out_of_date_conciliationonget_instant_payment(). - Fee quotation. No currently implemented Khipu operation, and no endpoint
in either vendored spec, returns the merchant processing fee before a payment
is created.
GET /v3/predictandGET /v3/predict/allreturn payer transfer limits, not fees;integrator_feeis a commission you send, not one you read. This SDK does not invent a fee formula. If you need to show a fee before the payer commits, that number has to come from your commercial agreement with Khipu, reconciled later against actual settlement. - Retries. See Idempotency.
OpenAPI specifications and regeneration
Specs live under openapi/ and are pinned by sha256:
make download-specs # refresh from docs.khipu.com
make lock-specs # update OPENAPI_SPECS.lock.json
make verify-spec-lock # CI check
make generate-all # regenerate both clients
make generate-all also recreates generated/__init__.py, which packaging
depends on, so the file survives regeneration.
CI verifies the lock against the committed specs, which is what makes
generation reproducible. It deliberately does not download specs in the blocking
path: make download-specs overwrites the committed files, so verifying
afterwards would compare upstream against the pin rather than checking this
repository, and any Khipu documentation edit would turn unrelated pull requests
red. Upstream drift is watched by a separate non-blocking spec-drift job that
reports the diff.
Adopting a newer published spec is therefore a deliberate change:
make download-specs
make lock-specs
make generate-all
make check # review the generated diff and adjust the facade if needed
The code generator is pinned exactly (openapi-python-client==0.28.0) in the
dev extra. It is the build tool that produces the committed contents of
generated/, so a floating version would let a generator release change that
code with no change in this repository. Bumping it is a deliberate step:
change the pin, run make generate-all, and review the diff.
Never edit files under generated/. If behaviour needs to change, change the
khipu_sdk facade.
Compatibility overlay
Rarely, the live API returns a value Khipu's own document does not declare.
The committed spec must stay Khipu's document byte for byte, and generated/
must stay reproducible, so neither is edited. Instead, make generate-instant
runs tools/spec_compat.py, which writes a derived copy of the Instant Payments
spec to build/openapi/ and generates from that.
The overlay currently has one entry: direct_debit is added to
payment-get-payment.payment_method (#15). The derived copy differs from
Khipu's document by exactly that one enum line, and generated/ by exactly
one enum member.
Each entry records the exact enum Khipu publishes today and refuses to apply to anything else:
- if Khipu publishes the value itself,
make generate-allfails and says the entry is obsolete. Delete the entry, regenerate, and remove theTestUpstreamStillOmitsDirectDebitguard intests/unit/test_instant_payments_spec_compat.py; - if the enum changes any other way, generation fails until the entry is reconciled with the new document;
- the non-blocking
spec-driftjob runsmake check-spec-compatagainst the freshly published spec, so either case is reported before anyone adopts it.
CI also fails when committed generated/ differs from a fresh
make generate-all, so the overlay can be neither bypassed nor hand-applied.
Development
make check # ruff + mypy + pytest
make test # pytest
make lint # ruff
make typecheck # mypy
make build # build the wheel
make smoke-dist # build, install into a clean venv, smoke test the installed artifact
The test suite performs no network access; HTTP is intercepted with
pytest-httpx at the transport layer, so tests exercise the real path from the
facade through serialization, httpx and generated response parsing.
Pinned tooling
ruff, mypy and openapi-python-client are pinned exactly in the dev
extra. All three decide what CI accepts, and all three change their verdict on
unchanged code as they release: a linter adds rules, a type checker tightens
inference, a generator emits different output. Left floating, make check
passes locally and fails in CI purely because the runner resolved a newer
version.
ruff also skips generated/ (see [tool.ruff]), because that code is
machine-produced and must never be hand-edited, so findings there could only be
silenced rather than fixed.
Bumping any of the three is a deliberate maintenance step: change the pin, run
make check (or make generate-all for the generator), and deal with whatever
the new version reports. At the time of writing, moving to a current ruff
would surface roughly 70 style findings in this repository's own code, mostly
Optional[X] to X | None modernisation -- a migration worth doing on its own,
not as a side effect of an unrelated build.
The live integration test is disabled by default:
export KHIPU_RUN_INTEGRATION_TESTS=1
export KHIPU_API_KEY="..."
make test
Releases
Merging to master does not release anything: it creates no tag, no GitHub
Release and no package upload. Publishing is always a deliberate maintainer
action, and both paths below build a fresh wheel and sdist, run
tools/check_dist.py and smoke-test both artifacts from a clean install before
uploading anything.
tools/check_dist.py --tag, which both publish workflows run, refuses to
publish unless both Issue #18 publication gates are resolved. Both are:
LICENSEnames its confirmed copyright holder. Resolved: it carriesCopyright (c) 2025-2026 Yasel Couce(Issue #24), and the check still refuses the template placeholder should it return; andPUBLICATION_GATES.tomlrecordsapproved = true, with evidence, for an explicit basis to redistribute Khipu's OpenAPI documents and the clients generated from them. Resolved: Khipu's written permission is recorded in Issue #18 (Issue #26), and the check still refusesapproved = falseor an approval without evidence should either return.
Each change is an owner decision made in its own reviewed pull request. Resolving the gates publishes nothing: publication remains the deliberate maintainer action described below.
- Bump
project.versioninpyproject.tomland move the changelog entries into a dated## [X.Y.Z] - YYYY-MM-DDsection in the same pull request. - TestPyPI (optional rehearsal). Run the Publish to TestPyPI workflow
(
.github/workflows/publish-testpypi.yml) manually frommaster, typing the version being published. It refuses to build a commit that is not already onmaster. The run installs the uploaded release back from TestPyPI and smoke-tests it. - PyPI. Publish a GitHub Release whose tag is exactly
vX.Y.Zon a commit that is already onmaster. The Publish to PyPI workflow (.github/workflows/publish-pypi.yml) refuses a tag that does not matchproject.versionor that is not onmaster, then uploads and verifies the release. - First public release only (0.3.3). Until it is released, 0.3.3's
changelog entries stay under
## [Unreleased]as its target release. Once 0.3.3 is genuinely on PyPI and tagv0.3.3exists, open a small follow-up pull request that:- cuts the 0.3.3 entries from
## [Unreleased]into a dated## [0.3.3] - YYYY-MM-DDsection carrying the actual release date; - removes the Pre-release status note from Installation;
- makes the install commands current instead of "after 0.3.3 is released";
- switches the pre-release README and CHANGELOG contract in
tests/unit/test_release_metadata.pyto released-state semantics.
- cuts the 0.3.3 entries from
Both workflows publish with PyPI
Trusted Publishing (OIDC), through
the GitHub environments testpypi and pypi. No PyPI API token is stored in
this repository.
License
MIT; see LICENSE. The OpenAPI documents under openapi/ are Khipu's
publications and are not covered by that license.
Security issues: see SECURITY.md. Contributions: see CONTRIBUTING.md.
Metadata
Release files for khipu-python-sdk 0.3.3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| khipu_python_sdk-0.3.3.tar.gz | 88.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| khipu_python_sdk-0.3.3-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 195.1 kB
Release files / khipu_python_sdk-0.3.3.tar.gz
| Download URL | khipu_python_sdk-0.3.3.tar.gz |
|---|---|
| Size | 88.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
29ffa225d4f5ad29ef6dd233ba12ed499a9a61ff46a3109ab1441d16ba2f17c5
|
|
BLAKE2b-256 checksum How to use checksums |
7418dffe2752327ac61ffd705e24306adbbfff968758af0febb0663872a522c5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.
Transparency logRelease files / khipu_python_sdk-0.3.3-py3-none-any.whl
| Download URL | khipu_python_sdk-0.3.3-py3-none-any.whl |
|---|---|
| Size | 106.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
24debf91c2886be405d2431096037818422293203b7bbcc7de31389289b28e87
|
|
BLAKE2b-256 checksum How to use checksums |
1b5d478769b931117b6ff83ae2bf06de2c8a4d258d7438b588f1348ee93ca90c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.
Transparency log