A collection of useful addons for the Kurigram library
Project description
kurigram-addons is a production-grade toolkit for building Telegram bots with Kurigram. It layers a clean, opinionated API on top of Pyrogram: a Client subclass, declarative FSM conversations, strongly-typed callback data, a full DI container, middleware with typed context, per-handler guards, broadcast helpers, i18n, SQLite storage, health checks, and a comprehensive testing module โ all with zero boilerplate.
๐ Full Documentation โ ๐ Changelog โ ๐ค Showcase bot โ
โ ๏ธ Final release notice
v0.5.0 is the last release from the original author. The library is feature-complete and all known bugs are fixed. It will not receive further updates from this repository.
Looking for a maintainer! If you use this library and want to keep it alive โ fix bugs, support new Kurigram/Pyrogram versions, or add features โ please open an issue on the issue tracker expressing your interest. Forks and community-maintained continuations are warmly encouraged under the MIT licence.
โจ What's new in v0.5.0
Please Note the dev branch is not usable at all
This release is the largest since v0.4.0. It fixes every known correctness bug, hardens concurrency, and adds new features.
Bug fixes
await helper.get_data()replaces the broken async propertyhelper.dataglobal_poolis now reset afterstop()so restarting a client works cleanlyMemoryStorageheap tombstoning โ TTL renewal and key deletion no longer cause premature expiry or ghost entriesMemoryStorage.get_state()returns a deep copy, fixing optimistic lockingRateLimitMiddlewarenow actually fires โ the counter schema was wrong in previous versionsFSMContextManagerraises a clear error instead of silently sharing state via a"global"fallback identifierinclude_middleware()onKurigramClientwas calling a nonexistent method โ fixed
Concurrency & security
FSMContext.update_data(),set_state(), andclear_data()are now atomic via CAS retry loops- Per-instance circuit breakers โ multi-account bots no longer share one breaker across all clients
_validate_telegram_id()tightened to realistic Telegram ID rangesfallback_messagevalidated at config load time with a 200-character cap
New features (Phase 4)
| Feature | What it gives you |
|---|---|
CallbackData |
Strongly-typed, versioned callback data โ no more raw strings |
SQLiteStorage |
Persistent FSM with zero infrastructure (aiosqlite) |
Conversation.timeout |
Auto-finish idle conversations via storage TTL |
@use_middleware |
Scope middleware to a single handler, not globally |
broadcast() |
Async-generator bulk sender with FloodWait handling |
FSMContext.get_history() |
State-transition audit ring-buffer |
Router.on_callback_data(pattern) |
Regex capture groups injected as handler kwargs |
KurigramClient(health_port=N) |
HTTP /health endpoint for container orchestration |
DIContainer / Depends |
Full dependency injection, resolved by type annotation |
I18nMiddleware |
Auto-detects user language, injects _() into helper |
kurigram_addons.testing |
Mock factories and ConversationTester for unit tests |
DX improvements
MiddlewareContextdataclass โ one typed object instead of positional name sniffingState.filter()/ConversationState.filter()โ replaceStateFilter("Cls:state")await app.stats()returns a typedPoolStatisticsfrozen dataclassBaseStorage.increment()โ atomic counter, maps to RedisINCRBYRoutersupportsasync withcontext managerMemoryStorageauto-started and auto-stopped byKurigramClientpatch()andunpatch()deprecated โ emitDeprecationWarning
๐ฆ Installation
pip
pip install kurigram-addons
Poetry
poetry add kurigram-addons
uv
uv add kurigram-addons
Optional extras (Redis and/or SQLite storage backends):
pip
pip install kurigram-addons[redis] # RedisStorage
pip install kurigram-addons[sqlite] # SQLiteStorage (aiosqlite)
pip install kurigram-addons[all] # both
Poetry
poetry add kurigram-addons[redis]
poetry add kurigram-addons[sqlite]
uv
uv add kurigram-addons[redis]
uv add kurigram-addons[sqlite]
Requirements: Python 3.10+, kurigram โฅ 2.1.35 (or compatible Pyrogram fork), pydantic โฅ 2.11.
๐ Quick start
from kurigram_addons import KurigramClient, Router, MemoryStorage, CallbackData
router = Router()
class Action(CallbackData, prefix="act"):
name: str
@router.on_command("start")
async def start(client, message):
from kurigram_addons import InlineKeyboard
kb = InlineKeyboard(row_width=2)
kb.add(Action(name="profile").button("๐ค Profile"))
kb.add(Action(name="settings").button("โ๏ธ Settings"))
await message.reply("Welcome!", reply_markup=kb)
@router.on_callback_data(r"act:(?P<name>\w+)")
async def on_action(client, query, name: str):
await query.answer(f"Opening {name}โฆ", show_alert=True)
app = KurigramClient(
"my_bot",
bot_token="YOUR_TOKEN",
storage=MemoryStorage(),
auto_flood_wait=True,
)
app.include_router(router)
app.run()
๐งฉ Features
KurigramClient
The recommended entry point. Replaces the deprecated patch() function. Everything is configured at construction time and the client manages its own lifecycle.
from kurigram_addons import KurigramClient, MemoryStorage
app = KurigramClient(
"my_bot",
api_id=12345,
api_hash="...",
bot_token="...",
storage=MemoryStorage(), # auto-started and auto-stopped
auto_flood_wait=True, # absorb FloodWait automatically
max_flood_wait=60, # raise if Telegram asks to wait longer
health_port=8080, # optional: GET /health for container probes
)
app.include_router(router)
app.include_conversation(Registration)
app.include_menus(main_menu, profile_menu)
app.run()
Lifecycle hooks
@app.on_startup
async def init_db():
await database.connect()
@app.on_shutdown
async def close_db():
await database.disconnect()
Live pool statistics
await app.stats() returns a frozen dataclass โ IDE-autocompleted fields, not a plain dict:
@router.on_command("stats")
async def stats_cmd(client, message):
s = await app.stats()
await message.reply(
f"Active helpers: {s.active_helpers}\n"
f"Uptime: {s.uptime:.0f}s\n"
f"Total created: {s.total_helpers_created}"
)
CallbackData โ strongly-typed callbacks
Declare callback payloads as typed classes. Encoding, decoding, overflow checks, and Pyrogram filters are handled automatically.
from kurigram_addons import CallbackData
class Page(CallbackData, prefix="pg"):
num: int
total: int
# Building a button
btn = Page(num=3, total=10).button("Page 3")
# btn.callback_data == "pg:3:10"
# Decoding in a handler
obj = Page.unpack("pg:3:10")
print(obj.num, obj.total) # 3 10
# Pyrogram filter โ exact class match
@router.on_callback_query(Page.filter())
async def any_page(client, query): ...
# Pyrogram filter โ specific field value
@router.on_callback_query(Page.filter(num=1))
async def first_page(client, query): ...
Supported field types: str, int, float, bool.
pack() raises ValueError if the encoded string exceeds Telegram's 64-byte limit.
Router
from kurigram_addons import Router
router = Router()
# Exact command
@router.on_command("start")
async def start(client, message): ...
# Exact callback_data string
@router.on_callback("profile")
async def profile(client, query): ...
# Regex with capture group injection (NEW in v0.5.0)
@router.on_callback_data(r"page:(?P<num>\d+)")
async def paginate(client, query, num: str):
# `num` injected automatically from the named capture group
await show_page(query, int(num))
# Positional groups โ group_1, group_2, ...
@router.on_callback_data(r"item:(\d+):(buy|sell)")
async def trade(client, query, group_1: str, group_2: str): ...
# Sub-router composition
child = Router()
router.include_router(child)
# Async context manager
async with Router() as r:
@r.on_command("ping")
async def ping(client, message): ...
r.set_client(app)
# handlers unregistered here
Conversation handler
Declarative multi-step FSM flows. State transitions are backed by the configured FSM storage โ conversations survive bot restarts when SQLiteStorage or RedisStorage is used.
from kurigram_addons import Conversation, ConversationState, ConversationContext
class Registration(Conversation):
timeout = 300 # auto-finish after 5 min of inactivity (NEW)
name = ConversationState(initial=True)
age = ConversationState()
confirm = ConversationState()
@name.on_enter
async def ask_name(self, ctx: ConversationContext):
await ctx.message.reply("What is your name?")
@name.on_message
async def save_name(self, ctx: ConversationContext):
await ctx.helper.update_data(name=ctx.message.text)
await self.goto(ctx, self.age)
@age.on_enter
async def ask_age(self, ctx: ConversationContext):
await ctx.message.reply("How old are you?")
@age.on_message
async def save_age(self, ctx: ConversationContext):
if not ctx.message.text.isdigit():
await ctx.message.reply("Please enter a number.")
return
await ctx.helper.update_data(age=int(ctx.message.text))
await self.goto(ctx, self.confirm)
@confirm.on_enter
async def ask_confirm(self, ctx: ConversationContext):
# v0.5.0: await ctx.get_data() โ lazy, cached, correctly awaited.
# The old ctx.helper.data async property is removed.
data = await ctx.get_data()
await ctx.message.reply(
f"Name: {data['name']}, Age: {data['age']}. Confirm? (yes/no)"
)
@confirm.on_message
async def do_confirm(self, ctx: ConversationContext):
if ctx.message.text.lower() == "yes":
data = await ctx.get_data()
await ctx.message.reply(f"Welcome, {data['name']}!")
else:
await ctx.message.reply("Cancelled.")
await self.finish(ctx)
app.include_conversation(Registration)
Typed data access with a Pydantic model:
from pydantic import BaseModel
class UserData(BaseModel):
name: str
age: int
data = await ctx.get_data(model=UserData)
print(data.name, data.age) # IDE-autocompleted, schema-validated
State filter shorthand:
# v0.5.0 โ no more stringly-typed filters
@router.on_message(Registration.name.filter())
async def in_name_state(client, message, patch_helper): ...
# v0.4.x โ the old way (still works, but verbose)
from pyrogram_patch.fsm.filter import StateFilter
@router.on_message(StateFilter("Registration:name"))
async def in_name_state(client, message, patch_helper): ...
FSM storage
Three backends, all implementing the same BaseStorage interface.
MemoryStorage
In-process, non-persistent. Good for development and stateless bots.
from kurigram_addons import MemoryStorage
# TTL cleanup runs automatically โ no need to call storage.start() manually.
# KurigramClient starts and stops the backend as part of its own lifecycle.
storage = MemoryStorage(default_ttl=0)
SQLiteStorage (new in v0.5.0)
Persistent with zero infrastructure. Uses aiosqlite under the hood. compare_and_set is atomic via BEGIN IMMEDIATE.
from kurigram_addons import KurigramClient, SQLiteStorage
app = KurigramClient("my_bot", storage=SQLiteStorage("fsm.db"), ...)
RedisStorage
For distributed or multi-process deployments. Each RedisStorage instance owns its own circuit breaker โ failures for one client do not trip the breaker for others.
import redis.asyncio as aioredis
from kurigram_addons import RedisStorage
redis = aioredis.from_url("redis://localhost")
app = KurigramClient("my_bot", storage=RedisStorage(redis, prefix="bot"), ...)
Custom storage
Subclass BaseStorage and implement six methods. increment() is new in v0.5.0 โ it maps to INCRBY on Redis, a locked integer add on MemoryStorage, and an UPSERT on SQLite.
from kurigram_addons import BaseStorage
class MyStorage(BaseStorage):
async def set_state(self, identifier, state, *, ttl=None): ...
async def get_state(self, identifier): ...
async def delete_state(self, identifier): ...
async def compare_and_set(self, identifier, new_state, *, expected_state=None, ttl=None): ...
async def list_keys(self, pattern="*"): ...
async def clear_namespace(self): ...
async def increment(self, identifier, amount=1, *, ttl=None): ... # NEW
FSM state history (new in v0.5.0)
A ring-buffer of up to 50 recent state transitions, stored under a separate key so it never interferes with FSM state.
@router.on_command("history")
async def history_cmd(client, message, patch_helper):
if patch_helper._fsm_ctx is None:
await message.reply("No active conversation.")
return
entries = await patch_helper._fsm_ctx.get_history(limit=5)
# [{"state": "Registration:name", "at": 1711234567.0}, ...]
for entry in reversed(entries):
print(entry["state"], entry["at"])
# Clear when done
await patch_helper._fsm_ctx.clear_history()
Middleware
Global middleware
from kurigram_addons import MiddlewareContext
# NEW in v0.5.0: single typed context object โ no positional name sniffing
async def audit_log(ctx: MiddlewareContext) -> None:
user = getattr(ctx.update, "from_user", None)
if user:
print(f"[{user.id}] {type(ctx.update).__name__}")
# Legacy positional signatures still work unchanged
async def old_style(update, client, patch_helper) -> None:
print(update)
@app.on_startup
async def setup():
await app._pool.add_middleware(audit_log, kind="before", priority=50)
await app._pool.add_middleware(old_style, kind="before", priority=40)
Per-handler middleware (new in v0.5.0)
Scope middleware to a single handler instead of running it globally on every update.
from kurigram_addons import use_middleware
async def require_admin(update, client, patch_helper):
user = getattr(update, "from_user", None)
if user and user.id not in ADMIN_IDS:
await update.reply("โ Admins only.")
from pyrogram import StopPropagation
raise StopPropagation
@router.on_command("ban")
@use_middleware(require_admin) # only runs for /ban
async def ban_cmd(client, message): ...
# Stack multiple guards (outer-first execution)
@router.on_command("broadcast")
@use_middleware(require_admin)
@use_middleware(RateLimit(per_user=1, window=60))
async def broadcast_cmd(client, message): ...
Around middleware
async def timing(handler, update, client=None):
import time
t = time.perf_counter()
result = await handler() # call the actual handler
elapsed = (time.perf_counter() - t) * 1000
print(f"Handler took {elapsed:.1f}ms")
return result
await app._pool.add_middleware(timing, kind="around")
Rate-limit middleware
Uses storage.increment() โ a single atomic call, no CAS loop, works correctly with all backends.
from kurigram_addons import RateLimitMiddleware
rl = RateLimitMiddleware(rate=10, period=60, scope="user", block=True)
@app.on_startup
async def setup():
await app._pool.add_middleware(rl, kind="before")
Dependency Injection (new in v0.5.0)
Declare what a handler needs by type annotation. The DIContainer resolves and injects automatically โ no Depends() marker required for annotated parameters.
from kurigram_addons import DIContainer, Depends
di = DIContainer()
class Database:
async def get_user(self, uid: int): ...
async def get_db() -> Database:
return Database()
di.register(Database, get_db)
# Attach once โ the dispatcher picks it up for every handler
di.attach(app)
# Injection by type annotation
@router.on_command("profile")
async def profile(client, message, db: Database):
user = await db.get_user(message.from_user.id)
await message.reply(f"Hello, {user.name}!")
# Explicit Depends() marker for ambiguous or dynamic providers
@router.on_command("config")
async def config_cmd(client, message, cfg=Depends(get_config)):
await message.reply(f"Version: {cfg.version}")
# Register a plain value (no factory call)
di.register_value(AppConfig, AppConfig(debug=True))
i18n middleware (new in v0.5.0)
Detects each user's language from their Telegram profile. Injects a _() translation callable into the helper under the key "_".
JSON locale files (locales/en.json, locales/ru.json, โฆ):
{"greeting": "Hello!", "farewell": "Goodbye!"}
from kurigram_addons import I18nMiddleware
i18n = I18nMiddleware(
default_lang="en",
locales_path="locales",
use_json=True,
)
@app.on_startup
async def setup():
await app._pool.add_middleware(i18n, kind="before")
@router.on_command("hello")
async def hello(client, message, patch_helper):
_ = await patch_helper.get("_") or (lambda k: k)
await message.reply(_("greeting"))
GNU gettext (.po/.mo files): omit use_json=True.
Broadcast (new in v0.5.0)
An async generator that sends a message to many users, automatically absorbing FloodWait and skipping blocked / deactivated accounts.
from kurigram_addons import broadcast
@router.on_command("announce")
async def announce(client, message):
user_ids = await db.get_all_user_ids()
ok = fail = skip = 0
async for result in broadcast(
client,
user_ids,
"๐ข Important update!",
delay=0.05, # 50ms between sends
max_flood_wait=60, # absorb FloodWait โค 60s; surface larger ones
on_error="skip", # "skip" or "stop"
):
if result.ok: ok += 1
elif result.skipped: skip += 1
else: fail += 1
await message.reply(f"Sent {ok} | Skipped {skip} | Failed {fail}")
Health check endpoint (new in v0.5.0)
Pass health_port=N to KurigramClient. A lightweight stdlib-only HTTP server starts alongside the bot and serves GET /health as JSON.
app = KurigramClient("my_bot", ..., health_port=8080)
GET /health
โ 200 OK
{
"status": "ok",
"pool": {"active_helpers": 2, "uptime": 3600.0, ...},
"storage": "healthy"
}
Returns 503 Service Unavailable when storage reports unhealthy. Useful for Kubernetes liveness/readiness probes and Docker HEALTHCHECK.
Menu system
from kurigram_addons import Menu
main = Menu("main", text="๐ Main Menu")
profile = Menu("profile", text="๐ค Profile", parent=main)
settings = Menu("settings", text="โ๏ธ Settings", parent=main)
main.button("๐ค Profile", goto="profile")
main.button("โ๏ธ Settings", goto="settings")
async def edit_name(client, query):
await query.answer("โ๏ธ Edit name")
profile.button("โ๏ธ Edit Name", callback=edit_name)
app.include_menus(main, profile, settings)
Every non-root menu automatically gets a Back button. Navigation state is maintained in the FSM storage.
Rate limiting
Decorator (per-handler, no storage required):
from kurigram_addons import RateLimit
@router.on_command("generate")
@RateLimit(per_user=3, window=60, message="โณ Retry in {remaining}s.")
async def generate(client, message):
await message.reply("Generatingโฆ")
# Custom handler for when the limit is hit
async def on_limited(client, update, remaining):
await update.reply(f"โณ Wait {remaining}s before trying again.")
@RateLimit(per_user=3, window=60, on_limited=on_limited)
Middleware (global, storage-backed, atomic):
from kurigram_addons import RateLimitMiddleware
rl = RateLimitMiddleware(rate=20, period=60, scope="user")
@app.on_startup
async def setup():
await app._pool.add_middleware(rl, kind="before")
Command parser
from kurigram_addons import parse_command, CommandParseError
from typing import Optional
@router.on_command("ban")
async def ban(client, message):
try:
args = parse_command(message.text, user_id=int, reason=Optional[str])
# /ban 12345 โ {"user_id": 12345, "reason": None}
# /ban 12345 spam โ {"user_id": 12345, "reason": "spam"}
await do_ban(args["user_id"], args.get("reason"))
except CommandParseError as e:
await message.reply(f"Usage: `/ban <user_id> [reason]`\n`{e}`")
Keyboards
from kurigram_addons import InlineKeyboard, ReplyKeyboard, ReplyButton, CallbackData
class Color(CallbackData, prefix="c"):
name: str
kb = InlineKeyboard(row_width=3)
for name in ("Red", "Green", "Blue"):
kb.add(Color(name=name.lower()).button(name))
# Reply keyboard
rk = ReplyKeyboard(resize_keyboard=True, one_time_keyboard=True)
rk.add(
ReplyButton("๐ Location", request_location=True),
ReplyButton("๐ Contact", request_contact=True),
)
# Pagination
kb.paginate(total_pages=10, current_page=3, pattern="page_{number}")
Testing (new in v0.5.0)
Unit-test handlers and conversations without hitting Telegram's API.
from kurigram_addons.testing import (
make_message,
make_callback_query,
MockClient,
ConversationTester,
)
# Mock objects
msg = make_message(text="/start", user_id=42, chat_id=100)
query = make_callback_query(data="pg:2:10", user_id=42)
client = MockClient(me_id=999)
# MockClient records all sends
await client.send_message(100, "Hello!")
assert client.sent[0]["text"] == "Hello!"
client.reset()
# ConversationTester drives a full Conversation flow
async def test_registration():
tester = ConversationTester(Registration)
await tester.start(user_id=1, chat_id=1)
assert tester.current_state == "Registration:name"
await tester.send_message("Alice")
assert tester.current_state == "Registration:age"
await tester.send_message("30")
assert tester.current_state == "Registration:confirm"
tester.assert_replied("Confirm your details")
๐ Migration from v0.4.x
helper.data โ await helper.get_data()
# v0.4.x โ broken async property (returned a coroutine, not the dict)
data = await ctx.helper.data # โ
# v0.5.0 โ correct
data = await ctx.helper.get_data() # โ
data = await ctx.get_data() # โ
inside ConversationContext
StateFilter("Cls:state") โ Cls.state.filter()
# v0.4.x
from pyrogram_patch.fsm.filter import StateFilter
@router.on_message(StateFilter("Registration:name"))
# v0.5.0
@router.on_message(Registration.name.filter())
patch() / unpatch() โ KurigramClient
patch() and unpatch() now emit DeprecationWarning and will be removed in v1.0.0.
# v0.4.x โ deprecated
from pyrogram_patch import patch
manager = await patch(app)
# v0.5.0 โ recommended
app = KurigramClient("my_bot", storage=MemoryStorage(), ...)
Direct package imports
Old import paths still work but also emit DeprecationWarning:
# โ ๏ธ Deprecated (still works in v0.5.0, removed in v1.0.0)
from pykeyboard import InlineKeyboard
from pyrogram_patch import patch
# โ
Recommended
from kurigram_addons import InlineKeyboard, KurigramClient
๐๏ธ Architecture overview
kurigram_addons/ โ unified public namespace
โโโ client.py โ KurigramClient
โโโ conversation.py โ Conversation, ConversationState, ConversationContext
โโโ menu.py โ Menu, MenuButton
โโโ broadcast.py โ broadcast(), BroadcastResult (NEW)
โโโ health.py โ HealthServer (NEW)
โโโ i18n.py โ I18nMiddleware (NEW)
โโโ testing.py โ MockClient, ConversationTester, ... (NEW)
pyrogram_patch/ โ dispatcher, FSM, middleware
โโโ dispatcher.py โ PatchedDispatcher (DI + per-handler middleware)
โโโ patch_data_pool.py โ PatchDataPool, PoolStatistics
โโโ di.py โ DIContainer, Depends (NEW)
โโโ fsm/
โ โโโ base_storage.py โ BaseStorage (+ increment() abstract method)
โ โโโ context.py โ FSMContext (CAS writes, get_history())
โ โโโ states.py โ State, StatesGroup (+ .filter())
โ โโโ storages/
โ โโโ memory_storage.py โ MemoryStorage (tombstone heap, deepcopy)
โ โโโ redis_storage.py โ RedisStorage (per-instance breaker)
โ โโโ sqlite_storage.py โ SQLiteStorage (NEW)
โโโ middlewares/
โโโ middleware_manager.py โ MiddlewareContext, MiddlewareManager
โโโ per_handler.py โ use_middleware, run_handler_middlewares (NEW)
โโโ rate_limit.py โ RateLimitMiddleware (uses increment())
pykeyboard/ โ keyboard builder
โโโ callback_data.py โ CallbackData (NEW)
๐ค Contributing
git clone https://github.com/johnnie-610/kurigram-addons.git
cd kurigram-addons
poetry install
poetry run pytest tests/
Bug reports and pull requests are welcome on the issue tracker.
๐ License
MIT โ see LICENSE for details.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file kurigram_addons-0.5.0.tar.gz.
File metadata
- Download URL: kurigram_addons-0.5.0.tar.gz
- Upload date:
- Size: 124.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8b4762f7b6e6a709e329ba70169164167151487625b24d707c87a755621850d3
|
|
| MD5 |
7d67d5e696b48373bc158c0299124fe0
|
|
| BLAKE2b-256 |
2a00cbfc64bf723d156cf0257d8c401595e6530e5cac9b33fb16e69c7a1d6454
|
Provenance
The following attestation bundles were made for kurigram_addons-0.5.0.tar.gz:
Publisher:
deploy.yml on johnnie-610/kurigram-addons
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
kurigram_addons-0.5.0.tar.gz -
Subject digest:
8b4762f7b6e6a709e329ba70169164167151487625b24d707c87a755621850d3 - Sigstore transparency entry: 1461087077
- Sigstore integration time:
-
Permalink:
johnnie-610/kurigram-addons@591320e79848fee8c808c0caca2b445186b8766b -
Branch / Tag:
refs/heads/main - Owner: https://github.com/johnnie-610
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
deploy.yml@591320e79848fee8c808c0caca2b445186b8766b -
Trigger Event:
push
-
Statement type:
File details
Details for the file kurigram_addons-0.5.0-py3-none-any.whl.
File metadata
- Download URL: kurigram_addons-0.5.0-py3-none-any.whl
- Upload date:
- Size: 152.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d9cb1224cd4150614e06f6bfa2a14a0738837b833da19520cfd292d1427fcbb4
|
|
| MD5 |
f9be63c69bc369633a258a2f4389970c
|
|
| BLAKE2b-256 |
3e0bb47e8f45d4ce655984168a960e65184f6724c0d1527a4a97ec1f12b64342
|
Provenance
The following attestation bundles were made for kurigram_addons-0.5.0-py3-none-any.whl:
Publisher:
deploy.yml on johnnie-610/kurigram-addons
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
kurigram_addons-0.5.0-py3-none-any.whl -
Subject digest:
d9cb1224cd4150614e06f6bfa2a14a0738837b833da19520cfd292d1427fcbb4 - Sigstore transparency entry: 1461087162
- Sigstore integration time:
-
Permalink:
johnnie-610/kurigram-addons@591320e79848fee8c808c0caca2b445186b8766b -
Branch / Tag:
refs/heads/main - Owner: https://github.com/johnnie-610
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
deploy.yml@591320e79848fee8c808c0caca2b445186b8766b -
Trigger Event:
push
-
Statement type: