gram-tools


Namegram-tools JSON
Version 1.0.2 PyPI version JSON
download
home_pagehttps://github.com/DedInc/gram-tools
SummaryUtilities for streamlined aiogram bot development.
upload_time2024-11-09 11:24:44
maintainerNone
docs_urlNone
authorMaehdakvan
requires_python>=3.6
licenseNone
keywords
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # gram_tools

Utilities for streamlined [aiogram 3](https://github.com/aiogram/aiogram) bot development.

## Features

- 📦 **Message Packing**: Serialize and deserialize Telegram messages with support for all media types
- 🗄️ **Asset Management**: Efficient handling of media files with caching and file_id reuse
- 📚 **CRUD Operations**: Generic async CRUD operations for SQLAlchemy models
- ⌨️ **Pagination**: Flexible inline keyboard pagination with search capability
- 🔧 **Template System**: Simple template system for message text formatting

## Installation

```bash
pip install gram-tools
```

## Components

### Message Packer

Pack and unpack Telegram messages with all their content for easy storage and reuse.

```python
from gram_tools.packer import pack_message, unpack_message, send_packed_message, answer_packed_message

# Pack a message
packed = await pack_message(message)

# Send packed message
await send_packed_message(bot, chat_id, packed)

# Answer with packed message
await answer_packed_message(message, packed)
```

### Whole example

```python
import asyncio
from aiogram import Bot, Dispatcher, Router, F
from aiogram.types import Message
from aiogram.filters import Command

from gram_tools.packer import (
    pack_message,
    send_packed_message,
)

API_TOKEN = "BOT TOKEN"

bot = Bot(token=API_TOKEN)
dp = Dispatcher()
router = Router()

# In-memory storage for packed messages
packed_messages = {}

@router.message(Command("save"))
async def save_message(message: Message):
    if not message.reply_to_message:
        await message.answer("Please reply to the message you want to save.")
        return

    # Pack the replied message
    packed = await pack_message(message.reply_to_message)

    # Store the packed message using user's chat ID
    packed_messages[message.from_user.id] = packed
    await message.answer("Message saved!")

@router.message(Command("send"))
async def send_saved_message(message: Message):
    packed = packed_messages.get(message.from_user.id)
    if not packed:
        await message.answer("No message saved. Use /save to save a message.")
        return

    # Send the packed message back to the user
    await send_packed_message(bot, message.chat.id, packed)

async def main():
    dp.include_router(router)
    await dp.start_polling(bot)

if __name__ == "__main__":
    asyncio.run(main())
```

### Asset Management

Efficiently handle media files with automatic file_id caching.

```python
from gram_tools.assets import answer_with_asset, send_with_asset

# Send media file
await send_with_asset(bot, chat_id, "path/to/file.jpg", caption="My photo") # also u can without caption, and also you can use reply_markup and more

# Answer with media file
await answer_with_asset(message, "path/to/file.mp4")
```

### Whole example how to use

```python
import asyncio
from aiogram import Bot, Dispatcher, Router, F
from aiogram.types import Message
from aiogram.filters import Command

from gram_tools.assets import send_with_asset, answer_with_asset

API_TOKEN = "BOT TOKEN"

bot = Bot(token=API_TOKEN)
dp = Dispatcher()
router = Router()

@router.message(Command("photo"))
async def send_photo(message: Message):
    await send_with_asset(
        bot,
        chat_id=message.chat.id,
        file_path=r"D:\Dokumenti\Pictures\photo_2024-08-11_18-25-01.jpg",
        caption="Here is a photo!"
    )

@router.message(Command("video"))
async def send_video(message: Message):
    await answer_with_asset(
        message=message,
        file_path=r"D:\Dokumenti\Documents\Untitled.mp4",
        caption="Here is a video!"
    )

async def main():
    dp.include_router(router)
    await dp.start_polling(bot)

if __name__ == "__main__":
    asyncio.run(main())

```

Supported media types:
- Photos: jpg, jpeg, png, svg, webp, bmp, jfif, heic, heif
- Videos: mp4, mov, avi, mkv, m4v, 3gp
- Audio: mp3, wav, flac, m4a, ogg, aac
- Voice: ogg, oga
- Animations: gif
- Any other file type as document

### CRUD Operations

Generic CRUD operations for SQLAlchemy models.

```python
from gram_tools.crud import get_crud
from sqlalchemy import Column, Integer, String

class User(Base):
    __tablename__ = "users"

    id = Column(Integer, primary_key=True, index=True)
    telegram_id = Column(Integer, unique=True, index=True)
    name = Column(String)


user_crud = get_crud(User)

# Add new instance
await user_crud.add(session, user)

# Get instance
user = await user_crud.get(session, id=1)

# Get all instances
active_users = await user_crud.get_all(session, status="active")

# Get count of instances
active_users_count = await user_crud.get_all_count(session, status="active")

# Update instance
await user_crud.update(session, user, name="New Name")

# Delete instance
await user_crud.delete(session, user)
```

### Pagination Keyboards

Create paginated inline keyboards with optional search functionality.

```python
from gram_tools.keyboards import InlineBuilder, SearchButton

# Create pagination with search
builder = InlineBuilder(
    per_page=5,
    layout=1,
    search_button=SearchButton("🔍")
)

items = ["Item 1", "Item 2", "Item 3", ...]
keyboard = builder.get_paginated(items, page=1)

# With search
keyboard = builder.get_paginated(items, page=1, search_term="search query")
```

Customization options:
- `per_page`: Items per page
- `layout`: Number of buttons per row
- `next_button_text`: Next page button text
- `prev_button_text`: Previous page button text
- `page_callback_prefix`: Optional callback prefix of core paginator and item selection
- `ignore_callback_prefix`: Optional callback prefox on click ignore items
- `not_exist_page`: Text for disabled navigation buttons
- `search_button`: Optional search button configuration

### Whole example:

```python
import asyncio
from aiogram import Bot, Dispatcher, Router, F
from aiogram.types import CallbackQuery, Message
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import StatesGroup, State

from gram_tools.keyboards import (
    InlineBuilder,
    SearchButton
)

API_TOKEN = "7350010616:AAHQXoWzmMuffBkv5mPAI2-cBziZBkvoTI8"

bot = Bot(token=API_TOKEN)
dp = Dispatcher()
router = Router()

items = [f"Item {i}" for i in range(1, 21)]
search_button = SearchButton(button_text="🔍 Search")
inline_builder = InlineBuilder(per_page=5, search_button=search_button)

class Form(StatesGroup):
    waiting_for_search_term = State()

@router.message(Command("start"))
async def show_inline_keyboard(message: Message, state: FSMContext):
    await state.clear()
    keyboard = inline_builder.get_paginated(items, page=1)
    await message.answer("Select an item:", reply_markup=keyboard)

@router.callback_query(inline_builder.page_callback.filter())
async def handle_callback(callback: CallbackQuery, callback_data: inline_builder.page_callback, state: FSMContext):
    action = callback_data.action
    value = callback_data.value

    data = await state.get_data()
    search_term = data.get('search_term')

    if action in ("next", "prev"):
        page = value
        keyboard = inline_builder.get_paginated(items, page=page, search_term=search_term)
        await callback.message.edit_reply_markup(reply_markup=keyboard)
        await callback.answer()
    elif action == "sel":
        item_index = value
        if search_term:
            filtered_items = [item for item in items if search_term.lower() in str(item).lower()]
        else:
            filtered_items = items

        if 0 <= item_index < len(filtered_items):
            selected_item = filtered_items[item_index]
            await callback.message.answer(f"You selected: {selected_item}")
        else:
            await callback.message.answer("Element not found.")
        await callback.answer()

@router.callback_query(search_button.search_callback.filter())
async def handle_search_callback(callback: CallbackQuery, state: FSMContext):
    print(callback.data)
    await state.update_data(search_term=None)
    await callback.message.answer("Type text to search:")
    await state.set_state(Form.waiting_for_search_term)
    await callback.answer()

@router.message(Form.waiting_for_search_term)
async def perform_search(message: Message, state: FSMContext):
    search_term = message.text
    await state.update_data(search_term=search_term)
    await state.set_state(None)

    keyboard = inline_builder.get_paginated(items, page=1, search_term=search_term)

    if keyboard.inline_keyboard:
        await message.answer("Results:", reply_markup=keyboard)
    else:
        await message.answer("Nothing found.")

@router.callback_query(inline_builder.ignore_callback.filter())
async def handle_ignore_callback(callback: CallbackQuery):
    await callback.answer()

async def main():
    dp.include_router(router)
    await dp.start_polling(bot)

if __name__ == "__main__":
    asyncio.run(main())
```

### Template System

Simple template system for message text formatting.

```python
from gram_tools.messages import T

template = T("Hello, ${name}!")
text = template.text(name="User")
```

### Whole example

```python
import asyncio
from aiogram import Bot, Dispatcher, Router, F
from aiogram.types import Message
from aiogram.filters import Command

from gram_tools.messages import T

API_TOKEN = "BOT TOKEN"

bot = Bot(token=API_TOKEN)
dp = Dispatcher()
router = Router()

# Define a template
welcome_template = T("Hello, ${name}! Welcome to our bot.")

@router.message(Command("start"))
async def send_welcome(message: Message):
    # Render the template with the user's name
    text = welcome_template.text(name=message.from_user.full_name)
    await message.answer(text)

@router.message(Command("info"))
async def send_info(message: Message):
    info_template = T(
        "User Info:\n"
        "- ID: ${id}\n"
        "- Username: ${username}\n"
        "- First Name: ${first_name}\n"
        "- Last Name: ${last_name}"
    )
    text = info_template.text(
        id=message.from_user.id,
        username=message.from_user.username or "N/A",
        first_name=message.from_user.first_name or "N/A",
        last_name=message.from_user.last_name or "N/A"
    )
    await message.answer(text)

async def main():
    dp.include_router(router)
    await dp.start_polling(bot)

if __name__ == "__main__":
    asyncio.run(main())
```

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/DedInc/gram-tools",
    "name": "gram-tools",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.6",
    "maintainer_email": null,
    "keywords": null,
    "author": "Maehdakvan",
    "author_email": "visitanimation@google.com",
    "download_url": "https://files.pythonhosted.org/packages/66/5b/a3b1ab3a02cfa87d5b2300b423797c64ed55805a9f50c93a346be8892ae3/gram_tools-1.0.2.tar.gz",
    "platform": null,
    "description": "# gram_tools\r\n\r\nUtilities for streamlined [aiogram 3](https://github.com/aiogram/aiogram) bot development.\r\n\r\n## Features\r\n\r\n- \ud83d\udce6 **Message Packing**: Serialize and deserialize Telegram messages with support for all media types\r\n- \ud83d\uddc4\ufe0f **Asset Management**: Efficient handling of media files with caching and file_id reuse\r\n- \ud83d\udcda **CRUD Operations**: Generic async CRUD operations for SQLAlchemy models\r\n- \u2328\ufe0f **Pagination**: Flexible inline keyboard pagination with search capability\r\n- \ud83d\udd27 **Template System**: Simple template system for message text formatting\r\n\r\n## Installation\r\n\r\n```bash\r\npip install gram-tools\r\n```\r\n\r\n## Components\r\n\r\n### Message Packer\r\n\r\nPack and unpack Telegram messages with all their content for easy storage and reuse.\r\n\r\n```python\r\nfrom gram_tools.packer import pack_message, unpack_message, send_packed_message, answer_packed_message\r\n\r\n# Pack a message\r\npacked = await pack_message(message)\r\n\r\n# Send packed message\r\nawait send_packed_message(bot, chat_id, packed)\r\n\r\n# Answer with packed message\r\nawait answer_packed_message(message, packed)\r\n```\r\n\r\n### Whole example\r\n\r\n```python\r\nimport asyncio\r\nfrom aiogram import Bot, Dispatcher, Router, F\r\nfrom aiogram.types import Message\r\nfrom aiogram.filters import Command\r\n\r\nfrom gram_tools.packer import (\r\n    pack_message,\r\n    send_packed_message,\r\n)\r\n\r\nAPI_TOKEN = \"BOT TOKEN\"\r\n\r\nbot = Bot(token=API_TOKEN)\r\ndp = Dispatcher()\r\nrouter = Router()\r\n\r\n# In-memory storage for packed messages\r\npacked_messages = {}\r\n\r\n@router.message(Command(\"save\"))\r\nasync def save_message(message: Message):\r\n    if not message.reply_to_message:\r\n        await message.answer(\"Please reply to the message you want to save.\")\r\n        return\r\n\r\n    # Pack the replied message\r\n    packed = await pack_message(message.reply_to_message)\r\n\r\n    # Store the packed message using user's chat ID\r\n    packed_messages[message.from_user.id] = packed\r\n    await message.answer(\"Message saved!\")\r\n\r\n@router.message(Command(\"send\"))\r\nasync def send_saved_message(message: Message):\r\n    packed = packed_messages.get(message.from_user.id)\r\n    if not packed:\r\n        await message.answer(\"No message saved. Use /save to save a message.\")\r\n        return\r\n\r\n    # Send the packed message back to the user\r\n    await send_packed_message(bot, message.chat.id, packed)\r\n\r\nasync def main():\r\n    dp.include_router(router)\r\n    await dp.start_polling(bot)\r\n\r\nif __name__ == \"__main__\":\r\n    asyncio.run(main())\r\n```\r\n\r\n### Asset Management\r\n\r\nEfficiently handle media files with automatic file_id caching.\r\n\r\n```python\r\nfrom gram_tools.assets import answer_with_asset, send_with_asset\r\n\r\n# Send media file\r\nawait send_with_asset(bot, chat_id, \"path/to/file.jpg\", caption=\"My photo\") # also u can without caption, and also you can use reply_markup and more\r\n\r\n# Answer with media file\r\nawait answer_with_asset(message, \"path/to/file.mp4\")\r\n```\r\n\r\n### Whole example how to use\r\n\r\n```python\r\nimport asyncio\r\nfrom aiogram import Bot, Dispatcher, Router, F\r\nfrom aiogram.types import Message\r\nfrom aiogram.filters import Command\r\n\r\nfrom gram_tools.assets import send_with_asset, answer_with_asset\r\n\r\nAPI_TOKEN = \"BOT TOKEN\"\r\n\r\nbot = Bot(token=API_TOKEN)\r\ndp = Dispatcher()\r\nrouter = Router()\r\n\r\n@router.message(Command(\"photo\"))\r\nasync def send_photo(message: Message):\r\n    await send_with_asset(\r\n        bot,\r\n        chat_id=message.chat.id,\r\n        file_path=r\"D:\\Dokumenti\\Pictures\\photo_2024-08-11_18-25-01.jpg\",\r\n        caption=\"Here is a photo!\"\r\n    )\r\n\r\n@router.message(Command(\"video\"))\r\nasync def send_video(message: Message):\r\n    await answer_with_asset(\r\n        message=message,\r\n        file_path=r\"D:\\Dokumenti\\Documents\\Untitled.mp4\",\r\n        caption=\"Here is a video!\"\r\n    )\r\n\r\nasync def main():\r\n    dp.include_router(router)\r\n    await dp.start_polling(bot)\r\n\r\nif __name__ == \"__main__\":\r\n    asyncio.run(main())\r\n\r\n```\r\n\r\nSupported media types:\r\n- Photos: jpg, jpeg, png, svg, webp, bmp, jfif, heic, heif\r\n- Videos: mp4, mov, avi, mkv, m4v, 3gp\r\n- Audio: mp3, wav, flac, m4a, ogg, aac\r\n- Voice: ogg, oga\r\n- Animations: gif\r\n- Any other file type as document\r\n\r\n### CRUD Operations\r\n\r\nGeneric CRUD operations for SQLAlchemy models.\r\n\r\n```python\r\nfrom gram_tools.crud import get_crud\r\nfrom sqlalchemy import Column, Integer, String\r\n\r\nclass User(Base):\r\n    __tablename__ = \"users\"\r\n\r\n    id = Column(Integer, primary_key=True, index=True)\r\n    telegram_id = Column(Integer, unique=True, index=True)\r\n    name = Column(String)\r\n\r\n\r\nuser_crud = get_crud(User)\r\n\r\n# Add new instance\r\nawait user_crud.add(session, user)\r\n\r\n# Get instance\r\nuser = await user_crud.get(session, id=1)\r\n\r\n# Get all instances\r\nactive_users = await user_crud.get_all(session, status=\"active\")\r\n\r\n# Get count of instances\r\nactive_users_count = await user_crud.get_all_count(session, status=\"active\")\r\n\r\n# Update instance\r\nawait user_crud.update(session, user, name=\"New Name\")\r\n\r\n# Delete instance\r\nawait user_crud.delete(session, user)\r\n```\r\n\r\n### Pagination Keyboards\r\n\r\nCreate paginated inline keyboards with optional search functionality.\r\n\r\n```python\r\nfrom gram_tools.keyboards import InlineBuilder, SearchButton\r\n\r\n# Create pagination with search\r\nbuilder = InlineBuilder(\r\n    per_page=5,\r\n    layout=1,\r\n    search_button=SearchButton(\"\ud83d\udd0d\")\r\n)\r\n\r\nitems = [\"Item 1\", \"Item 2\", \"Item 3\", ...]\r\nkeyboard = builder.get_paginated(items, page=1)\r\n\r\n# With search\r\nkeyboard = builder.get_paginated(items, page=1, search_term=\"search query\")\r\n```\r\n\r\nCustomization options:\r\n- `per_page`: Items per page\r\n- `layout`: Number of buttons per row\r\n- `next_button_text`: Next page button text\r\n- `prev_button_text`: Previous page button text\r\n- `page_callback_prefix`: Optional callback prefix of core paginator and item selection\r\n- `ignore_callback_prefix`: Optional callback prefox on click ignore items\r\n- `not_exist_page`: Text for disabled navigation buttons\r\n- `search_button`: Optional search button configuration\r\n\r\n### Whole example:\r\n\r\n```python\r\nimport asyncio\r\nfrom aiogram import Bot, Dispatcher, Router, F\r\nfrom aiogram.types import CallbackQuery, Message\r\nfrom aiogram.filters import Command\r\nfrom aiogram.fsm.context import FSMContext\r\nfrom aiogram.fsm.state import StatesGroup, State\r\n\r\nfrom gram_tools.keyboards import (\r\n    InlineBuilder,\r\n    SearchButton\r\n)\r\n\r\nAPI_TOKEN = \"7350010616:AAHQXoWzmMuffBkv5mPAI2-cBziZBkvoTI8\"\r\n\r\nbot = Bot(token=API_TOKEN)\r\ndp = Dispatcher()\r\nrouter = Router()\r\n\r\nitems = [f\"Item {i}\" for i in range(1, 21)]\r\nsearch_button = SearchButton(button_text=\"\ud83d\udd0d Search\")\r\ninline_builder = InlineBuilder(per_page=5, search_button=search_button)\r\n\r\nclass Form(StatesGroup):\r\n    waiting_for_search_term = State()\r\n\r\n@router.message(Command(\"start\"))\r\nasync def show_inline_keyboard(message: Message, state: FSMContext):\r\n    await state.clear()\r\n    keyboard = inline_builder.get_paginated(items, page=1)\r\n    await message.answer(\"Select an item:\", reply_markup=keyboard)\r\n\r\n@router.callback_query(inline_builder.page_callback.filter())\r\nasync def handle_callback(callback: CallbackQuery, callback_data: inline_builder.page_callback, state: FSMContext):\r\n    action = callback_data.action\r\n    value = callback_data.value\r\n\r\n    data = await state.get_data()\r\n    search_term = data.get('search_term')\r\n\r\n    if action in (\"next\", \"prev\"):\r\n        page = value\r\n        keyboard = inline_builder.get_paginated(items, page=page, search_term=search_term)\r\n        await callback.message.edit_reply_markup(reply_markup=keyboard)\r\n        await callback.answer()\r\n    elif action == \"sel\":\r\n        item_index = value\r\n        if search_term:\r\n            filtered_items = [item for item in items if search_term.lower() in str(item).lower()]\r\n        else:\r\n            filtered_items = items\r\n\r\n        if 0 <= item_index < len(filtered_items):\r\n            selected_item = filtered_items[item_index]\r\n            await callback.message.answer(f\"You selected: {selected_item}\")\r\n        else:\r\n            await callback.message.answer(\"Element not found.\")\r\n        await callback.answer()\r\n\r\n@router.callback_query(search_button.search_callback.filter())\r\nasync def handle_search_callback(callback: CallbackQuery, state: FSMContext):\r\n    print(callback.data)\r\n    await state.update_data(search_term=None)\r\n    await callback.message.answer(\"Type text to search:\")\r\n    await state.set_state(Form.waiting_for_search_term)\r\n    await callback.answer()\r\n\r\n@router.message(Form.waiting_for_search_term)\r\nasync def perform_search(message: Message, state: FSMContext):\r\n    search_term = message.text\r\n    await state.update_data(search_term=search_term)\r\n    await state.set_state(None)\r\n\r\n    keyboard = inline_builder.get_paginated(items, page=1, search_term=search_term)\r\n\r\n    if keyboard.inline_keyboard:\r\n        await message.answer(\"Results:\", reply_markup=keyboard)\r\n    else:\r\n        await message.answer(\"Nothing found.\")\r\n\r\n@router.callback_query(inline_builder.ignore_callback.filter())\r\nasync def handle_ignore_callback(callback: CallbackQuery):\r\n    await callback.answer()\r\n\r\nasync def main():\r\n    dp.include_router(router)\r\n    await dp.start_polling(bot)\r\n\r\nif __name__ == \"__main__\":\r\n    asyncio.run(main())\r\n```\r\n\r\n### Template System\r\n\r\nSimple template system for message text formatting.\r\n\r\n```python\r\nfrom gram_tools.messages import T\r\n\r\ntemplate = T(\"Hello, ${name}!\")\r\ntext = template.text(name=\"User\")\r\n```\r\n\r\n### Whole example\r\n\r\n```python\r\nimport asyncio\r\nfrom aiogram import Bot, Dispatcher, Router, F\r\nfrom aiogram.types import Message\r\nfrom aiogram.filters import Command\r\n\r\nfrom gram_tools.messages import T\r\n\r\nAPI_TOKEN = \"BOT TOKEN\"\r\n\r\nbot = Bot(token=API_TOKEN)\r\ndp = Dispatcher()\r\nrouter = Router()\r\n\r\n# Define a template\r\nwelcome_template = T(\"Hello, ${name}! Welcome to our bot.\")\r\n\r\n@router.message(Command(\"start\"))\r\nasync def send_welcome(message: Message):\r\n    # Render the template with the user's name\r\n    text = welcome_template.text(name=message.from_user.full_name)\r\n    await message.answer(text)\r\n\r\n@router.message(Command(\"info\"))\r\nasync def send_info(message: Message):\r\n    info_template = T(\r\n        \"User Info:\\n\"\r\n        \"- ID: ${id}\\n\"\r\n        \"- Username: ${username}\\n\"\r\n        \"- First Name: ${first_name}\\n\"\r\n        \"- Last Name: ${last_name}\"\r\n    )\r\n    text = info_template.text(\r\n        id=message.from_user.id,\r\n        username=message.from_user.username or \"N/A\",\r\n        first_name=message.from_user.first_name or \"N/A\",\r\n        last_name=message.from_user.last_name or \"N/A\"\r\n    )\r\n    await message.answer(text)\r\n\r\nasync def main():\r\n    dp.include_router(router)\r\n    await dp.start_polling(bot)\r\n\r\nif __name__ == \"__main__\":\r\n    asyncio.run(main())\r\n```\r\n\r\n## Contributing\r\n\r\nContributions are welcome! Please feel free to submit a Pull Request.\r\n",
    "bugtrack_url": null,
    "license": null,
    "summary": "Utilities for streamlined aiogram bot development.",
    "version": "1.0.2",
    "project_urls": {
        "Bug Tracker": "https://github.com/DedInc/gram-tools/issues",
        "Homepage": "https://github.com/DedInc/gram-tools"
    },
    "split_keywords": [],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "3764743fdf6610cb59061caaa61358538d3f860acd62339a5a81586999558f20",
                "md5": "5183e50ddf326bce6ffaac0f9a9a8875",
                "sha256": "5806e1aef8e204f7eae815cc3e043b4642b83cb4856df43da8e073537a82e2bc"
            },
            "downloads": -1,
            "filename": "gram_tools-1.0.2-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "5183e50ddf326bce6ffaac0f9a9a8875",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.6",
            "size": 9424,
            "upload_time": "2024-11-09T11:24:40",
            "upload_time_iso_8601": "2024-11-09T11:24:40.258249Z",
            "url": "https://files.pythonhosted.org/packages/37/64/743fdf6610cb59061caaa61358538d3f860acd62339a5a81586999558f20/gram_tools-1.0.2-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "665ba3b1ab3a02cfa87d5b2300b423797c64ed55805a9f50c93a346be8892ae3",
                "md5": "5c8e46ed1b43b6aebc03e89dc71e0502",
                "sha256": "6f598f583111da350836dc3df9cfdaf2eba5cd9646536d3ec4696c7414a41e62"
            },
            "downloads": -1,
            "filename": "gram_tools-1.0.2.tar.gz",
            "has_sig": false,
            "md5_digest": "5c8e46ed1b43b6aebc03e89dc71e0502",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.6",
            "size": 11431,
            "upload_time": "2024-11-09T11:24:44",
            "upload_time_iso_8601": "2024-11-09T11:24:44.309181Z",
            "url": "https://files.pythonhosted.org/packages/66/5b/a3b1ab3a02cfa87d5b2300b423797c64ed55805a9f50c93a346be8892ae3/gram_tools-1.0.2.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-11-09 11:24:44",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "DedInc",
    "github_project": "gram-tools",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": false,
    "lcname": "gram-tools"
}
        
Elapsed time: 0.82322s