richpyro 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
richpyro/__init__.py ADDED
@@ -0,0 +1,762 @@
1
+ """
2
+ richpyro — a complete, easy-to-call wrapper around kurigram's Rich Messages
3
+ (Bot API 10.1+ InputRichMessage / InputRichBlock* / RichText* / raw
4
+ Client.send_rich_message / Message.edit_text(rich_message=...) / Message.reply_rich).
5
+
6
+ coded by devgagan — https://github.com/devgaganin
7
+
8
+ ────────────────────────────────────────────────────────────────────────
9
+ WHY THIS EXISTS (read this before using rich messages anywhere)
10
+ ────────────────────────────────────────────────────────────────────────
11
+ Two real production bugs, both discovered live on this project, are baked
12
+ into this library as automatic protections so nobody hits them again:
13
+
14
+ 1. RICH MESSAGES ARE BOT-ONLY.
15
+ Telegram only renders `rich_message=...` for a BOT account. Send it
16
+ from a real user/userbot client (a logged-in session, not a bot
17
+ token) and Telegram silently drops/rejects it — progress bars, status
18
+ cards, everything just stops updating with no visible error.
19
+ Fix baked in here: every send/edit/reply call in this library checks
20
+ `client.me.is_bot` and automatically falls back to a plain text
21
+ message with equivalent content when the client isn't a bot. You
22
+ never have to remember to branch on this yourself.
23
+
24
+ 2. A NUMERIC-STRING chat_id LOOKS LIKE A PHONE NUMBER TO kurigram.
25
+ `Client.resolve_peer()` takes any string chat_id, strips `+()-` and
26
+ whitespace, and if what's left is all digits it assumes it's a phone
27
+ number and calls `contacts.ResolvePhone` — a method BOT ACCOUNTS
28
+ CANNOT CALL AT ALL (`[400 BOT_METHOD_INVALID]`), and which fails with
29
+ `PHONE_NOT_OCCUPIED` for user accounts too unless that literal string
30
+ happens to be someone's real registered phone number. This bites
31
+ silently any time a chat_id is passed as `str(chat.id)` instead of
32
+ `int` — which is an extremely easy mistake since Telegram chat IDs
33
+ are commonly stringified for DB storage/dict keys elsewhere in a
34
+ codebase and then reused as-is for an API call.
35
+ Fix baked in here: every chat_id passed through this library is
36
+ auto-int-ified (`"-1001234567890"` -> `-1001234567890`) before it
37
+ ever reaches kurigram. Usernames/`@handles` pass through untouched.
38
+
39
+ ────────────────────────────────────────────────────────────────────────
40
+ INSTALL
41
+ ────────────────────────────────────────────────────────────────────────
42
+ pip install richpyro
43
+
44
+ Requires Kurigram (the actively-maintained Pyrogram fork, `import pyrogram`
45
+ still works) — richpyro builds on its Rich Messages support (Bot API
46
+ 10.1+). Plain Pyrogram/Pyrofork installs without Rich Messages will import
47
+ fine but the send()/edit()/reply() wrappers need the raw types this
48
+ library constructs.
49
+
50
+ ────────────────────────────────────────────────────────────────────────
51
+ QUICK START
52
+ ────────────────────────────────────────────────────────────────────────
53
+ import richpyro as rp
54
+
55
+ # simplest possible call — one line, works from a bot OR a user client:
56
+ await rp.quick(client, chat_id, "Processing your request...")
57
+
58
+ # a structured card with formatting + buttons:
59
+ card = rp.message(
60
+ rp.para("Hello ", rp.bold(name), "! Choose a plan:"),
61
+ rp.divider(),
62
+ rp.para("● ", rp.bold("Day Plan"), " — ₹20 (24 hours)"),
63
+ rp.buttons(
64
+ rp.btn("Day Plan ₹20", "pay_day"),
65
+ rp.url_btn("Learn more", "https://example.com"),
66
+ ),
67
+ )
68
+ sent = await rp.send(client, chat_id, card)
69
+ ...
70
+ await rp.edit(client, chat_id, sent.id, rp.text("Payment confirmed."))
71
+
72
+ # replying directly to an incoming message (uses its own client):
73
+ await rp.reply(message, card)
74
+
75
+ Every builder below returns a plain kurigram object — you can always drop
76
+ down to raw `types.InputRichMessage(...)` etc. if you need something this
77
+ library doesn't wrap yet; richpyro objects and raw kurigram objects mix
78
+ freely since richpyro never subclasses anything, it only constructs.
79
+ """
80
+ from __future__ import annotations
81
+
82
+ from datetime import datetime
83
+ from typing import Any, Iterable, Union
84
+
85
+ from pyrogram import enums, types
86
+
87
+ __version__ = "0.1.0"
88
+ __author__ = "devgagan"
89
+ __all__ = [
90
+ # text formatting
91
+ "bold", "italic", "underline", "strike", "spoiler", "code", "link",
92
+ "user_mention", "mention", "custom_emoji", "hashtag", "bot_command",
93
+ "phone_number", "email", "cashtag", "bank_card", "date_time",
94
+ "subscript", "superscript", "anchor_target", "anchor_link",
95
+ "footnote_ref", "footnote_link", "marked", "math_inline", "inline_button",
96
+ # buttons
97
+ "Style", "btn", "url_btn", "webapp_btn", "login_btn", "switch_inline_btn",
98
+ "switch_inline_here_btn", "switch_inline_chosen_btn", "copy_btn",
99
+ "disabled_btn", "buttons",
100
+ # media
101
+ "photo_media", "video_media", "animation_media", "audio_media",
102
+ "document_media", "voice_media", "caption", "named_media",
103
+ # blocks
104
+ "para", "heading", "divider", "footer", "anchor_block", "photo_block",
105
+ "video_block", "animation_block", "audio_block", "document_block",
106
+ "voice_block", "collage", "slideshow", "list_item", "bullet_list",
107
+ "table_cell", "table", "details", "blockquote", "expandable_quote",
108
+ "pull_quote", "preformatted", "map_block", "math_block", "thinking",
109
+ # message builders
110
+ "message", "blocks_message", "html_message", "markdown_message",
111
+ "text", "text_message",
112
+ # plain-text fallback rendering
113
+ "flatten_message",
114
+ # send / edit / reply
115
+ "is_bot_client", "send", "edit", "reply", "quick", "quick_edit",
116
+ ]
117
+
118
+ # A "rich text" argument anywhere in this module accepts: a plain str, a
119
+ # list mixing str/RichText parts (auto-concatenated), or a single RichText
120
+ # node (bold(...), italic(...), etc. — nest freely, e.g. bold(italic("x"))).
121
+ RichTextArg = Union[str, list, "types.RichText"]
122
+
123
+
124
+ # ════════════════════════════════════════════════════════════════════
125
+ # TEXT FORMATTING (wraps every RichText* input type)
126
+ # ════════════════════════════════════════════════════════════════════
127
+ def bold(text: RichTextArg) -> types.RichTextBold:
128
+ return types.RichTextBold(text=text)
129
+
130
+
131
+ def italic(text: RichTextArg) -> types.RichTextItalic:
132
+ return types.RichTextItalic(text=text)
133
+
134
+
135
+ def underline(text: RichTextArg) -> types.RichTextUnderline:
136
+ return types.RichTextUnderline(text=text)
137
+
138
+
139
+ def strike(text: RichTextArg) -> types.RichTextStrikethrough:
140
+ return types.RichTextStrikethrough(text=text)
141
+
142
+
143
+ def spoiler(text: RichTextArg) -> types.RichTextSpoiler:
144
+ return types.RichTextSpoiler(text=text)
145
+
146
+
147
+ def code(text: RichTextArg) -> types.RichTextCode:
148
+ """Inline monospace code (for a full code block use `preformatted()`)."""
149
+ return types.RichTextCode(text=text)
150
+
151
+
152
+ def link(text: RichTextArg, url: str) -> types.RichTextUrl:
153
+ """A clickable text hyperlink: link("Docs", "https://docs.pyrogram.org")."""
154
+ return types.RichTextUrl(text=text, url=url)
155
+
156
+
157
+ def user_mention(text: RichTextArg, user: "types.User") -> types.RichTextTextMention:
158
+ """Mention a user by their User object — works even if they have no
159
+ @username (unlike `mention()` below)."""
160
+ return types.RichTextTextMention(text=text, user=user)
161
+
162
+
163
+ def mention(text: RichTextArg, username: str) -> types.RichTextMention:
164
+ """Mention a user/channel by @username string."""
165
+ return types.RichTextMention(text=text, username=username)
166
+
167
+
168
+ def custom_emoji(custom_emoji_id: str, alternative_text: str) -> types.RichTextCustomEmoji:
169
+ """A premium custom emoji. `alternative_text` is what non-premium
170
+ clients / plain-text fallback show instead (e.g. the matching plain emoji)."""
171
+ return types.RichTextCustomEmoji(custom_emoji_id=custom_emoji_id, alternative_text=alternative_text)
172
+
173
+
174
+ def hashtag(text: RichTextArg, tag: str) -> types.RichTextHashtag:
175
+ return types.RichTextHashtag(text=text, hashtag=tag)
176
+
177
+
178
+ def bot_command(text: RichTextArg, command: str) -> types.RichTextBotCommand:
179
+ return types.RichTextBotCommand(text=text, bot_command=command)
180
+
181
+
182
+ def phone_number(text: RichTextArg, number: str) -> types.RichTextPhoneNumber:
183
+ return types.RichTextPhoneNumber(text=text, phone_number=number)
184
+
185
+
186
+ def email(text: RichTextArg, address: str) -> types.RichTextEmailAddress:
187
+ return types.RichTextEmailAddress(text=text, email_address=address)
188
+
189
+
190
+ def cashtag(text: RichTextArg, tag: str) -> types.RichTextCashtag:
191
+ return types.RichTextCashtag(text=text, cashtag=tag)
192
+
193
+
194
+ def bank_card(text: RichTextArg, number: str) -> types.RichTextBankCardNumber:
195
+ return types.RichTextBankCardNumber(text=text, bank_card_number=number)
196
+
197
+
198
+ def date_time(text: RichTextArg, date: datetime, fmt: str | None = None) -> types.RichTextDateTime:
199
+ """`fmt` is an optional display format string; omit to use Telegram's default."""
200
+ return types.RichTextDateTime(text=text, date=date, date_time_format=fmt)
201
+
202
+
203
+ def subscript(text: RichTextArg) -> types.RichTextSubscript:
204
+ return types.RichTextSubscript(text=text)
205
+
206
+
207
+ def superscript(text: RichTextArg) -> types.RichTextSuperscript:
208
+ return types.RichTextSuperscript(text=text)
209
+
210
+
211
+ def anchor_target(text: RichTextArg, name: str) -> types.RichTextAnchor:
212
+ """Marks `text` as a named jump-target (`name`) that `anchor_link()`
213
+ or `anchor_block()` can jump to within the same rich message."""
214
+ return types.RichTextAnchor(text=text, name=name)
215
+
216
+
217
+ def anchor_link(text: RichTextArg, anchor_name: str) -> types.RichTextAnchorLink:
218
+ """A clickable link that jumps to a target defined by `anchor_target()`
219
+ or `anchor_block()` with the same name."""
220
+ return types.RichTextAnchorLink(text=text, anchor_name=anchor_name)
221
+
222
+
223
+ def footnote_ref(text: RichTextArg, name: str) -> types.RichTextReference:
224
+ """Superscript footnote marker — pair with `footnote_link()` elsewhere
225
+ in the message to link back to this reference point."""
226
+ return types.RichTextReference(text=text, name=name)
227
+
228
+
229
+ def footnote_link(text: RichTextArg, reference_name: str) -> types.RichTextReferenceLink:
230
+ return types.RichTextReferenceLink(text=text, reference_name=reference_name)
231
+
232
+
233
+ def marked(text: RichTextArg) -> types.RichTextMarked:
234
+ """Highlighter-style marked/highlighted text."""
235
+ return types.RichTextMarked(text=text)
236
+
237
+
238
+ def math_inline(expression: str) -> types.RichTextMathematicalExpression:
239
+ """Inline LaTeX-style math, e.g. math_inline(r"x^2 + y^2 = z^2")."""
240
+ return types.RichTextMathematicalExpression(expression=expression)
241
+
242
+
243
+ def inline_button(button: "types.RichMessageButton") -> types.RichTextButton:
244
+ """Embeds a button (see `btn()`/`url_btn()` etc. below) inline within
245
+ running text instead of its own button row."""
246
+ return types.RichTextButton(button=button)
247
+
248
+
249
+ # ════════════════════════════════════════════════════════════════════
250
+ # BUTTONS
251
+ # ════════════════════════════════════════════════════════════════════
252
+ Style = enums.ButtonStyle # Style.DEFAULT / .PRIMARY / .DANGER / .SUCCESS / .LINK
253
+
254
+
255
+ def btn(text: RichTextArg, callback_data: str | bytes, style: enums.ButtonStyle = Style.DEFAULT) -> types.RichMessageButton:
256
+ """A standard callback button (handled by your @on_callback_query)."""
257
+ return types.RichMessageButton(text=text, callback_data=callback_data, style=style)
258
+
259
+
260
+ def url_btn(text: RichTextArg, url: str, style: enums.ButtonStyle = Style.LINK) -> types.RichMessageButton:
261
+ return types.RichMessageButton(text=text, url=url, style=style)
262
+
263
+
264
+ def webapp_btn(text: RichTextArg, url: str, style: enums.ButtonStyle = Style.DEFAULT) -> types.RichMessageButton:
265
+ """Opens a Telegram Mini App / web app."""
266
+ return types.RichMessageButton(text=text, web_app=types.WebAppInfo(url=url), style=style)
267
+
268
+
269
+ def login_btn(
270
+ text: RichTextArg, url: str, *,
271
+ forward_text: str | None = None, bot_username: str | None = None,
272
+ request_write_access: bool | None = None, style: enums.ButtonStyle = Style.DEFAULT,
273
+ ) -> types.RichMessageButton:
274
+ """Telegram Login Widget button."""
275
+ return types.RichMessageButton(
276
+ text=text, style=style,
277
+ login_url=types.LoginUrl(
278
+ url=url, forward_text=forward_text, bot_username=bot_username,
279
+ request_write_access=request_write_access,
280
+ ),
281
+ )
282
+
283
+
284
+ def switch_inline_btn(text: RichTextArg, query: str = "", style: enums.ButtonStyle = Style.DEFAULT) -> types.RichMessageButton:
285
+ """Prompts the user to pick another chat and opens your bot's inline
286
+ mode there with `query` pre-filled."""
287
+ return types.RichMessageButton(text=text, switch_inline_query=query, style=style)
288
+
289
+
290
+ def switch_inline_here_btn(text: RichTextArg, query: str = "", style: enums.ButtonStyle = Style.DEFAULT) -> types.RichMessageButton:
291
+ """Same as `switch_inline_btn()` but opens inline mode in the CURRENT chat."""
292
+ return types.RichMessageButton(text=text, switch_inline_query_current_chat=query, style=style)
293
+
294
+
295
+ def switch_inline_chosen_btn(
296
+ text: RichTextArg, *, query: str | None = None,
297
+ allow_user_chats: bool | None = None, allow_bot_chats: bool | None = None,
298
+ allow_group_chats: bool | None = None, allow_channel_chats: bool | None = None,
299
+ style: enums.ButtonStyle = Style.DEFAULT,
300
+ ) -> types.RichMessageButton:
301
+ """Like `switch_inline_btn()` but restricts which chat types the user
302
+ can pick (only relevant chat kinds are shown in the picker)."""
303
+ return types.RichMessageButton(
304
+ text=text, style=style,
305
+ switch_inline_query_chosen_chat=types.SwitchInlineQueryChosenChat(
306
+ query=query, allow_user_chats=allow_user_chats, allow_bot_chats=allow_bot_chats,
307
+ allow_group_chats=allow_group_chats, allow_channel_chats=allow_channel_chats,
308
+ ),
309
+ )
310
+
311
+
312
+ def copy_btn(text: RichTextArg, copy_text: str, style: enums.ButtonStyle = Style.DEFAULT) -> types.RichMessageButton:
313
+ """Tapping the button copies `copy_text` to the user's clipboard."""
314
+ return types.RichMessageButton(text=text, copy_text=types.CopyTextButton(text=copy_text), style=style)
315
+
316
+
317
+ def disabled_btn(text: RichTextArg, style: enums.ButtonStyle = Style.DEFAULT) -> types.RichMessageButton:
318
+ """A visually-present but non-interactive button (e.g. a status label)."""
319
+ return types.RichMessageButton(text=text, disabled=types.DisabledButton(), style=style)
320
+
321
+
322
+ def buttons(*btns: "types.RichMessageButton", align: str | None = None) -> types.InputRichBlockButtons:
323
+ """One row of up to 8 buttons. Call multiple times (one per row) inside
324
+ `message()`/`blocks_message()` to build a multi-row keyboard."""
325
+ assert 1 <= len(btns) <= 8, "a button row holds 1-8 buttons"
326
+ return types.InputRichBlockButtons(buttons=list(btns), align=align)
327
+
328
+
329
+ # ════════════════════════════════════════════════════════════════════
330
+ # MEDIA (for photo_block/video_block/etc. below — these are the
331
+ # *input* media wrappers, same PathType | BinaryIO | file_id you'd
332
+ # already pass to send_photo/send_video/...)
333
+ # ════════════════════════════════════════════════════════════════════
334
+ def photo_media(media, *, caption: str = "", has_spoiler: bool | None = None, **kw) -> types.InputMediaPhoto:
335
+ return types.InputMediaPhoto(media=media, caption=caption, has_spoiler=has_spoiler, **kw)
336
+
337
+
338
+ def video_media(media, *, caption: str = "", thumb=None, width=0, height=0, duration=0,
339
+ supports_streaming=True, has_spoiler: bool | None = None, **kw) -> types.InputMediaVideo:
340
+ return types.InputMediaVideo(
341
+ media=media, caption=caption, thumb=thumb, width=width, height=height,
342
+ duration=duration, supports_streaming=supports_streaming, has_spoiler=has_spoiler, **kw
343
+ )
344
+
345
+
346
+ def animation_media(media, *, caption: str = "", thumb=None, width=0, height=0, duration=0,
347
+ has_spoiler: bool | None = None, **kw) -> types.InputMediaAnimation:
348
+ return types.InputMediaAnimation(
349
+ media=media, caption=caption, thumb=thumb, width=width, height=height,
350
+ duration=duration, has_spoiler=has_spoiler, **kw
351
+ )
352
+
353
+
354
+ def audio_media(media, *, caption: str = "", thumb=None, duration=0, performer="", title="", **kw) -> types.InputMediaAudio:
355
+ return types.InputMediaAudio(media=media, caption=caption, thumb=thumb, duration=duration, performer=performer, title=title, **kw)
356
+
357
+
358
+ def document_media(media, *, caption: str = "", thumb=None, **kw) -> types.InputMediaDocument:
359
+ return types.InputMediaDocument(media=media, caption=caption, thumb=thumb, **kw)
360
+
361
+
362
+ def voice_media(media, *, caption: str = "", duration=0, **kw) -> types.InputMediaVoiceNote:
363
+ return types.InputMediaVoiceNote(media=media, caption=caption, duration=duration, **kw)
364
+
365
+
366
+ def caption(text: RichTextArg, credit: RichTextArg | None = None) -> types.RichBlockCaption:
367
+ """Rich (formattable) caption for photo_block/video_block/etc. —
368
+ `credit` renders as a smaller attribution line under the caption."""
369
+ return types.RichBlockCaption(text=text, credit=credit)
370
+
371
+
372
+ def named_media(id: str, media) -> types.InputRichMessageMedia:
373
+ """A named media attachment for `html_message()`/`markdown_message()`
374
+ (pass a list of these as `message(..., media=[...])`'s `media=` arg) —
375
+ lets your HTML/Markdown content reference it inline by `id` (e.g.
376
+ `<img src="{id}">` in HTML) instead of needing a separate block.
377
+ `media` is one of the `*_media()` builders above (photo_media(),
378
+ video_media(), etc.)."""
379
+ return types.InputRichMessageMedia(id=id, media=media)
380
+
381
+
382
+ # ════════════════════════════════════════════════════════════════════
383
+ # BLOCKS (wraps every InputRichBlock* type — the building blocks of
384
+ # a rich message body, used inside `message()`/`blocks_message()`)
385
+ # ════════════════════════════════════════════════════════════════════
386
+ def para(*parts: RichTextArg) -> types.InputRichBlockParagraph:
387
+ """A paragraph block — the workhorse block, equivalent to one line/
388
+ paragraph of a plain message. Pass one string, or several parts that
389
+ get concatenated: para("Hello ", bold(name), "!")."""
390
+ text = parts[0] if len(parts) == 1 else list(parts)
391
+ return types.InputRichBlockParagraph(text=text)
392
+
393
+
394
+ def heading(text: RichTextArg, size: int = 2) -> types.InputRichBlockSectionHeading:
395
+ """A section heading. `size` is 1 (largest) to ~4 (smallest)."""
396
+ return types.InputRichBlockSectionHeading(text=text, size=size)
397
+
398
+
399
+ def divider() -> types.InputRichBlockDivider:
400
+ return types.InputRichBlockDivider()
401
+
402
+
403
+ def footer(text: RichTextArg) -> types.InputRichBlockFooter:
404
+ """Small, muted footer line — good for 'Powered by ...' style credits."""
405
+ return types.InputRichBlockFooter(text=text)
406
+
407
+
408
+ def anchor_block(name: str) -> types.InputRichBlockAnchor:
409
+ """An invisible jump-target block — pair with `anchor_link()` text to
410
+ build a table-of-contents style message."""
411
+ return types.InputRichBlockAnchor(name=name)
412
+
413
+
414
+ def photo_block(media: "types.InputMediaPhoto", cap: "types.RichBlockCaption | None" = None) -> types.InputRichBlockPhoto:
415
+ return types.InputRichBlockPhoto(photo=media, caption=cap)
416
+
417
+
418
+ def video_block(media: "types.InputMediaVideo", cap: "types.RichBlockCaption | None" = None) -> types.InputRichBlockVideo:
419
+ return types.InputRichBlockVideo(video=media, caption=cap)
420
+
421
+
422
+ def animation_block(media: "types.InputMediaAnimation", cap: "types.RichBlockCaption | None" = None) -> types.InputRichBlockAnimation:
423
+ return types.InputRichBlockAnimation(animation=media, caption=cap)
424
+
425
+
426
+ def audio_block(media: "types.InputMediaAudio", cap: "types.RichBlockCaption | None" = None) -> types.InputRichBlockAudio:
427
+ return types.InputRichBlockAudio(audio=media, caption=cap)
428
+
429
+
430
+ def document_block(media: "types.InputMediaDocument", cap: "types.RichBlockCaption | None" = None) -> types.InputRichBlockDocument:
431
+ return types.InputRichBlockDocument(document=media, caption=cap)
432
+
433
+
434
+ def voice_block(media: "types.InputMediaVoiceNote", cap: "types.RichBlockCaption | None" = None) -> types.InputRichBlockVoiceNote:
435
+ return types.InputRichBlockVoiceNote(voice_note=media, caption=cap)
436
+
437
+
438
+ def collage(*blocks, cap: "types.RichBlockCaption | None" = None) -> types.InputRichBlockCollage:
439
+ """A grid of media blocks (photo_block/video_block/...) sharing one caption."""
440
+ return types.InputRichBlockCollage(blocks=list(blocks), caption=cap)
441
+
442
+
443
+ def slideshow(*blocks, cap: "types.RichBlockCaption | None" = None) -> types.InputRichBlockSlideshow:
444
+ """Like `collage()` but swipeable one-at-a-time instead of a grid."""
445
+ return types.InputRichBlockSlideshow(blocks=list(blocks), caption=cap)
446
+
447
+
448
+ def list_item(*blocks, has_checkbox: bool | None = None, is_checked: bool | None = None,
449
+ value: int | None = None, type: str | None = None) -> types.InputRichBlockListItem:
450
+ """One item of `bullet_list()`. Pass `has_checkbox=True` for a checklist
451
+ item, `type="ordered"` for numbered lists (default is bulleted)."""
452
+ return types.InputRichBlockListItem(blocks=list(blocks), has_checkbox=has_checkbox, is_checked=is_checked, value=value, type=type)
453
+
454
+
455
+ def bullet_list(*items: "types.InputRichBlockListItem") -> types.InputRichBlockList:
456
+ """Build items with `list_item()` first: bullet_list(list_item(para("A")), list_item(para("B")))."""
457
+ return types.InputRichBlockList(items=list(items))
458
+
459
+
460
+ def table_cell(text: RichTextArg | None = None, *, is_header: bool | None = None,
461
+ colspan: int | None = None, rowspan: int | None = None,
462
+ align: str | None = None, valign: str | None = None) -> types.RichBlockTableCell:
463
+ return types.RichBlockTableCell(text=text, is_header=is_header, colspan=colspan, rowspan=rowspan, align=align, valign=valign)
464
+
465
+
466
+ def table(rows: list[list["types.RichBlockTableCell"]], *, bordered: bool | None = None,
467
+ striped: bool | None = None, compact: bool | None = None,
468
+ cap: RichTextArg | None = None) -> types.InputRichBlockTable:
469
+ """`rows` is a list of rows, each a list of `table_cell(...)`."""
470
+ return types.InputRichBlockTable(cells=rows, is_bordered=bordered, is_striped=striped, is_compact=compact, caption=cap)
471
+
472
+
473
+ def details(summary: RichTextArg, *blocks, is_open: bool | None = None) -> types.InputRichBlockDetails:
474
+ """A collapsible <details>-style section: a `summary` line the user
475
+ taps to reveal the nested `blocks`."""
476
+ return types.InputRichBlockDetails(summary=summary, blocks=list(blocks), is_open=is_open)
477
+
478
+
479
+ def blockquote(*blocks, credit: RichTextArg | None = None) -> types.InputRichBlockBlockQuotation:
480
+ """A block-level quotation wrapping one or more nested blocks."""
481
+ return types.InputRichBlockBlockQuotation(blocks=list(blocks), credit=credit)
482
+
483
+
484
+ def expandable_quote(text: RichTextArg, credit: RichTextArg | None = None) -> types.InputRichBlockExpandableBlockQuotation:
485
+ """A quote block that starts collapsed with a 'Show more' toggle."""
486
+ return types.InputRichBlockExpandableBlockQuotation(text=text, credit=credit)
487
+
488
+
489
+ def pull_quote(text: RichTextArg, credit: RichTextArg | None = None) -> types.InputRichBlockPullQuotation:
490
+ """A visually-emphasized standalone quote (magazine-style pull-quote)."""
491
+ return types.InputRichBlockPullQuotation(text=text, credit=credit)
492
+
493
+
494
+ def preformatted(text: RichTextArg, language: str | None = None) -> types.InputRichBlockPreformatted:
495
+ """A full code block. `language` enables syntax highlighting (e.g. "python")."""
496
+ return types.InputRichBlockPreformatted(text=text, language=language)
497
+
498
+
499
+ def map_block(latitude: float, longitude: float, *, zoom: int | None = None,
500
+ width: int | None = None, height: int | None = None,
501
+ cap: "types.RichBlockCaption | None" = None) -> types.InputRichBlockMap:
502
+ return types.InputRichBlockMap(
503
+ location=types.Location(latitude=latitude, longitude=longitude),
504
+ zoom=zoom, width=width, height=height, caption=cap,
505
+ )
506
+
507
+
508
+ def math_block(expression: str) -> types.InputRichBlockMathematicalExpression:
509
+ """A standalone (block-level, centered) LaTeX-style expression."""
510
+ return types.InputRichBlockMathematicalExpression(expression=expression)
511
+
512
+
513
+ def thinking(text: RichTextArg) -> types.InputRichBlockThinking:
514
+ """Renders as a collapsed 'thought process' style block (chain-of-thought UI)."""
515
+ return types.InputRichBlockThinking(text=text)
516
+
517
+
518
+ # ════════════════════════════════════════════════════════════════════
519
+ # MESSAGE (wraps InputRichMessage construction)
520
+ # ════════════════════════════════════════════════════════════════════
521
+ def message(*blocks, media: list | None = None, rtl: bool | None = None,
522
+ skip_entity_detection: bool | None = None) -> types.InputRichMessage:
523
+ """The main way to build a rich message: message(para(...), divider(), buttons(...), ...)."""
524
+ return types.InputRichMessage(blocks=list(blocks), media=media, is_rtl=rtl, skip_entity_detection=skip_entity_detection)
525
+
526
+
527
+ # alias — `blocks_message` reads more explicitly next to `html_message`/`markdown_message`
528
+ blocks_message = message
529
+
530
+
531
+ def html_message(html: str, *, rtl: bool | None = None, skip_entity_detection: bool | None = None) -> types.InputRichMessage:
532
+ """Build straight from an HTML string instead of block-by-block."""
533
+ return types.InputRichMessage(html=html, is_rtl=rtl, skip_entity_detection=skip_entity_detection)
534
+
535
+
536
+ def markdown_message(md: str, *, rtl: bool | None = None, skip_entity_detection: bool | None = None) -> types.InputRichMessage:
537
+ """Build straight from a Markdown string instead of block-by-block."""
538
+ return types.InputRichMessage(markdown=md, is_rtl=rtl, skip_entity_detection=skip_entity_detection)
539
+
540
+
541
+ def text(content: RichTextArg) -> types.InputRichMessage:
542
+ """The rich-message equivalent of a plain `send_message(text)` call —
543
+ a single paragraph, no buttons. The most common case — use this unless
544
+ you actually need multiple blocks/buttons."""
545
+ return types.InputRichMessage(blocks=[types.InputRichBlockParagraph(text=content)])
546
+
547
+
548
+ # common alias some codebases expect
549
+ text_message = text
550
+
551
+
552
+ # ════════════════════════════════════════════════════════════════════
553
+ # PLAIN-TEXT FALLBACK RENDERING
554
+ # Best-effort flatten of a RichText tree / block list down to a plain
555
+ # string, for the automatic bot->user degrade in send()/edit()/reply()
556
+ # below, or for logging/previews. Formatting-only nodes (bold/italic/
557
+ # etc.) degrade to Pyrogram markdown syntax so *some* formatting still
558
+ # shows even in the plain fallback; structural nodes (media/table/map/
559
+ # math/...) degrade to a short bracketed label since there's no text
560
+ # equivalent for them.
561
+ # ════════════════════════════════════════════════════════════════════
562
+ def _flatten_richtext(node: Any) -> str:
563
+ if node is None:
564
+ return ""
565
+ if isinstance(node, str):
566
+ return node
567
+ if isinstance(node, (list, tuple)):
568
+ return "".join(_flatten_richtext(n) for n in node)
569
+
570
+ inner = _flatten_richtext(getattr(node, "text", None))
571
+ if isinstance(node, types.RichTextBold):
572
+ return f"**{inner}**"
573
+ if isinstance(node, types.RichTextItalic):
574
+ return f"__{inner}__"
575
+ if isinstance(node, types.RichTextUnderline):
576
+ return f"--{inner}--"
577
+ if isinstance(node, types.RichTextStrikethrough):
578
+ return f"~~{inner}~~"
579
+ if isinstance(node, types.RichTextSpoiler):
580
+ return f"||{inner}||"
581
+ if isinstance(node, types.RichTextCode):
582
+ return f"`{inner}`"
583
+ if isinstance(node, types.RichTextUrl):
584
+ return f"[{inner}]({node.url})"
585
+ if isinstance(node, types.RichTextTextMention):
586
+ return f"[{inner}](tg://user?id={node.user.id})"
587
+ if isinstance(node, types.RichTextMention):
588
+ return f"{inner} (@{node.username})" if inner and inner != node.username else f"@{node.username}"
589
+ if isinstance(node, types.RichTextCustomEmoji):
590
+ return node.alternative_text or ""
591
+ if isinstance(node, (types.RichTextHashtag, types.RichTextBotCommand, types.RichTextPhoneNumber,
592
+ types.RichTextEmailAddress, types.RichTextCashtag, types.RichTextBankCardNumber,
593
+ types.RichTextSubscript, types.RichTextSuperscript, types.RichTextMarked,
594
+ types.RichTextAnchor, types.RichTextAnchorLink, types.RichTextReference,
595
+ types.RichTextReferenceLink, types.RichTextDateTime)):
596
+ return inner
597
+ if isinstance(node, types.RichTextMathematicalExpression):
598
+ return node.expression
599
+ if isinstance(node, types.RichTextButton):
600
+ return f"[{_flatten_richtext(getattr(node.button, 'text', ''))}]"
601
+ return inner or str(node)
602
+
603
+
604
+ def _flatten_block(block: Any) -> str:
605
+ if isinstance(block, types.InputRichBlockParagraph):
606
+ return _flatten_richtext(block.text)
607
+ if isinstance(block, types.InputRichBlockFooter):
608
+ return _flatten_richtext(block.text)
609
+ if isinstance(block, types.InputRichBlockSectionHeading):
610
+ return _flatten_richtext(block.text).upper()
611
+ if isinstance(block, types.InputRichBlockDivider):
612
+ return "───────────"
613
+ if isinstance(block, types.InputRichBlockPreformatted):
614
+ return f"```{block.language or ''}\n{_flatten_richtext(block.text)}\n```"
615
+ if isinstance(block, (types.InputRichBlockPullQuotation, types.InputRichBlockExpandableBlockQuotation)):
616
+ return f"> {_flatten_richtext(block.text)}"
617
+ if isinstance(block, types.InputRichBlockBlockQuotation):
618
+ return "\n".join(f"> {line}" for line in "\n".join(_flatten_block(b) for b in block.blocks).splitlines())
619
+ if isinstance(block, types.InputRichBlockDetails):
620
+ inner = "\n".join(_flatten_block(b) for b in block.blocks)
621
+ return f"{_flatten_richtext(block.summary)}\n{inner}"
622
+ if isinstance(block, types.InputRichBlockList):
623
+ lines = []
624
+ for item in block.items:
625
+ mark = "☑" if item.is_checked else ("☐" if item.has_checkbox else "•")
626
+ lines.append(f"{mark} " + " ".join(_flatten_block(b) for b in item.blocks))
627
+ return "\n".join(lines)
628
+ if isinstance(block, types.InputRichBlockButtons):
629
+ return " | ".join(f"[{_flatten_richtext(b.text)}]" for b in block.buttons)
630
+ if isinstance(block, types.InputRichBlockAnchor):
631
+ return ""
632
+ if isinstance(block, types.InputRichBlockMathematicalExpression):
633
+ return block.expression
634
+ if isinstance(block, (types.InputRichBlockPhoto, types.InputRichBlockVideo, types.InputRichBlockAnimation,
635
+ types.InputRichBlockAudio, types.InputRichBlockDocument, types.InputRichBlockVoiceNote,
636
+ types.InputRichBlockCollage, types.InputRichBlockSlideshow, types.InputRichBlockMap)):
637
+ label = type(block).__name__.replace("InputRichBlock", "")
638
+ cap = getattr(block, "caption", None)
639
+ cap_text = f": {_flatten_richtext(cap.text)}" if cap else ""
640
+ return f"[{label}{cap_text}]"
641
+ if isinstance(block, types.InputRichBlockThinking):
642
+ return f"[Thinking] {_flatten_richtext(block.text)}"
643
+ if isinstance(block, types.InputRichBlockTable):
644
+ rows = [" | ".join(_flatten_richtext(c.text) for c in row) for row in block.cells]
645
+ return "\n".join(rows)
646
+ return ""
647
+
648
+
649
+ def flatten_message(rich_message: "types.InputRichMessage") -> str:
650
+ """Best-effort plain-text rendering of an InputRichMessage — used
651
+ automatically by send()/edit()/reply() below when the sending client
652
+ is a user account (Rich Messages don't render for those). Pass your
653
+ own `fallback_text=` to those calls instead whenever you want a
654
+ hand-written plain version rather than this generic flatten."""
655
+ if rich_message.markdown:
656
+ return rich_message.markdown
657
+ if rich_message.html:
658
+ return rich_message.html # best-effort — no HTML stripping, shown as-is
659
+ lines = [_flatten_block(b) for b in (rich_message.blocks or [])]
660
+ return "\n".join(line for line in lines if line)
661
+
662
+
663
+ # ════════════════════════════════════════════════════════════════════
664
+ # SEND / EDIT / REPLY — the easy, safe call surface.
665
+ # Handles the two footguns from the module docstring automatically:
666
+ # bot-only rich messages, and numeric-string chat_id misdetection.
667
+ # ════════════════════════════════════════════════════════════════════
668
+ def _safe_chat_id(chat_id):
669
+ """int-ify a numeric chat_id string (e.g. str(chat.id) from elsewhere
670
+ in a codebase) so kurigram's resolve_peer() can't mistake it for a
671
+ phone number. Usernames / "@handle" strings pass through untouched."""
672
+ if isinstance(chat_id, str):
673
+ s = chat_id.strip()
674
+ digits = s[1:] if s.startswith("-") else s
675
+ if digits.isdigit():
676
+ return int(s)
677
+ return chat_id
678
+
679
+
680
+ async def is_bot_client(client) -> bool:
681
+ """True if `client` is logged in as a bot account. Rich Messages only
682
+ render for bots — see the module docstring. Uses the cached
683
+ `client.me` (set once at Client.start()); fetches it if not cached yet."""
684
+ try:
685
+ if client.me is None:
686
+ await client.get_me()
687
+ return bool(client.me and client.me.is_bot)
688
+ except Exception:
689
+ return False
690
+
691
+
692
+ async def send(
693
+ client, chat_id, msg: "types.InputRichMessage", *,
694
+ fallback_text: str | None = None, is_bot: bool | None = None,
695
+ reply_markup=None, disable_notification: bool | None = None,
696
+ reply_parameters=None, effect_id: int | None = None,
697
+ ):
698
+ """Send a rich message. Auto-degrades to plain text if `client` isn't
699
+ a bot account, and auto-fixes a numeric-string `chat_id`. Pass
700
+ `is_bot=True/False` yourself to skip the `client.me` check (e.g. if
701
+ you already know it, for a hot loop); pass `fallback_text=` for a
702
+ hand-written plain version instead of the generic flatten."""
703
+ cid = _safe_chat_id(chat_id)
704
+ bot = is_bot if is_bot is not None else await is_bot_client(client)
705
+ if bot:
706
+ return await client.send_rich_message(
707
+ cid, rich_message=msg, reply_markup=reply_markup,
708
+ disable_notification=disable_notification,
709
+ reply_parameters=reply_parameters, effect_id=effect_id,
710
+ )
711
+ return await client.send_message(
712
+ cid, fallback_text if fallback_text is not None else flatten_message(msg),
713
+ reply_markup=reply_markup, disable_notification=disable_notification,
714
+ reply_parameters=reply_parameters,
715
+ )
716
+
717
+
718
+ async def edit(
719
+ client, chat_id, message_id: int, msg: "types.InputRichMessage", *,
720
+ fallback_text: str | None = None, is_bot: bool | None = None, reply_markup=None,
721
+ ):
722
+ """Edit a message in place with new rich content. Same auto-degrade /
723
+ auto-int-ify guarantees as send()."""
724
+ cid = _safe_chat_id(chat_id)
725
+ bot = is_bot if is_bot is not None else await is_bot_client(client)
726
+ if bot:
727
+ return await client.edit_message_text(cid, message_id, rich_message=msg, reply_markup=reply_markup)
728
+ return await client.edit_message_text(
729
+ cid, message_id, fallback_text if fallback_text is not None else flatten_message(msg),
730
+ reply_markup=reply_markup,
731
+ )
732
+
733
+
734
+ async def reply(
735
+ message_obj: "types.Message", msg: "types.InputRichMessage", *,
736
+ fallback_text: str | None = None, is_bot: bool | None = None, reply_markup=None,
737
+ disable_notification: bool | None = None,
738
+ ):
739
+ """Reply to an incoming Message, using its own client automatically
740
+ (message_obj._client — the same internal attribute Message.reply_text/
741
+ reply_rich use). Same auto-degrade guarantee as send()."""
742
+ client = message_obj._client
743
+ bot = is_bot if is_bot is not None else await is_bot_client(client)
744
+ if bot:
745
+ return await message_obj.reply_rich(
746
+ rich_message=msg, reply_markup=reply_markup, disable_notification=disable_notification,
747
+ )
748
+ return await message_obj.reply_text(
749
+ fallback_text if fallback_text is not None else flatten_message(msg),
750
+ reply_markup=reply_markup, disable_notification=disable_notification,
751
+ )
752
+
753
+
754
+ async def quick(client, chat_id, plain_text: str, **kw):
755
+ """The single-line shortcut for the common case: one line of text, no
756
+ blocks/buttons to compose by hand. Equivalent to
757
+ send(client, chat_id, text(plain_text)), skipping message()/text() entirely."""
758
+ return await send(client, chat_id, text(plain_text), fallback_text=plain_text, **kw)
759
+
760
+
761
+ async def quick_edit(client, chat_id, message_id: int, plain_text: str, **kw):
762
+ return await edit(client, chat_id, message_id, text(plain_text), fallback_text=plain_text, **kw)
richpyro/py.typed ADDED
File without changes
@@ -0,0 +1,144 @@
1
+ Metadata-Version: 2.4
2
+ Name: richpyro
3
+ Version: 1.0.0
4
+ Summary: A complete, easy-to-call wrapper for Pyrogram Rich Messages (Bot API 10.1+) — every block, every text style, every button type, with bot/user-account fallback and chat_id safety built in.
5
+ Author: devgagan
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/devgaganin/richpyro
8
+ Project-URL: Repository, https://github.com/devgaganin/richpyro
9
+ Project-URL: Issues, https://github.com/devgaganin/richpyro/issues
10
+ Keywords: telegram,pyrogram,kurigram,telegram-bot,bot-api,rich-message,rich-messages,telegram-api
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Communications :: Chat
21
+ Classifier: Topic :: Internet
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.9
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: Kurigram>=2.2.26
28
+ Dynamic: license-file
29
+
30
+ # richpyro
31
+
32
+ A complete, easy-to-call wrapper around [kurigram](https://github.com/kurimuzon/kurigram)'s
33
+ **Rich Messages** (Telegram Bot API 10.1+ `InputRichMessage` / `InputRichBlock*` /
34
+ `RichText*`, plus `Client.send_rich_message`, `Message.edit_text(rich_message=...)`
35
+ and `Message.reply_rich`).
36
+
37
+ Every block type, every text style, every button type — one flat, documented
38
+ function per thing, plus a safe `send()` / `edit()` / `reply()` call surface
39
+ that handles two real Telegram/kurigram footguns automatically so you never
40
+ have to think about them.
41
+
42
+ ```bash
43
+ pip install richpyro
44
+ ```
45
+
46
+ ## Why
47
+
48
+ Rich Messages are powerful but the raw API is deep (~50 constructor types)
49
+ and has two sharp edges that fail silently in production:
50
+
51
+ 1. **Rich Messages only render for bot accounts.** Send `rich_message=...`
52
+ from a real user/userbot client (a logged-in session, not a bot token)
53
+ and Telegram silently drops or rejects it — no visible error, your
54
+ progress bar / status card just stops updating.
55
+ 2. **A numeric-string `chat_id` looks like a phone number to kurigram.**
56
+ `Client.resolve_peer()` strips `+()-` and whitespace from any string
57
+ `chat_id`; if what's left is all digits, it assumes it's a phone number
58
+ and calls `contacts.ResolvePhone` — a method **bot accounts cannot call
59
+ at all** (`[400 BOT_METHOD_INVALID]`), and which fails with
60
+ `PHONE_NOT_OCCUPIED` for user accounts too unless that string happens to
61
+ be someone's real registered phone number. This bites silently any time
62
+ a chat id is passed as `str(chat.id)` instead of `int` — an extremely
63
+ easy mistake, since chat ids get stringified for DB keys/dict lookups
64
+ all over a typical bot codebase and then get reused as-is for an API call.
65
+
66
+ `richpyro.send()` / `.edit()` / `.reply()` check `client.me.is_bot` and
67
+ auto-degrade to an equivalent plain-text message for user-account senders,
68
+ and auto-int-ify any numeric-string `chat_id` before it reaches kurigram
69
+ (`@usernames` pass through untouched). You get both fixes for free just by
70
+ using this library's call surface instead of the raw kurigram methods.
71
+
72
+ ## Quick start
73
+
74
+ ```python
75
+ import richpyro as rp
76
+
77
+ # simplest possible call — one line, works from a bot OR a user client:
78
+ await rp.quick(client, chat_id, "Processing your request...")
79
+
80
+ # a structured card with formatting + buttons:
81
+ card = rp.message(
82
+ rp.para("Hello ", rp.bold(name), "! Choose a plan:"),
83
+ rp.divider(),
84
+ rp.para("● ", rp.bold("Day Plan"), " — ₹20 (24 hours)"),
85
+ rp.buttons(
86
+ rp.btn("Day Plan ₹20", "pay_day"),
87
+ rp.url_btn("Learn more", "https://example.com"),
88
+ ),
89
+ )
90
+ sent = await rp.send(client, chat_id, card)
91
+ ...
92
+ await rp.edit(client, chat_id, sent.id, rp.text("Payment confirmed."))
93
+
94
+ # replying directly to an incoming message (uses its own client):
95
+ await rp.reply(message, card)
96
+ ```
97
+
98
+ Every builder returns a plain kurigram object — you can always drop down to
99
+ raw `types.InputRichMessage(...)` etc. for anything this library doesn't
100
+ wrap yet. richpyro objects and raw kurigram objects mix freely; nothing here
101
+ subclasses kurigram, it only constructs.
102
+
103
+ ## What's covered
104
+
105
+ **Text formatting** — `bold`, `italic`, `underline`, `strike`, `spoiler`,
106
+ `code`, `link`, `user_mention`, `mention`, `custom_emoji`, `hashtag`,
107
+ `bot_command`, `phone_number`, `email`, `cashtag`, `bank_card`, `date_time`,
108
+ `subscript`, `superscript`, `anchor_target`, `anchor_link`, `footnote_ref`,
109
+ `footnote_link`, `marked`, `math_inline`, `inline_button`.
110
+
111
+ **Buttons** — `btn` (callback), `url_btn`, `webapp_btn`, `login_btn`,
112
+ `switch_inline_btn`, `switch_inline_here_btn`, `switch_inline_chosen_btn`,
113
+ `copy_btn`, `disabled_btn`, plus `buttons(...)` to lay out a row and
114
+ `Style` (`DEFAULT` / `PRIMARY` / `DANGER` / `SUCCESS` / `LINK`).
115
+
116
+ **Blocks** — `para`, `heading`, `divider`, `footer`, `anchor_block`,
117
+ `photo_block`, `video_block`, `animation_block`, `audio_block`,
118
+ `document_block`, `voice_block` (with matching `*_media` input helpers and
119
+ `caption(...)`), `collage`, `slideshow`, `list_item` / `bullet_list`,
120
+ `table_cell` / `table`, `details`, `blockquote`, `expandable_quote`,
121
+ `pull_quote`, `preformatted`, `map_block`, `math_block`, `thinking`.
122
+
123
+ **Message assembly** — `message(*blocks)` (the main entry point),
124
+ `html_message`, `markdown_message`, `text(...)` (the single-line shortcut
125
+ for the common case).
126
+
127
+ **Safe send surface** — `send`, `edit`, `reply`, `quick`, `quick_edit`,
128
+ `is_bot_client`, and `flatten_message` (the plain-text renderer used
129
+ automatically for the bot → user-account fallback, also handy standalone
130
+ for logs/previews).
131
+
132
+ ## Requirements
133
+
134
+ - Python 3.9+
135
+ - [Kurigram](https://pypi.org/project/Kurigram/) 2.2.26+ (the actively
136
+ maintained Pyrogram fork with Rich Messages support — still `import pyrogram`)
137
+
138
+ ## License
139
+
140
+ MIT — see [LICENSE](LICENSE).
141
+
142
+ ## Author
143
+
144
+ coded by **devgagan** — [github.com/devgaganin](https://github.com/devgaganin)
@@ -0,0 +1,7 @@
1
+ richpyro/__init__.py,sha256=B49ydcZjC-Ey6jpnY-XWdJT8jaaqYD7v-uzvkVk6o10,39064
2
+ richpyro/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ richpyro-1.0.0.dist-info/licenses/LICENSE,sha256=vGQnZGz_0vhLg1MAnpwwy7BgdA_Q_RebjCE38vKovL4,1065
4
+ richpyro-1.0.0.dist-info/METADATA,sha256=OsWv14Wb5vupDSI456vO5Rcni0krTz2N1mhFJ_lXokc,6264
5
+ richpyro-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
6
+ richpyro-1.0.0.dist-info/top_level.txt,sha256=KKmyQQPgYtJyuEyrOikG4p7rjfmU2QaHlULD50HEW8Y,9
7
+ richpyro-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 devgagan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ richpyro