Compare commits

..

4 Commits

10 changed files with 211 additions and 103 deletions

View File

@ -77,7 +77,8 @@ explicit = true
torch = { index = "torch" }
triton = { index = "torch" }
torchvision = { index = "torch" }
py-leap = { git = "https://github.com/guilledk/py-leap.git", branch = "struct_unwrap" }
# py-leap = { git = "https://github.com/guilledk/py-leap.git", branch = "struct_unwrap" }
py-leap = { path = "../py-leap", editable = true }
pytest-dockerctl = { git = "https://github.com/pikers/pytest-dockerctl.git", branch = "g_update" }
[build-system]

View File

@ -165,7 +165,7 @@ def run(*args, **kwargs):
@run.command()
def db():
from .db import open_new_database
from skynet.frontend.chatbot.db import open_new_database
logging.basicConfig(level=logging.INFO)
with open_new_database(cleanup=False) as db_params:

View File

@ -32,12 +32,17 @@ class FrontendConfig(msgspec.Struct):
account: str
permission: str
key: str
node_url: str
hyperion_url: str
ipfs_url: str
token: str
db_host: str
db_user: str
db_pass: str
db_name: str = 'skynet'
node_url: str = 'https://testnet.telos.net'
hyperion_url: str = 'https://testnet.skygpu.net'
ipfs_domain: str = 'ipfs.skygpu.net'
explorer_domain: str = 'explorer.skygpu.net'
request_timeout: int = 60 * 3
proto_version: int = 0
reward: str = '20.0000 GPU'
receiver: str = 'gpu.scd'

View File

@ -1,6 +1,10 @@
import random
from ..constants import *
from ..constants import (
MODELS,
get_model_by_shortname,
MAX_STEP, MIN_STEP, MAX_WIDTH, MAX_HEIGHT, MAX_GUIDANCE
)
class ConfigRequestFormatError(BaseException):
@ -26,17 +30,17 @@ class ConfigSizeDivisionByEight(BaseException):
def validate_user_config_request(req: str):
params = req.split(' ')
if len(params) < 3:
if len(params) < 2:
raise ConfigRequestFormatError('config request format incorrect')
else:
try:
attr = params[1]
attr = params[0]
match attr:
case 'model' | 'algo':
attr = 'model'
val = params[2]
val = params[1]
shorts = [model_info.short for model_info in MODELS.values()]
if val not in shorts:
raise ConfigUnknownAlgorithm(f'no model named {val}')
@ -44,38 +48,38 @@ def validate_user_config_request(req: str):
val = get_model_by_shortname(val)
case 'step':
val = int(params[2])
val = int(params[1])
val = max(min(val, MAX_STEP), MIN_STEP)
case 'width':
val = max(min(int(params[2]), MAX_WIDTH), 16)
val = max(min(int(params[1]), MAX_WIDTH), 16)
if val % 8 != 0:
raise ConfigSizeDivisionByEight(
'size must be divisible by 8!')
case 'height':
val = max(min(int(params[2]), MAX_HEIGHT), 16)
val = max(min(int(params[1]), MAX_HEIGHT), 16)
if val % 8 != 0:
raise ConfigSizeDivisionByEight(
'size must be divisible by 8!')
case 'seed':
val = params[2]
val = params[1]
if val == 'auto':
val = None
else:
val = int(params[2])
val = int(params[1])
case 'guidance':
val = float(params[2])
val = float(params[1])
val = max(min(val, MAX_GUIDANCE), 0)
case 'strength':
val = float(params[2])
val = float(params[1])
val = max(min(val, 0.99), 0.01)
case 'upscaler':
val = params[2]
val = params[1]
if val == 'off':
val = None
elif val != 'x4':
@ -83,7 +87,7 @@ def validate_user_config_request(req: str):
f'\"{val}\" is not a valid upscaler')
case 'autoconf':
val = params[2]
val = params[1]
if val == 'on':
val = True

View File

@ -22,7 +22,8 @@ from skynet.constants import (
HELP_TOPICS,
HELP_UNKWNOWN_PARAM,
COOL_WORDS,
DONATION_INFO
DONATION_INFO,
UNKNOWN_CMD_TEXT
)
from skynet.frontend import validate_user_config_request
from skynet.frontend.chatbot.db import FrontendUserDB
@ -54,7 +55,11 @@ def perform_auto_conf(config: dict) -> dict:
def sanitize_params(params: dict) -> dict:
if 'seed' not in params:
if (
'seed' not in params
or
params['seed'] is None
):
params['seed'] = randint(0, 0xffffffff)
s_params = {}
@ -82,7 +87,8 @@ class BaseChatbot(ABC):
self.config = config
self.ipfs = AsyncIPFSHTTP(config.ipfs_url)
self.cleos = CLEOS(endpoint=config.node_url)
self.cleos.load_abi('gpu.scd', GPU_CONTRACT_ABI)
self.cleos.load_abi(config.receiver, GPU_CONTRACT_ABI)
self.cleos.import_key(config.account, config.key)
self.hyperion = HyperionAPI(config.hyperion_url)
async def init(self):
@ -117,9 +123,12 @@ class BaseChatbot(ABC):
'''
...
async def create_status_msg(self, msg: BaseMessage, init_text: str) -> tuple[BaseUser, BaseMessage, dict]:
async def create_status_msg(self, msg: BaseMessage, init_text: str, force_user: BaseUser | None = None) -> tuple[BaseUser, BaseMessage, dict]:
# maybe init user
user = msg.author
if force_user:
user = force_user
user_row = await self.db.get_or_create_user(user.id)
# create status msg
@ -151,7 +160,7 @@ class BaseChatbot(ABC):
...
@abstractmethod
async def update_request_status_step_0(self, status_msg: BaseMessage, user: BaseUser):
async def update_request_status_step_0(self, status_msg: BaseMessage, user_msg: BaseMessage):
'''
First step in request status message lifecycle, should notify which user sent the request
and that we are about to broadcast the request to chain
@ -196,19 +205,24 @@ class BaseChatbot(ABC):
async def handle_request(
self,
msg: BaseMessage
msg: BaseMessage,
force_user: BaseUser | None = None
):
if msg.chat.is_private:
return
if len(msg.text) == 0:
await self.reply_to(msg.id, 'empty prompt ignored.')
if (
len(msg.text) == 0
and
msg.command != BaseCommands.REDO
):
await self.reply_to(msg, 'empty prompt ignored.')
return
# maybe initialize user db row and send a new msg thats gonna
# be updated throughout the request lifecycle
user, status_msg, user_row = await self.create_status_msg(
msg, 'started processing a {msg.command} request...')
msg, f'started processing a {msg.command} request...', force_user=force_user)
# if this is a redo msg, we attempt to get the input params from db
# else use msg properties
@ -224,18 +238,28 @@ class BaseChatbot(ABC):
inputs = await self.db.get_last_inputs_of(user.id)
if not prompt:
await self.reply_to(msg.id, 'no last prompt found, try doing a non-redo request first')
await self.reply_to(msg, 'no last prompt found, try doing a non-redo request first')
return
case _:
await self.reply_to(msg.id, f'unknown request of type {msg.command}')
await self.reply_to(msg, f'unknown request of type {msg.command}')
return
if (
msg.command == BaseCommands.IMG2IMG
and
len(inputs) == 0
):
await self.edit_msg(status_msg, 'seems you tried to do an img2img command without sending image')
return
# maybe apply recomended settings to this request
del user_row['id']
if user_row['autoconf']:
user_row = perform_auto_conf(user_row)
user_row = sanitize_params(user_row)
body = BodyV0(
method=command,
params=BodyV0Params(
@ -247,7 +271,7 @@ class BaseChatbot(ABC):
# publish inputs to ipfs
input_cids = []
for i in inputs:
i.publish()
await i.publish(self.ipfs, user_row)
input_cids.append(i.cid)
inputs_str = ','.join((i for i in input_cids))
@ -261,22 +285,25 @@ class BaseChatbot(ABC):
last_inputs=inputs
)
await self.update_request_status_step_0(status_msg)
await self.update_request_status_step_0(status_msg, msg)
# prepare and send enqueue request
request_time = datetime.now().isoformat()
str_body = msgspec.json.encode(body).decode('utf-8')
enqueue_receipt = await self.cleos.a_push_action(
self.receiver,
self.config.receiver,
'enqueue',
(
[
self.config.account,
str_body,
inputs_str,
self.config.reward,
1
)
],
self.config.account,
key=self.cleos.private_keys[self.config.account],
permission=self.config.permission
)
await self.update_request_status_step_1(status_msg, enqueue_receipt)
@ -295,7 +322,7 @@ class BaseChatbot(ABC):
'''
request_id, nonce = console_lines[-1].rstrip().split(':')
request_hash = sha256(
(nonce + str_body + inputs_str).encode('utf-8')).hexdigest.upper()
(nonce + str_body + inputs_str).encode('utf-8')).hexdigest().upper()
request_id = int(request_id)
@ -343,13 +370,14 @@ class BaseChatbot(ABC):
data = action['act']['data']
result_cid = data['ipfs_hash']
worker = data['worker']
logging.info('found matching submit! tx: {submit_tx_hash} cid: {ipfs_hash}')
logging.info(f'found matching submit! tx: {submit_tx_hash} cid: {result_cid}')
break
except json.JSONDecodeError:
if i < self.config.request_timeout:
logging.error('network error while searching for submit, retry...')
await asyncio.sleep(1)
await asyncio.sleep(1)
# if we found matching submit submit_tx_hash, worker, and result_cid will not be None
if not result_cid:
@ -365,7 +393,7 @@ class BaseChatbot(ABC):
result_img = None
if get_img_response and get_img_response.status_code == 200:
try:
with Image.open(io.BytesIO(get_img_response.raw)) as img:
with Image.open(io.BytesIO(get_img_response.read())) as img:
w, h = img.size
if (
@ -386,7 +414,7 @@ class BaseChatbot(ABC):
logging.warning(f'couldn\'t get ipfs result at {result_link}!')
await self.update_request_status_final(
msg, status_msg, user, body, inputs, submit_tx_hash, worker, result_img)
msg, status_msg, user, body.params, inputs, submit_tx_hash, worker, result_img)
await self.db.increment_generated(user.id)
@ -442,7 +470,14 @@ class BaseChatbot(ABC):
await self.reply_to(msg, DONATION_INFO)
async def say(self, msg: BaseMessage):
if not msg.chat.is_private or not msg.author.is_admin:
if (
msg.chat.is_private
or
not msg.author.is_admin
):
return
await self.new_msg(self.main_group, msg.text)
async def echo_unknown(self, msg: BaseMessage):
await self.reply_to(msg, UNKNOWN_CMD_TEXT)

View File

@ -59,25 +59,6 @@ CREATE TABLE IF NOT EXISTS skynet.user_requests(
);
"""
def try_decode_uid(uid: str) -> tuple[str | None, int | None]:
"""
Attempts to decode the user ID. The user ID can be just an integer
or of the format 'proto+uid'. Returns (None, int) if it's just an
integer or (proto, int) if it's 'proto+uid'. Returns (None, None)
if neither format is valid.
"""
try:
return None, int(uid)
except ValueError:
pass
try:
proto, uid_str = uid.split("+", 1)
return proto, int(uid_str)
except ValueError:
logging.warning(f"Got non-chat-proto UID?: {uid}")
return None, None
@cm
def open_new_database(cleanup: bool = True):
@ -118,14 +99,17 @@ def open_new_database(cleanup: bool = True):
time.sleep(1)
logging.info("Creating 'skynet' database...")
with psycopg2.connect(
conn = psycopg2.connect(
user="postgres", password=root_password, host="localhost", port=port
) as conn:
conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT)
with conn.cursor() as cursor:
cursor.execute(f"CREATE USER skynet WITH PASSWORD '{skynet_password}'")
cursor.execute("CREATE DATABASE skynet")
cursor.execute("GRANT ALL PRIVILEGES ON DATABASE skynet TO skynet")
)
conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT)
conn.autocommit = True
cursor = conn.cursor()
cursor.execute(f"CREATE USER skynet WITH PASSWORD '{skynet_password}'")
cursor.execute("CREATE DATABASE skynet")
cursor.execute("GRANT ALL PRIVILEGES ON DATABASE skynet TO skynet")
cursor.close()
conn.close()
logging.info("Database setup complete.")
yield container, skynet_password, db_host
@ -204,7 +188,7 @@ class FrontendUserDB:
records = await conn.fetch(
"SELECT * FROM skynet.user_config WHERE id = $1", user_id
)
return records[0] if len(records) == 1 else None
return dict(records[0]) if len(records) == 1 else None
async def get_user(self, user_id: int):
"""Alias for get_user_config (same data returned)."""
@ -430,7 +414,7 @@ class FrontendUserDB:
)
if not last_inputs_str:
return None
return []
last_inputs = []
for i in last_inputs_str.split(','):

View File

@ -1,19 +1,21 @@
import json
import logging
import traceback
from typing import Self, Awaitable
from datetime import datetime, timezone
from telebot.types import (
AsyncTeleBot,
User as TGUser,
Chat as TGChat,
PhotoSize as TGPhotoSize,
Message as TGMessage,
CallbackQuery,
InputMediaPhoto,
InlineKeyboardButton,
InlineKeyboardMarkup
)
from telebot.async_telebot import ExceptionHandler
from telebot.async_telebot import AsyncTeleBot, ExceptionHandler
from telebot.formatting import hlink
from skynet.types import BodyV0Params
@ -21,7 +23,7 @@ from skynet.config import FrontendConfig
from skynet.constants import VERSION
from skynet.frontend.chatbot import BaseChatbot
from skynet.frontend.chatbot.db import FrontendUserDB
from skynet.frontend.types import (
from skynet.frontend.chatbot.types import (
BaseUser,
BaseChatRoom,
BaseFileInput,
@ -30,6 +32,7 @@ from skynet.frontend.types import (
)
GROUP_ID = -1001541979235
TEST_GROUP_ID = -4099622703
ADMIN_USER_ID = 383385940
@ -100,6 +103,9 @@ class TelegramFileInput(BaseFileInput):
raise ValueError
def set_cid(self, cid: str):
self._cid = cid
async def download(self, bot: AsyncTeleBot) -> bytes:
file_path = (await bot.get_file(self.id)).file_path
self._raw = await bot.download_file(file_path)
@ -112,6 +118,9 @@ class TelegramMessage(BaseMessage):
self._msg = msg
self._cmd = cmd
self._chat = TelegramChatRoom(msg.chat)
self._inputs: list[TelegramFileInput] | None = None
self._author = None
@property
def id(self) -> int:
@ -123,10 +132,17 @@ class TelegramMessage(BaseMessage):
@property
def text(self) -> str:
return self._msg.text[len(self._cmd) + 1:]
# remove command name, slash and first space
if self._msg.text:
return self._msg.text[len(self._cmd) + 2:]
return self._msg.caption[len(self._cmd) + 2:]
@property
def author(self) -> TelegramUser:
if self._author:
return self._author
return TelegramUser(self._msg.from_user)
@property
@ -135,10 +151,15 @@ class TelegramMessage(BaseMessage):
@property
def inputs(self) -> list[TelegramFileInput]:
return [
TelegramFileInput(photo=p)
for p in self._msg.photo
]
if self._inputs is None:
self._inputs = []
if self._msg.photo:
self._inputs = [
TelegramFileInput(photo=p)
for p in self._msg.photo
]
return self._inputs
# generic tg utils
@ -186,19 +207,18 @@ def prepare_metainfo_caption(user: TelegramUser, worker: str, reward: str, param
def generate_reply_caption(
config: FrontendConfig,
user: TelegramUser,
params: BodyV0Params,
tx_hash: str,
worker: str,
reward: str,
explorer_domain: str
):
explorer_link = hlink(
'SKYNET Transaction Explorer',
f'https://{explorer_domain}/v2/explore/transaction/{tx_hash}'
f'https://{config.explorer_domain}/v2/explore/transaction/{tx_hash}'
)
meta_info = prepare_metainfo_caption(user, worker, reward, params)
meta_info = prepare_metainfo_caption(user, worker, config.reward, params)
final_msg = '\n'.join([
'Worker finished your task!',
@ -239,14 +259,43 @@ class TelegramChatbot(BaseChatbot):
append_handler(bot, BaseCommands.SAY, self.say)
append_handler(bot, BaseCommands.TXT2IMG, self.handle_request)
append_handler(bot, BaseCommands.IMG2IMG, self.handle_request)
@bot.message_handler(func=lambda _: True, content_types=['photo', 'document'])
async def handle_img2img(tg_msg: TGMessage):
msg = TelegramMessage(cmd='img2img', msg=tg_msg)
for file in msg.inputs:
await file.download(bot)
await self.handle_request(msg)
append_handler(bot, BaseCommands.REDO, self.handle_request)
@bot.message_handler(func=lambda _: True)
async def unknown_cmd(tg_msg: TGMessage):
if tg_msg.text[0] == '/':
msg = TelegramMessage(cmd='unknown', msg=tg_msg)
await self.echo_unknown(msg)
@bot.callback_query_handler(func=lambda _: True)
async def callback_query(call: CallbackQuery):
call_json = json.loads(call.data)
method = call_json.get('method')
match method:
case 'redo':
msg = await self.new_msg(self.main_group, 'processing a redo request...')
msg._cmd = 'redo'
await self.handle_request(msg, force_user=TelegramUser(user=call.from_user))
await bot.delete_message(chat_id=self.main_group.id, message_id=msg.id)
self.bot = bot
self._main_room: TelegramChatRoom | None = None
async def init(self):
tg_group = await self.bot.get_chat(GROUP_ID)
tg_group = await self.bot.get_chat(TEST_GROUP_ID)
self._main_room = TelegramChatRoom(chat=tg_group)
logging.info('initialized')
async def run(self):
await self.init()
@ -281,14 +330,14 @@ class TelegramChatbot(BaseChatbot):
f'\n[{timestamp_pretty()}] <b>timeout processing request</b>',
)
async def update_request_status_step_0(self, status_msg: TelegramMessage):
async def update_request_status_step_0(self, status_msg: TelegramMessage, user_msg: TelegramMessage):
'''
First step in request status message lifecycle, should notify which user sent the request
and that we are about to broadcast the request to chain
'''
await self.update_status_msg(
status_msg,
f'processing a \'{status_msg.command}\' request by {status_msg.author.name}\n'
f'processing a \'{user_msg.command}\' request by {user_msg.author.name}\n'
f'[{timestamp_pretty()}] <i>broadcasting transaction to chain...</i>'
)
@ -300,7 +349,7 @@ class TelegramChatbot(BaseChatbot):
enqueue_tx_id = tx_result['transaction_id']
enqueue_tx_link = hlink(
'Your request on Skynet Explorer',
f'https://{self.explorer_domain}/v2/explore/transaction/{enqueue_tx_id}'
f'https://{self.config.explorer_domain}/v2/explore/transaction/{enqueue_tx_id}'
)
await self.append_status_msg(
status_msg,
@ -316,7 +365,7 @@ class TelegramChatbot(BaseChatbot):
'''
tx_link = hlink(
'Your result on Skynet Explorer',
f'https://{self.explorer_domain}/v2/explore/transaction/{submit_tx_hash}'
f'https://{self.config.explorer_domain}/v2/explore/transaction/{submit_tx_hash}'
)
await self.append_status_msg(
status_msg,
@ -342,7 +391,7 @@ class TelegramChatbot(BaseChatbot):
reply caption and if provided also sent the found result img
'''
caption = generate_reply_caption(
user, worker, self.config.reward, params)
self.config, user, params, submit_tx_hash, worker)
await self.bot.delete_message(
chat_id=status_msg.chat.id,
@ -364,8 +413,8 @@ class TelegramChatbot(BaseChatbot):
parse_mode='HTML'
)
case 1:
_input = inputs.pop()
case _:
_input = inputs[-1]
await self.bot.send_media_group(
status_msg.chat.id,
media=[
@ -373,6 +422,3 @@ class TelegramChatbot(BaseChatbot):
InputMediaPhoto(result_img, caption=caption, parse_mode='HTML')
]
)
case _:
raise NotImplementedError

View File

@ -1,8 +1,9 @@
import io
from ABC import ABC, abstractproperty, abstractmethod
from abc import ABC, abstractproperty, abstractmethod
from enum import StrEnum
from typing import Self
from pathlib import Path
from PIL import Image
from skynet.ipfs import AsyncIPFSHTTP
@ -52,6 +53,10 @@ class BaseFileInput(ABC):
async def download(self, *args) -> bytes:
...
@abstractmethod
def set_cid(self, cid: str):
...
async def publish(self, ipfs_api: AsyncIPFSHTTP, user_row: dict):
with Image.open(io.BytesIO(self._raw)) as img:
w, h = img.size
@ -63,11 +68,12 @@ class BaseFileInput(ABC):
):
img.thumbnail((user_row['width'], user_row['height']))
img_path = '/tmp/ipfs-staging/img.png'
img_path = Path('/tmp/ipfs-staging/img.png')
img.save(img_path, format='PNG')
ipfs_info = await ipfs_api.add(img_path)
ipfs_hash = ipfs_info['Hash']
self.set_cid(ipfs_hash)
await ipfs_api.pin(ipfs_hash)

View File

@ -85,6 +85,7 @@ class BodyV0Params(Struct):
strength: str | float | None = None
output_type: str | None = 'png'
upscaler: str | None = None
autoconf: bool | None = None
class BodyV0(Struct):

48
uv.lock
View File

@ -1,4 +1,5 @@
version = 1
revision = 1
requires-python = ">=3.10, <3.13"
resolution-markers = [
"python_full_version >= '3.12' and sys_platform == 'darwin'",
@ -271,7 +272,7 @@ name = "cffi"
version = "1.17.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pycparser" },
{ name = "pycparser", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 }
wheels = [
@ -1271,7 +1272,7 @@ name = "nvidia-cudnn-cu12"
version = "9.1.0.70"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nvidia-cublas-cu12" },
{ name = "nvidia-cublas-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/9f/fd/713452cd72343f682b1c7b9321e23829f00b842ceaedcda96e742ea0b0b3/nvidia_cudnn_cu12-9.1.0.70-py3-none-manylinux2014_x86_64.whl", hash = "sha256:165764f44ef8c61fcdfdfdbe769d687e06374059fbb388b6c89ecb0e28793a6f", size = 664752741 },
@ -1298,9 +1299,9 @@ name = "nvidia-cusolver-cu12"
version = "11.4.5.107"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nvidia-cublas-cu12" },
{ name = "nvidia-cusparse-cu12" },
{ name = "nvidia-nvjitlink-cu12" },
{ name = "nvidia-cublas-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" },
{ name = "nvidia-cusparse-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" },
{ name = "nvidia-nvjitlink-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/bc/1d/8de1e5c67099015c834315e333911273a8c6aaba78923dd1d1e25fc5f217/nvidia_cusolver_cu12-11.4.5.107-py3-none-manylinux1_x86_64.whl", hash = "sha256:8a7ec542f0412294b15072fa7dab71d31334014a69f953004ea7a118206fe0dd", size = 124161928 },
@ -1311,7 +1312,7 @@ name = "nvidia-cusparse-cu12"
version = "12.1.0.106"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nvidia-nvjitlink-cu12" },
{ name = "nvidia-nvjitlink-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/65/5b/cfaeebf25cd9fdec14338ccb16f6b2c4c7fa9163aefcf057d86b9cc248bb/nvidia_cusparse_cu12-12.1.0.106-py3-none-manylinux1_x86_64.whl", hash = "sha256:f3b50f42cf363f86ab21f720998517a659a48131e8d538dc02f8768237bd884c", size = 195958278 },
@ -1598,7 +1599,7 @@ wheels = [
[[package]]
name = "py-leap"
version = "0.1a35"
source = { git = "https://github.com/guilledk/py-leap.git?branch=struct_unwrap#18b3c73e724922a060db5f8ea2b9d9727b6152cc" }
source = { editable = "../py-leap" }
dependencies = [
{ name = "base58" },
{ name = "cryptos" },
@ -1608,6 +1609,33 @@ dependencies = [
{ name = "ripemd-hash" },
]
[package.metadata]
requires-dist = [
{ name = "base58", specifier = ">=2.1.1,<3" },
{ name = "cryptos", specifier = ">=2.0.9,<3" },
{ name = "httpx", specifier = ">=0.28.1,<0.29" },
{ name = "msgspec", specifier = ">=0.19.0" },
{ name = "requests", specifier = "<2.32.0" },
{ name = "ripemd-hash", specifier = ">=1.0.1,<2" },
]
[package.metadata.requires-dev]
dev = [
{ name = "docker", specifier = ">=6.1.3,<7" },
{ name = "pdbpp", specifier = ">=0.10.3,<0.11" },
{ name = "pytest", specifier = ">=8.3.4,<9" },
{ name = "pytest-trio", specifier = ">=0.8.0,<0.9" },
]
docs = [
{ name = "sphinx", specifier = "==7.1.2" },
{ name = "sphinx-rtd-theme", specifier = "==1.3.0" },
]
snaps = [
{ name = "bs4", specifier = ">=0.0.2,<0.0.3" },
{ name = "tdqm", specifier = ">=0.0.1,<0.0.2" },
{ name = "zstandard", specifier = ">=0.21.0,<0.22" },
]
[[package]]
name = "pycparser"
version = "2.22"
@ -1633,8 +1661,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/61/74/49f5d20c514ccc631b940cc9dfec45dcce418dc84a98463a2e2ebec33904/pycryptodomex-3.21.0-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:52e23a0a6e61691134aa8c8beba89de420602541afaae70f66e16060fdcd677e", size = 2257982 },
{ url = "https://files.pythonhosted.org/packages/92/4b/d33ef74e2cc0025a259936661bb53432c5bbbadc561c5f2e023bcd73ce4c/pycryptodomex-3.21.0-cp36-abi3-win32.whl", hash = "sha256:a3d77919e6ff56d89aada1bd009b727b874d464cb0e2e3f00a49f7d2e709d76e", size = 1779052 },
{ url = "https://files.pythonhosted.org/packages/5b/be/7c991840af1184009fc86267160948350d1bf875f153c97bb471ad944e40/pycryptodomex-3.21.0-cp36-abi3-win_amd64.whl", hash = "sha256:b0e9765f93fe4890f39875e6c90c96cb341767833cfa767f41b490b506fa9ec0", size = 1816307 },
{ url = "https://files.pythonhosted.org/packages/af/ac/24125ad36778914a36f08d61ba5338cb9159382c638d9761ee19c8de822c/pycryptodomex-3.21.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:feaecdce4e5c0045e7a287de0c4351284391fe170729aa9182f6bd967631b3a8", size = 1694999 },
{ url = "https://files.pythonhosted.org/packages/93/73/be7a54a5903508070e5508925ba94493a1f326cfeecfff750e3eb250ea28/pycryptodomex-3.21.0-pp27-pypy_73-win32.whl", hash = "sha256:365aa5a66d52fd1f9e0530ea97f392c48c409c2f01ff8b9a39c73ed6f527d36c", size = 1769437 },
{ url = "https://files.pythonhosted.org/packages/e5/9f/39a6187f3986841fa6a9f35c6fdca5030ef73ff708b45a993813a51d7d10/pycryptodomex-3.21.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3efddfc50ac0ca143364042324046800c126a1d63816d532f2e19e6f2d8c0c31", size = 1619607 },
{ url = "https://files.pythonhosted.org/packages/f8/70/60bb08e9e9841b18d4669fb69d84b64ce900aacd7eb0ebebd4c7b9bdecd3/pycryptodomex-3.21.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0df2608682db8279a9ebbaf05a72f62a321433522ed0e499bc486a6889b96bf3", size = 1653571 },
{ url = "https://files.pythonhosted.org/packages/c9/6f/191b73509291c5ff0dddec9cc54797b1d73303c12b2e4017b24678e57099/pycryptodomex-3.21.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5823d03e904ea3e53aebd6799d6b8ec63b7675b5d2f4a4bd5e3adcb512d03b37", size = 1691548 },
@ -2126,7 +2152,7 @@ requires-dist = [
{ name = "outcome", specifier = ">=1.3.0.post0" },
{ name = "pillow", specifier = ">=10.0.1,<11" },
{ name = "protobuf", specifier = ">=5.29.3,<6" },
{ name = "py-leap", git = "https://github.com/guilledk/py-leap.git?branch=struct_unwrap" },
{ name = "py-leap", editable = "../py-leap" },
{ name = "pytz", specifier = "~=2023.3.post1" },
{ name = "toml", specifier = ">=0.10.2,<0.11" },
{ name = "trio", specifier = ">=0.22.2,<0.23" },
@ -2436,7 +2462,7 @@ name = "triton"
version = "3.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "filelock" },
{ name = "filelock", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/98/29/69aa56dc0b2eb2602b553881e34243475ea2afd9699be042316842788ff5/triton-3.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b0dd10a925263abbe9fa37dcde67a5e9b2383fc269fdf59f5657cac38c5d1d8", size = 209460013 },