Skip to main content

Python client for the Markpost API (sync + async, typed)

Project description

English | 简体中文

Markpost Python Client

A typed, sync + async Python client for the Markpost API.

PyPI version Python versions License: MIT CI


Installation

Requires Python >=3.10.

uv add markpost
# or
pip install markpost

Quick Start

Sync

from markpost import Markpost

with Markpost("https://markpost.cc", "alice", "secret") as client:
    created = client.create_post("Hello", "# Body in **markdown**")
    print(created.id)           # "p-<nanoid>"

    html = client.get_post(created.id)               # full HTML page (str)
    md = client.get_post(created.id, format="raw")   # "# Hello\n\n..." (str)

    page = client.list_posts(limit=20)
    for item in page.items:
        print(item.qid, item.title)

Async

from markpost import AsyncMarkpost

async with AsyncMarkpost("https://markpost.cc", "alice", "secret") as client:
    created = await client.create_post("Hello", "# Body")
    html = await client.get_post(created.id)
    page = await client.list_posts(limit=20)

Authentication

Pass username + password to the constructor to auto-login (sync logs in immediately; async logs in lazily on the first call or __aenter__):

client = Markpost("https://markpost.cc", username="alice", password="secret")

Or log in manually:

client = Markpost("https://markpost.cc")
result = client.login("alice", "secret")
print(result.user.role, result.token)

The access token is refreshed automatically when it is about to expire or when the backend returns 401. Concurrent refreshes are deduplicated into a single backend call (single-flight), which is essential because the backend rotates refresh tokens one-time and revokes the whole session on reuse.

Posts

create_post authenticates with a post key (not a JWT). If you don't pass one, the client fetches and caches your post key automatically.

created = client.create_post("Title", "body")               # auto-fetches post key
created = client.create_post("Title", "body", post_key="mpk-...")

get_post returns a str (HTML by default, or markdown when format="raw"). It supports conditional requests via If-None-Match:

etag = "...from a previous response..."
result = client.get_post("p-abc", if_none_match=etag)
# result is None when the backend returns 304 (not modified)

Pagination uses a flat structure:

page = client.list_posts(page=2, limit=50)
# page.items, page.total, page.page, page.limit, page.total_pages

Delivery channels & history

ch = client.create_channel(
    kind="feishu",
    name="ops-alerts",
    configuration={"webhook_url": "https://...", "card_link_url": "https://..."},
    keywords="",  # optional keyword filter expression
)

# PATCH semantics: pass only the fields you want to change.
client.update_channel(ch.id, enabled=False)

channels = client.list_channels()          # list[Channel] (no pagination)
history = client.list_delivery_history(channel_id=ch.id)  # Page[DeliveryHistoryItem]
latest = client.list_latest_delivery()     # list[DeliveryHistoryItem], one per channel

# Send a diagnostic card to verify the webhook is wired up. Fire-and-forget:
# the backend sends it synchronously but writes no delivery_history row.
client.test_channel(ch.id)

Error handling

Errors are a typed hierarchy rooted at MarkpostError:

from markpost import (
    MarkpostError, APIError,
    BadRequestError, AuthenticationError, PermissionDeniedError,
    NotFoundError, ConflictError, UnprocessableEntityError, RateLimitError,
    InternalServerError, APITimeoutError, APIConnectionError,
)

try:
    client.create_post("", "body")
except UnprocessableEntityError as e:
    print(e.status_code, e.code)   # 422 "title_too_long" / "required" / ...
    for fe in e.errors:            # parsed per-field details
        print(fe.field, fe.code, fe.message)
except RateLimitError as e:
    print(e.limit, e.remaining, e.reset)   # parsed from RateLimit-* headers
except AuthenticationError as e:
    print(e.code)                  # "invalid_credentials", "invalid_token", ...
except APIError as e:
    print(e.status_code, e.code, e.message)

Timeouts and network errors map to APITimeoutError / APIConnectionError (both retried automatically). 5xx and 429 are retried; 4xx are not.

Configuration

Markpost(
    base_url,
    username=None,
    password=None,
    *,
    timeout=None,             # float | httpx.Timeout (defaults to a safe connect/read/write/pool split)
    max_retries=2,             # 0 disables retries
    post_key=None,             # pre-seed a post key
    http_client=None,          # inject a custom httpx client (testing)
    verify=True,               # set False for the self-signed e2e container
)

License

MIT License — see LICENSE.txt.

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

markpost-0.2.0rc2.tar.gz (122.5 kB view details)

Uploaded Source

Built Distribution

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

markpost-0.2.0rc2-py3-none-any.whl (23.7 kB view details)

Uploaded Python 3

File details

Details for the file markpost-0.2.0rc2.tar.gz.

File metadata

  • Download URL: markpost-0.2.0rc2.tar.gz
  • Upload date:
  • Size: 122.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.13

File hashes

Hashes for markpost-0.2.0rc2.tar.gz
Algorithm Hash digest
SHA256 8666416dea8c9d5de7adb4740f3d14ea047854745f5f963c5cd88d34627522df
MD5 f39fc7fc20f2f439a349fa851374b213
BLAKE2b-256 2e04de45983ff4978e646afe4c88c3c1b9ec20158611fb13c698a5588f9876e2

See more details on using hashes here.

File details

Details for the file markpost-0.2.0rc2-py3-none-any.whl.

File metadata

  • Download URL: markpost-0.2.0rc2-py3-none-any.whl
  • Upload date:
  • Size: 23.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.13

File hashes

Hashes for markpost-0.2.0rc2-py3-none-any.whl
Algorithm Hash digest
SHA256 0b9d664da1910debcd039648714e79ec1a44ff69ddf7a7f0b78ab79d8d2e5091
MD5 2f88b42fab81d848b4c6dfe97da8f84e
BLAKE2b-256 0e2663d1c873bf3b5f7f842a70965d7fd1e23f22bc5005e69588bb3acd7d7bff

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