deleetify 0.1.0__tar.gz

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.
deleetify-0.1.0/.env ADDED
@@ -0,0 +1 @@
1
+ UV_PUBLISH_TOKEN=pypi-AgEIcHlwaS5vcmcCJDYwNjhiMjU3LWE3N2ItNDc5Yy1hNjc4LTRhNWM0NGJmM2YwYQACKlszLCJmY2Y5NjVlZS1hMWRlLTQzYzUtOTc0Ni1mM2UzMGE4MmNiNDIiXQAABiB4Yf7AUVnCGutky2UELZ816RqFMwqx3kMWTQm5fxrBVA
@@ -0,0 +1,10 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
@@ -0,0 +1 @@
1
+ 3.13
@@ -0,0 +1,120 @@
1
+ Metadata-Version: 2.4
2
+ Name: deleetify
3
+ Version: 0.1.0
4
+ Summary: Makes local leetcode setup less painfull
5
+ Author-email: "Dharshan.S" <me.dharshan.1@gmail.com>
6
+ Maintainer-email: "Dharshan.S" <me.dharshan.1@gmail.com>
7
+ License-Expression: MIT
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Software Development
13
+ Requires-Python: >=3.13
14
+ Requires-Dist: agent-client-protocol>=0.10.1
15
+ Requires-Dist: beautifulsoup4>=4.15.0
16
+ Requires-Dist: httpx>=0.28.1
17
+ Requires-Dist: typed-clap>=0.11.1
18
+ Description-Content-Type: text/markdown
19
+
20
+ # Deleetify
21
+
22
+ > Makes local LeetCode setup less painful.
23
+
24
+ A CLI tool that fetches any LeetCode problem and uses an AI agent to generate a ready-to-run boilerplate solution file — so you can jump straight into solving, not scaffolding.
25
+
26
+ ## Features
27
+
28
+ - Fetch any LeetCode problem by name
29
+ - AI-generated boilerplate via any ACP-compatible agent (e.g., opencode, claude-code)
30
+ - Supports Python and JavaScript starter code
31
+ - Configurable AI provider via TOML
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ pip install deleetify
37
+ ```
38
+
39
+ Or with [uv](https://docs.astral.sh/uv/):
40
+
41
+ ```bash
42
+ uv tool install deleetify
43
+ ```
44
+
45
+ ## Usage
46
+
47
+ ```bash
48
+ # Basic usage — generates Python boilerplate
49
+ deleetify "208. Implement Trie (Prefix Tree)"
50
+
51
+ # Specify language
52
+ deleetify "1. Two Sum" --lang javascript
53
+
54
+ # Custom config path
55
+ deleetify "15. 3Sum" --config ./my-agent.toml
56
+ ```
57
+
58
+ ## Configuration
59
+
60
+ Deleetify uses a `deleetify.toml` to configure the AI provider. By default it looks for the file in the current directory.
61
+
62
+ ```toml
63
+ [provider.opencode]
64
+ command = "opencode"
65
+ args = ["acp"]
66
+ ```
67
+
68
+ If you have multiple providers, set a default:
69
+
70
+ ```toml
71
+ [default]
72
+ provider = "opencode"
73
+
74
+ [provider.opencode]
75
+ command = "opencode"
76
+ args = ["acp"]
77
+
78
+ [provider.gemini]
79
+ command = "gemini"
80
+ args = ["--acp"]
81
+ # envs = {SOME_ENV = "value"}
82
+ ```
83
+
84
+ ## How it works
85
+
86
+ 1. You pass a LeetCode problem name (e.g., `"208. Implement Trie (Prefix Tree)"`)
87
+ 2. Deleetify parses the name and fetches the problem from the LeetCode API
88
+ 3. It scrapes the description, examples, and starter code from the HTML response
89
+ 4. It spawns an AI agent subprocess over the Agent Communication Protocol (ACP)
90
+ 5. The agent receives a prompt with the problem details and generates boilerplate code with built-in test cases
91
+ 6. The generated code is printed to stdout — pipe it to a file or your editor
92
+
93
+ ## Supported languages
94
+
95
+ | CLI flag | Language |
96
+ |--------------|------------|
97
+ | `python3` | Python 3 |
98
+ | `javascript` | JavaScript |
99
+
100
+ ## Requirements
101
+
102
+ - Python 3.13+
103
+ - An ACP-compatible agent (e.g., [opencode](https://opencode.ai))
104
+
105
+ ## Development
106
+
107
+ ```bash
108
+ git clone https://github.com/<user>/deleetify
109
+ cd deleetify
110
+
111
+ uv venv
112
+
113
+ uv sync
114
+
115
+ uv run deleetify "217. Contains Duplicate"
116
+ ```
117
+
118
+ ## License
119
+
120
+ MIT
@@ -0,0 +1,101 @@
1
+ # Deleetify
2
+
3
+ > Makes local LeetCode setup less painful.
4
+
5
+ A CLI tool that fetches any LeetCode problem and uses an AI agent to generate a ready-to-run boilerplate solution file — so you can jump straight into solving, not scaffolding.
6
+
7
+ ## Features
8
+
9
+ - Fetch any LeetCode problem by name
10
+ - AI-generated boilerplate via any ACP-compatible agent (e.g., opencode, claude-code)
11
+ - Supports Python and JavaScript starter code
12
+ - Configurable AI provider via TOML
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ pip install deleetify
18
+ ```
19
+
20
+ Or with [uv](https://docs.astral.sh/uv/):
21
+
22
+ ```bash
23
+ uv tool install deleetify
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ ```bash
29
+ # Basic usage — generates Python boilerplate
30
+ deleetify "208. Implement Trie (Prefix Tree)"
31
+
32
+ # Specify language
33
+ deleetify "1. Two Sum" --lang javascript
34
+
35
+ # Custom config path
36
+ deleetify "15. 3Sum" --config ./my-agent.toml
37
+ ```
38
+
39
+ ## Configuration
40
+
41
+ Deleetify uses a `deleetify.toml` to configure the AI provider. By default it looks for the file in the current directory.
42
+
43
+ ```toml
44
+ [provider.opencode]
45
+ command = "opencode"
46
+ args = ["acp"]
47
+ ```
48
+
49
+ If you have multiple providers, set a default:
50
+
51
+ ```toml
52
+ [default]
53
+ provider = "opencode"
54
+
55
+ [provider.opencode]
56
+ command = "opencode"
57
+ args = ["acp"]
58
+
59
+ [provider.gemini]
60
+ command = "gemini"
61
+ args = ["--acp"]
62
+ # envs = {SOME_ENV = "value"}
63
+ ```
64
+
65
+ ## How it works
66
+
67
+ 1. You pass a LeetCode problem name (e.g., `"208. Implement Trie (Prefix Tree)"`)
68
+ 2. Deleetify parses the name and fetches the problem from the LeetCode API
69
+ 3. It scrapes the description, examples, and starter code from the HTML response
70
+ 4. It spawns an AI agent subprocess over the Agent Communication Protocol (ACP)
71
+ 5. The agent receives a prompt with the problem details and generates boilerplate code with built-in test cases
72
+ 6. The generated code is printed to stdout — pipe it to a file or your editor
73
+
74
+ ## Supported languages
75
+
76
+ | CLI flag | Language |
77
+ |--------------|------------|
78
+ | `python3` | Python 3 |
79
+ | `javascript` | JavaScript |
80
+
81
+ ## Requirements
82
+
83
+ - Python 3.13+
84
+ - An ACP-compatible agent (e.g., [opencode](https://opencode.ai))
85
+
86
+ ## Development
87
+
88
+ ```bash
89
+ git clone https://github.com/<user>/deleetify
90
+ cd deleetify
91
+
92
+ uv venv
93
+
94
+ uv sync
95
+
96
+ uv run deleetify "217. Contains Duplicate"
97
+ ```
98
+
99
+ ## License
100
+
101
+ MIT
@@ -0,0 +1,11 @@
1
+
2
+ [default]
3
+ provider = "gemini"
4
+
5
+ [provider.opencode]
6
+ command = "opencode"
7
+ args = ["acp"]
8
+
9
+ [provider.gemini]
10
+ command = "gemini"
11
+ args = ["--acp"]
@@ -0,0 +1,29 @@
1
+ [project]
2
+ name = "deleetify"
3
+ version = "0.1.0"
4
+ description = "Makes local leetcode setup less painfull"
5
+ authors = [{ name = "Dharshan.S", email = "me.dharshan.1@gmail.com" }]
6
+ maintainers = [ { name = "Dharshan.S", email = "me.dharshan.1@gmail.com" } ]
7
+ license = "MIT"
8
+ readme = "README.md"
9
+ requires-python = ">=3.13"
10
+ dependencies = [
11
+ "agent-client-protocol>=0.10.1",
12
+ "beautifulsoup4>=4.15.0",
13
+ "httpx>=0.28.1",
14
+ "typed-clap>=0.11.1",
15
+ ]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Intended Audience :: Developers",
19
+ "Topic :: Software Development",
20
+ "Programming Language :: Python :: 3",
21
+ "Operating System :: OS Independent",
22
+ ]
23
+
24
+ [build-system]
25
+ requires = ["hatchling"]
26
+ build-backend = "hatchling.build"
27
+
28
+ [project.scripts]
29
+ deleetify = "deleetify.__main__:main"
File without changes
@@ -0,0 +1,8 @@
1
+ from deleetify.main import main as app
2
+ import asyncio
3
+
4
+ def main():
5
+ asyncio.run(app())
6
+
7
+ if __name__ == "__main__":
8
+ main()
@@ -0,0 +1,161 @@
1
+ from typing import Any, override
2
+
3
+ from acp import (
4
+ Agent,
5
+ Client,
6
+ RequestError,
7
+ )
8
+ from acp.schema import (
9
+ AgentMessageChunk,
10
+ AgentPlanUpdate,
11
+ AgentThoughtChunk,
12
+ AudioContentBlock,
13
+ AvailableCommandsUpdate,
14
+ ConfigOptionUpdate,
15
+ CreateTerminalResponse,
16
+ CurrentModeUpdate,
17
+ EmbeddedResourceContentBlock,
18
+ EnvVariable,
19
+ ImageContentBlock,
20
+ KillTerminalResponse,
21
+ PermissionOption,
22
+ ReadTextFileResponse,
23
+ ReleaseTerminalResponse,
24
+ RequestPermissionResponse,
25
+ ResourceContentBlock,
26
+ SessionInfoUpdate,
27
+ TerminalOutputResponse,
28
+ TextContentBlock,
29
+ ToolCallProgress,
30
+ ToolCallStart,
31
+ ToolCallUpdate,
32
+ UsageUpdate,
33
+ UserMessageChunk,
34
+ WaitForTerminalExitResponse,
35
+ WriteTextFileResponse,
36
+ )
37
+
38
+
39
+ class DeleetifyClient(Client):
40
+
41
+ def __init__(self) -> None:
42
+ super().__init__()
43
+
44
+ self.response: str = ""
45
+
46
+ @override
47
+ async def request_permission(
48
+ self,
49
+ options: list[PermissionOption],
50
+ session_id: str,
51
+ tool_call: ToolCallUpdate,
52
+ **kwargs: Any,
53
+ ) -> RequestPermissionResponse:
54
+ raise RequestError.method_not_found("session/request_permission")
55
+
56
+ @override
57
+ async def write_text_file(
58
+ self, content: str, path: str, session_id: str, **kwargs: Any
59
+ ) -> WriteTextFileResponse | None:
60
+ raise RequestError.method_not_found("fs/write_text_file")
61
+
62
+ @override
63
+ async def read_text_file(
64
+ self,
65
+ path: str,
66
+ session_id: str,
67
+ limit: int | None = None,
68
+ line: int | None = None,
69
+ **kwargs: Any,
70
+ ) -> ReadTextFileResponse:
71
+ raise RequestError.method_not_found("fs/read_text_file")
72
+
73
+ @override
74
+ async def create_terminal(
75
+ self,
76
+ command: str,
77
+ session_id: str,
78
+ args: list[str] | None = None,
79
+ cwd: str | None = None,
80
+ env: list[EnvVariable] | None = None,
81
+ output_byte_limit: int | None = None,
82
+ **kwargs: Any,
83
+ ) -> CreateTerminalResponse:
84
+ raise RequestError.method_not_found("terminal/create")
85
+
86
+ @override
87
+ async def terminal_output(
88
+ self, session_id: str, terminal_id: str, **kwargs: Any
89
+ ) -> TerminalOutputResponse:
90
+ raise RequestError.method_not_found("terminal/output")
91
+
92
+ @override
93
+ async def release_terminal(
94
+ self, session_id: str, terminal_id: str, **kwargs: Any
95
+ ) -> ReleaseTerminalResponse | None:
96
+ raise RequestError.method_not_found("terminal/release")
97
+
98
+ @override
99
+ async def wait_for_terminal_exit(
100
+ self, session_id: str, terminal_id: str, **kwargs: Any
101
+ ) -> WaitForTerminalExitResponse:
102
+ raise RequestError.method_not_found("terminal/wait_for_exit")
103
+
104
+ @override
105
+ async def kill_terminal(
106
+ self, session_id: str, terminal_id: str, **kwargs: Any
107
+ ) -> KillTerminalResponse | None:
108
+ raise RequestError.method_not_found("terminal/kill")
109
+
110
+ @override
111
+ async def session_update(
112
+ self,
113
+ session_id: str,
114
+ update: (
115
+ UserMessageChunk
116
+ | AgentMessageChunk
117
+ | AgentThoughtChunk
118
+ | ToolCallStart
119
+ | ToolCallProgress
120
+ | AgentPlanUpdate
121
+ | AvailableCommandsUpdate
122
+ | CurrentModeUpdate
123
+ | ConfigOptionUpdate
124
+ | SessionInfoUpdate
125
+ | UsageUpdate
126
+ ),
127
+ **kwargs: Any,
128
+ ) -> None:
129
+ if not isinstance(update, AgentMessageChunk):
130
+ return
131
+
132
+ content = update.content
133
+ text: str = ""
134
+
135
+ if isinstance(content, TextContentBlock):
136
+ text = content.text
137
+ elif isinstance(content, ImageContentBlock):
138
+ text = "<image>"
139
+ elif isinstance(content, AudioContentBlock):
140
+ text = "<audio>"
141
+ elif isinstance(content, ResourceContentBlock):
142
+ text = content.uri or "<resource>"
143
+ elif isinstance(content, EmbeddedResourceContentBlock):
144
+ text = "<resource>"
145
+ else:
146
+ text = "<content>"
147
+
148
+ self.response += text
149
+
150
+ @override
151
+ async def ext_method(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
152
+ raise RequestError.method_not_found(method)
153
+
154
+ @override
155
+ async def ext_notification(self, method: str, params: dict[str, Any]) -> None:
156
+ raise RequestError.method_not_found(method)
157
+
158
+ @override
159
+ def on_connect(self, conn: Agent) -> None: ...
160
+
161
+
@@ -0,0 +1,90 @@
1
+ from __future__ import annotations
2
+ import asyncio.subprocess as aio_subprocess
3
+ import os
4
+ from uuid import uuid4
5
+
6
+ from acp import (
7
+ PROTOCOL_VERSION,
8
+ NewSessionResponse,
9
+ connect_to_agent,
10
+ text_block,
11
+ )
12
+ from acp.core import ClientSideConnection
13
+ from acp.schema import (
14
+ ClientCapabilities,
15
+ Implementation,
16
+ )
17
+
18
+ import asyncio
19
+ import asyncio.subprocess as aio_subprocess
20
+ from deleetify.acp_client.client import DeleetifyClient
21
+ from deleetify.acp_client.exceptions import AgentProcessError
22
+ from deleetify.processor.config.model import Config
23
+ import asyncio.subprocess as aio_subprocess
24
+
25
+ async def create_agent_process(config: Config) -> aio_subprocess.Process:
26
+ program = config.command
27
+ spawn_program = program
28
+ spawn_args = config.args
29
+
30
+ try:
31
+ proc = await asyncio.create_subprocess_exec(
32
+ spawn_program,
33
+ *spawn_args,
34
+ env=config.envs,
35
+ stdin=aio_subprocess.PIPE,
36
+ stdout=aio_subprocess.PIPE,
37
+ )
38
+ except FileNotFoundError:
39
+ raise AgentProcessError(
40
+ f"No agent {config.command} fount in system"
41
+ )
42
+
43
+ if proc.stdin is None or proc.stdout is None:
44
+ raise AgentProcessError(
45
+ f"Agent {config.command} process does not expose stdio pipes"
46
+ )
47
+
48
+ return proc
49
+
50
+ class AcpHandler:
51
+ def __init__(
52
+ self,
53
+ client: DeleetifyClient,
54
+ proc: aio_subprocess.Process,
55
+ conn: ClientSideConnection
56
+ ) -> None:
57
+ """Keep __init__ synchronous. Only assign pre-resolved variables here."""
58
+ self.client: DeleetifyClient = client
59
+ self.proc: aio_subprocess.Process = proc
60
+ self.conn: ClientSideConnection = conn
61
+
62
+ @classmethod
63
+ async def create(
64
+ cls, client: DeleetifyClient, proc: aio_subprocess.Process
65
+ ) -> AcpHandler:
66
+ conn: ClientSideConnection = connect_to_agent(
67
+ client, proc.stdin, proc.stdout
68
+ )
69
+ _ = await conn.initialize(
70
+ protocol_version=PROTOCOL_VERSION,
71
+ client_capabilities=ClientCapabilities(),
72
+ client_info=Implementation(
73
+ name="deleetify", title="Deleetify Client", version="0.1.0"
74
+ ),
75
+ )
76
+
77
+ return cls(client, proc, conn)
78
+
79
+ async def createSession(self) -> NewSessionResponse:
80
+ session = await self.conn.new_session(mcp_servers=[], cwd=os.getcwd())
81
+ return session
82
+
83
+ async def prompt(self, session_id: str, prompt: str) -> str:
84
+ response = await self.conn.prompt(
85
+ session_id=session_id,
86
+ prompt=[text_block(prompt)],
87
+ message_id=str(uuid4()),
88
+ )
89
+
90
+ return self.client.response
@@ -0,0 +1,5 @@
1
+
2
+ class AgentProcessError(Exception):
3
+ def __init__(self, message: str) -> None:
4
+ super().__init__(message)
5
+
@@ -0,0 +1,9 @@
1
+
2
+ class ConfigNotFound(Exception):
3
+ def __init__(self, message: str) -> None:
4
+ super().__init__(message)
5
+
6
+
7
+ class NotATomlFile(Exception):
8
+ def __init__(self, message: str) -> None:
9
+ super().__init__(message)
@@ -0,0 +1,10 @@
1
+ from enum import StrEnum, auto
2
+
3
+ class Lang(StrEnum):
4
+ PYTHON3 = auto()
5
+ JAVASCRIPT = auto()
6
+
7
+ def externsion(self) -> str:
8
+ match self:
9
+ case Lang.PYTHON3: return 'py'
10
+ case Lang.JAVASCRIPT: return 'js'
@@ -0,0 +1,19 @@
1
+ from pathlib import Path
2
+ import clap
3
+ from clap import arg
4
+
5
+ from deleetify.cli.lang_model import Lang
6
+
7
+
8
+ @clap.command()
9
+ class Cli(clap.Parser):
10
+ """
11
+ Makes local leetcode setup less painfull
12
+ """
13
+
14
+ name: str
15
+ """Name of the leetcode problem [Example: `208. Implement Trie (Prefix Tree)`]"""
16
+
17
+ lang: Lang = arg(short=True, long=True, default_value=Lang.PYTHON3)
18
+
19
+ config: Path = arg(short=True, long=True, default_value=Path("deleetify.toml"))
@@ -0,0 +1,14 @@
1
+ from .exceptions import ConfigNotFound, NotATomlFile
2
+ from .model import Cli
3
+
4
+ def parse_cli_values() -> Cli:
5
+ parsed = Cli.parse()
6
+ config = parsed.config
7
+
8
+ if not config.is_file():
9
+ raise ConfigNotFound(f"{config.name} not exist")
10
+
11
+ if not config.name.endswith('.toml'):
12
+ raise NotATomlFile(f"It is not a toml file: {config.name}")
13
+
14
+ return parsed
@@ -0,0 +1,4 @@
1
+
2
+ class InvalidUserInput(Exception):
3
+ def __init__(self, message: str) -> None:
4
+ super().__init__(message)
@@ -0,0 +1,27 @@
1
+ from typing import Self
2
+ import re
3
+
4
+ from deleetify.file_name.exceptions import InvalidUserInput
5
+
6
+
7
+
8
+ class FileNameProcessor:
9
+ file_name: str
10
+
11
+ def __init__(self, file_name: str) -> None:
12
+ self.file_name = file_name
13
+
14
+ def convert_to_kebab_case(self) -> Self:
15
+ s = re.sub(r'(?<!^)(?=[A-Z])', ' ', self.file_name)
16
+ s = s.lower()
17
+ s = re.sub(r'[^a-z0-9]+', '-', s)
18
+ self.file_name = s.strip('-')
19
+ return self
20
+
21
+ def split_number_and_name(self) -> tuple[str, str]:
22
+ matches = re.match(r'^(\d+)-([a-z0-9-]+)', self.file_name)
23
+ if matches is None:
24
+ raise InvalidUserInput(f"Leetcode name is not valid")
25
+ code = matches.group(1)
26
+ name = matches.group(2)
27
+ return (code, name)
@@ -0,0 +1,63 @@
1
+ import asyncio
2
+ from collections.abc import Coroutine
3
+ import logging
4
+ import sys
5
+ from typing import Any, Callable
6
+
7
+ from deleetify.acp_client.client import DeleetifyClient
8
+ from deleetify.acp_client.client_handler import AcpHandler, create_agent_process
9
+ from .cli.parser import parse_cli_values
10
+ from deleetify.file_name.parser import FileNameProcessor
11
+ from deleetify.processor.config.parser import process_config_toml
12
+ from deleetify.processor.leetcode.parser import parse_leetcode
13
+ from deleetify.processor.leetcode.request import get_leetcode_response
14
+ from deleetify.prompt.agent_prompt import get_prompt
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def exception_then_log_exit[T](
20
+ callback: Callable[..., Coroutine[Any, Any, T]],
21
+ ) -> Callable[..., Coroutine[Any, Any, T]]:
22
+
23
+ async def wrapper(*args, **kwargs) -> T:
24
+ try:
25
+ return await callback(*args, **kwargs)
26
+ except Exception as e:
27
+ logging.error(e)
28
+ sys.exit(1)
29
+
30
+ return wrapper
31
+
32
+
33
+ @exception_then_log_exit
34
+ async def main():
35
+ cli = parse_cli_values()
36
+ config = process_config_toml(cli.config)
37
+
38
+ code, leetcode_name = (
39
+ FileNameProcessor(cli.name).convert_to_kebab_case().split_number_and_name()
40
+ )
41
+
42
+ leetcode_response = get_leetcode_response(leetcode_name)
43
+ leetcode_response = parse_leetcode(leetcode_response, cli.lang)
44
+
45
+ proc = await create_agent_process(config)
46
+ client_impl = DeleetifyClient()
47
+
48
+ client_handler = await AcpHandler.create(client_impl, proc)
49
+
50
+ session = await client_handler.createSession()
51
+
52
+ # Let the agent create file if needed
53
+ file_name = f"{code}-{leetcode_name}.{cli.lang.externsion()}"
54
+
55
+ prompt = get_prompt(file_name, leetcode_response)
56
+ agent_response = await client_handler.prompt(session.session_id, prompt)
57
+ print(f"{agent_response}")
58
+
59
+ proc.terminate()
60
+
61
+
62
+ if __name__ == "__main__":
63
+ asyncio.run(main())
@@ -0,0 +1,4 @@
1
+
2
+ class ConfigParseError(Exception):
3
+ def __init__(self, message: str) -> None:
4
+ super().__init__(message)
@@ -0,0 +1,8 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass
5
+ class Config:
6
+ command: str
7
+ args: list[str]
8
+ envs: dict[str, str]
@@ -0,0 +1,107 @@
1
+ from pathlib import Path
2
+ import tomllib
3
+ from typing import Any
4
+
5
+ from deleetify.processor.config.exceptions import ConfigParseError
6
+ from deleetify.processor.config.model import Config
7
+
8
+
9
+ def read_toml(file: Path) -> dict[str, Any]:
10
+ with open(file.name, "rb") as f:
11
+ return tomllib.load(f)
12
+
13
+
14
+ def select_provider_name(
15
+ providers: dict[str, Any], content: dict[str, Any], path: Path
16
+ ) -> str:
17
+ provider_keys = providers.keys()
18
+ if len(provider_keys) > 1:
19
+ if "default" not in content.keys():
20
+ raise ConfigParseError(f"multiple provider found specify `default` key")
21
+
22
+ default_key = content["default"]
23
+ if not isinstance(default_key, dict):
24
+ raise ConfigParseError(
25
+ f"default key must contain key-value pair in toml: {path.name}"
26
+ )
27
+
28
+ if "provider" not in default_key:
29
+ raise ConfigParseError(f"No provider key in default")
30
+ default_provider = default_key["provider"]
31
+
32
+ if not isinstance(default_provider, str):
33
+ raise ConfigParseError(
34
+ f"provider inside default must contain string but {default_provider}"
35
+ )
36
+
37
+ if default_provider not in providers:
38
+ raise ConfigParseError(f"default provider must be configured")
39
+
40
+ return default_provider
41
+
42
+ if len(provider_keys) == 0:
43
+ raise ConfigParseError(f"No providers defined")
44
+
45
+ selected_provider_name = ""
46
+ for provider_name in provider_keys:
47
+ selected_provider_name = provider_name
48
+
49
+ return selected_provider_name
50
+
51
+
52
+ def parse_provider(provider_name: str, providers: dict[str, Any]) -> Config:
53
+ provider = providers[provider_name]
54
+ if not isinstance(provider, dict):
55
+ raise ConfigParseError(f"Provider {provider_name} config is not valid")
56
+
57
+ provider_keys = provider.keys()
58
+ if "command" not in provider_keys:
59
+ raise ConfigParseError(f"command key in {provider_name} is missing")
60
+
61
+ if not isinstance(provider["command"], str):
62
+ raise ConfigParseError(f"command key is not valid in provider {provider_name}")
63
+
64
+ args: list[str] = []
65
+ if "args" in provider_keys:
66
+ if not isinstance(provider["args"], list):
67
+ raise ConfigParseError(f"args key is not valid in provider {provider_name}")
68
+ for value in provider["args"]:
69
+ if not isinstance(value, str):
70
+ raise ConfigParseError(f"args item `{value}` is not valid in provider {provider_name}")
71
+
72
+ args = provider["args"]
73
+
74
+ envs: dict[str, str] = {}
75
+ if "envs" in provider_keys:
76
+ if not isinstance(provider["envs"], dict):
77
+ raise ConfigParseError(f"envs key is not valid in provider {provider_name}")
78
+ for key, value in provider["envs"].items():
79
+ if not isinstance(key, str):
80
+ raise ConfigParseError(
81
+ f"envs key `{key}` is not valid in provider {provider_name}"
82
+ )
83
+ if not isinstance(value, str):
84
+ raise ConfigParseError(
85
+ f"envs value `{value}` for `{key}` is not valid in provider {provider_name}"
86
+ )
87
+ envs[key] = value
88
+
89
+ config = Config(provider["command"], args, envs)
90
+ return config
91
+
92
+
93
+ def process_config_toml(path: Path) -> Config:
94
+ content = read_toml(path)
95
+
96
+ if "provider" not in content.keys():
97
+ raise ConfigParseError(f"provider key not found in toml: {path.name}")
98
+
99
+ providers = content["provider"]
100
+ if not isinstance(providers, dict):
101
+ raise ConfigParseError(
102
+ f"provider key must contain key-value pair in toml: {path.name}"
103
+ )
104
+
105
+ provider_name = select_provider_name(providers, content, path)
106
+ config = parse_provider(provider_name, providers)
107
+ return config
@@ -0,0 +1,9 @@
1
+
2
+ class LeetCodeRequestError(Exception):
3
+ def __init__(self, message: str) -> None:
4
+ super().__init__(message)
5
+
6
+
7
+ class LeetCodeRequestParseError(Exception):
8
+ def __init__(self, message: str) -> None:
9
+ super().__init__(message)
@@ -0,0 +1,70 @@
1
+ from typing import Any
2
+ from pydantic import BaseModel, Field
3
+
4
+
5
+ class TopicTag(BaseModel):
6
+ name: str
7
+ slug: str
8
+ translated_name: Any = Field(..., alias='translatedName')
9
+
10
+
11
+ class CodeSnippet(BaseModel):
12
+ lang: str
13
+ lang_slug: str = Field(..., alias='langSlug')
14
+ code: str
15
+
16
+
17
+ class Solution(BaseModel):
18
+ id: str
19
+ can_see_detail: bool = Field(..., alias='canSeeDetail')
20
+ paid_only: bool = Field(..., alias='paidOnly')
21
+ has_video_solution: bool = Field(..., alias='hasVideoSolution')
22
+ paid_only_video: bool = Field(..., alias='paidOnlyVideo')
23
+
24
+
25
+ class Question(BaseModel):
26
+ question_id: str = Field(..., alias='questionId')
27
+ question_frontend_id: str = Field(..., alias='questionFrontendId')
28
+ bound_topic_id: Any = Field(..., alias='boundTopicId')
29
+ title: str
30
+ title_slug: str = Field(..., alias='titleSlug')
31
+ content: str
32
+ translated_title: Any = Field(..., alias='translatedTitle')
33
+ translated_content: Any = Field(..., alias='translatedContent')
34
+ is_paid_only: bool = Field(..., alias='isPaidOnly')
35
+ difficulty: str
36
+ likes: int
37
+ dislikes: int
38
+ is_liked: Any = Field(..., alias='isLiked')
39
+ similar_questions: str = Field(..., alias='similarQuestions')
40
+ example_testcases: str = Field(..., alias='exampleTestcases')
41
+ contributors: list[Any]
42
+ topic_tags: list[TopicTag] = Field(..., alias='topicTags')
43
+ company_tag_stats: Any = Field(..., alias='companyTagStats')
44
+ code_snippets: list[CodeSnippet] = Field(..., alias='codeSnippets')
45
+ stats: str
46
+ hints: list[str]
47
+ solution: Solution
48
+ status: Any
49
+ sample_test_case: str = Field(..., alias='sampleTestCase')
50
+ meta_data: str = Field(..., alias='metaData')
51
+ judger_available: bool = Field(..., alias='judgerAvailable')
52
+ judge_type: str = Field(..., alias='judgeType')
53
+ mysql_schemas: list[Any] = Field(..., alias='mysqlSchemas')
54
+ enable_run_code: bool = Field(..., alias='enableRunCode')
55
+ enable_test_mode: bool = Field(..., alias='enableTestMode')
56
+ enable_debugger: bool = Field(..., alias='enableDebugger')
57
+ env_info: str = Field(..., alias='envInfo')
58
+ library_url: Any = Field(..., alias='libraryUrl')
59
+ admin_url: Any = Field(..., alias='adminUrl')
60
+ challenge_question: Any = Field(..., alias='challengeQuestion')
61
+ note: Any
62
+
63
+
64
+ class LeetCodeResponse(BaseModel):
65
+ question: Question
66
+
67
+ class LeetCode(BaseModel):
68
+ description: str
69
+ examples: list[str]
70
+ starter_code: str
@@ -0,0 +1,39 @@
1
+ from deleetify.cli.lang_model import Lang
2
+ from deleetify.processor.leetcode.exceptions import LeetCodeRequestParseError
3
+ from deleetify.processor.leetcode.model import LeetCode, LeetCodeResponse
4
+ from bs4 import BeautifulSoup # type: ignore[import]
5
+
6
+
7
+ def parse_leetcode(leetcode_response: LeetCodeResponse, lang: Lang) -> LeetCode:
8
+ html_content = leetcode_response.question.content
9
+ soup = BeautifulSoup(html_content, "html.parser")
10
+
11
+ desciption = ""
12
+ for p in soup.find_all("p"):
13
+ # Skip empty paragraphs, "Constraints", and "Example X:" placeholders
14
+ text = p.get_text(strip=True)
15
+ if not text or "Constraints:" in text or "Example" in text:
16
+ continue
17
+ # Print the clean text of the description
18
+ desciption = p.get_text().strip()
19
+
20
+ examples: list[str] = []
21
+ for pre in soup.find_all("pre"):
22
+ # .get_text() extracts text inside <pre>, <strong>, etc., cleanly
23
+ # we split by lines and remove unnecessary leading/trailing spaces
24
+ lines = [line.strip() for line in pre.get_text().split("\\n") if line.strip()]
25
+
26
+ examples.append("\n".join(lines))
27
+
28
+ code_snp_list = list(
29
+ filter(lambda x: x.lang_slug == str(lang), leetcode_response.question.code_snippets)
30
+ )
31
+ if len(code_snp_list) == 0:
32
+ raise LeetCodeRequestParseError(f"Leetcode dont support {lang}")
33
+
34
+ code_snp = code_snp_list[0]
35
+ starter_code = code_snp.code
36
+
37
+ return LeetCode(
38
+ description=desciption, examples=examples, starter_code=starter_code
39
+ )
@@ -0,0 +1,37 @@
1
+ import logging
2
+ import httpx # type: ignore[import]
3
+ from pydantic import ValidationError # type: ignore[import]
4
+ from deleetify.processor.leetcode.exceptions import (
5
+ LeetCodeRequestError,
6
+ LeetCodeRequestParseError,
7
+ )
8
+ from deleetify.processor.leetcode.model import LeetCodeResponse
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ BASE_URL = "https://alfa-leetcode-api.onrender.com/"
13
+
14
+
15
+ def get_question_url(name: str):
16
+ return BASE_URL + f"select/raw?titleSlug={name}"
17
+
18
+
19
+ def get_leetcode_response(name: str) -> LeetCodeResponse:
20
+ logging.info(f"Getting problem {name} from leetcode")
21
+ try:
22
+ response = httpx.get(get_question_url(name))
23
+ _ = response.raise_for_status()
24
+ except httpx.HTTPError as e:
25
+ logging.error(f"{e}")
26
+ raise LeetCodeRequestError(
27
+ "Something went wrong while fetching leetcode problem"
28
+ )
29
+
30
+ try:
31
+ response_json = response.json()
32
+ validated_resp = LeetCodeResponse.model_validate(response_json)
33
+ except ValidationError:
34
+ raise LeetCodeRequestParseError(
35
+ "Something went wrong in parsing leetcode response"
36
+ )
37
+ return validated_resp
@@ -0,0 +1,25 @@
1
+ from deleetify.processor.leetcode.model import LeetCode
2
+
3
+
4
+ def get_prompt(file_path: str, leetcode: LeetCode) -> str:
5
+ prompt = f"""
6
+ Act as a raw text generator for an automated file-writing agent. Generate a clean, modern boilerplate script for the LeetCode problem description provided below, written in the target language of the starter code.
7
+
8
+ Follow these structural requirements strictly:
9
+ 1. Include all necessary standard library imports and use modern idiomatic type definitions for the target language (avoid deprecated types or legacy wrappers like in python use `list`, `dict`, and `type | None` instead of 'List', 'Dict', 'Optional' respectively).
10
+ 2. Inside the primary class/module, implement a static method named bootstrap (or equivalent global runner function if the language is non-OOP). This method must accept parameters representing a single test case, instantiate the solution class, call the target function with those inputs, and clean-print both the inputs and the returned results to standard output.
11
+ 3. Keep the target solution method completely unimplemented (stubbed with a basic return or empty block), matching the signature of the original LeetCode starter code.
12
+ 4. Call the bootstrap method individually for each separate example test case at the entry point of the script (e.g., if there are 3 example test cases, call the bootstrap runner 3 distinct times with the respective arguments).
13
+
14
+ Write this to @{file_path}
15
+
16
+ Here is the problem details:
17
+ {leetcode.description}
18
+
19
+ {leetcode.examples}
20
+
21
+ Starter code:
22
+ {leetcode.starter_code}
23
+ """
24
+
25
+ return prompt
@@ -0,0 +1,233 @@
1
+ version = 1
2
+ revision = 3
3
+ requires-python = ">=3.13"
4
+
5
+ [[package]]
6
+ name = "agent-client-protocol"
7
+ version = "0.10.1"
8
+ source = { registry = "https://pypi.org/simple" }
9
+ dependencies = [
10
+ { name = "pydantic" },
11
+ ]
12
+ sdist = { url = "https://files.pythonhosted.org/packages/88/a0/3b96cd8374725c69bc3dae9fcc2082f3f6cafec1be35d24d7af0f8c3265f/agent_client_protocol-0.10.1.tar.gz", hash = "sha256:355c65ca19f0568344aafc2c1552b7066a8fc491df23ab28e7e253c6c9a85a25", size = 81924, upload-time = "2026-05-24T18:46:44.444Z" }
13
+ wheels = [
14
+ { url = "https://files.pythonhosted.org/packages/7b/18/d8c7ff337cf621ea79a84006a7252ff057bfb5767549bb102cc6649f4ec2/agent_client_protocol-0.10.1-py3-none-any.whl", hash = "sha256:a03d3198f4d772f2e0ec012c00ac1cce131b4710220a3dc9fae3c991d047c750", size = 65401, upload-time = "2026-05-24T18:46:43.202Z" },
15
+ ]
16
+
17
+ [[package]]
18
+ name = "annotated-types"
19
+ version = "0.7.0"
20
+ source = { registry = "https://pypi.org/simple" }
21
+ sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
22
+ wheels = [
23
+ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
24
+ ]
25
+
26
+ [[package]]
27
+ name = "anyio"
28
+ version = "4.13.0"
29
+ source = { registry = "https://pypi.org/simple" }
30
+ dependencies = [
31
+ { name = "idna" },
32
+ ]
33
+ sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" }
34
+ wheels = [
35
+ { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" },
36
+ ]
37
+
38
+ [[package]]
39
+ name = "beautifulsoup4"
40
+ version = "4.15.0"
41
+ source = { registry = "https://pypi.org/simple" }
42
+ dependencies = [
43
+ { name = "soupsieve" },
44
+ { name = "typing-extensions" },
45
+ ]
46
+ sdist = { url = "https://files.pythonhosted.org/packages/43/65/318323f98dbee45d42dff61d8f047181bc6f2268a9068cfad035a46be5af/beautifulsoup4-4.15.0.tar.gz", hash = "sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7", size = 632571, upload-time = "2026-06-07T16:44:20.453Z" }
47
+ wheels = [
48
+ { url = "https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl", hash = "sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9", size = 109924, upload-time = "2026-06-07T16:44:21.566Z" },
49
+ ]
50
+
51
+ [[package]]
52
+ name = "certifi"
53
+ version = "2026.5.20"
54
+ source = { registry = "https://pypi.org/simple" }
55
+ sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" }
56
+ wheels = [
57
+ { url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" },
58
+ ]
59
+
60
+ [[package]]
61
+ name = "deleetify"
62
+ version = "0.1.0"
63
+ source = { editable = "." }
64
+ dependencies = [
65
+ { name = "agent-client-protocol" },
66
+ { name = "beautifulsoup4" },
67
+ { name = "httpx" },
68
+ { name = "typed-clap" },
69
+ ]
70
+
71
+ [package.metadata]
72
+ requires-dist = [
73
+ { name = "agent-client-protocol", specifier = ">=0.10.1" },
74
+ { name = "beautifulsoup4", specifier = ">=4.15.0" },
75
+ { name = "httpx", specifier = ">=0.28.1" },
76
+ { name = "typed-clap", specifier = ">=0.11.1" },
77
+ ]
78
+
79
+ [[package]]
80
+ name = "h11"
81
+ version = "0.16.0"
82
+ source = { registry = "https://pypi.org/simple" }
83
+ sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
84
+ wheels = [
85
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
86
+ ]
87
+
88
+ [[package]]
89
+ name = "httpcore"
90
+ version = "1.0.9"
91
+ source = { registry = "https://pypi.org/simple" }
92
+ dependencies = [
93
+ { name = "certifi" },
94
+ { name = "h11" },
95
+ ]
96
+ sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
97
+ wheels = [
98
+ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
99
+ ]
100
+
101
+ [[package]]
102
+ name = "httpx"
103
+ version = "0.28.1"
104
+ source = { registry = "https://pypi.org/simple" }
105
+ dependencies = [
106
+ { name = "anyio" },
107
+ { name = "certifi" },
108
+ { name = "httpcore" },
109
+ { name = "idna" },
110
+ ]
111
+ sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
112
+ wheels = [
113
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
114
+ ]
115
+
116
+ [[package]]
117
+ name = "idna"
118
+ version = "3.18"
119
+ source = { registry = "https://pypi.org/simple" }
120
+ sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" }
121
+ wheels = [
122
+ { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
123
+ ]
124
+
125
+ [[package]]
126
+ name = "pydantic"
127
+ version = "2.13.4"
128
+ source = { registry = "https://pypi.org/simple" }
129
+ dependencies = [
130
+ { name = "annotated-types" },
131
+ { name = "pydantic-core" },
132
+ { name = "typing-extensions" },
133
+ { name = "typing-inspection" },
134
+ ]
135
+ sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" }
136
+ wheels = [
137
+ { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" },
138
+ ]
139
+
140
+ [[package]]
141
+ name = "pydantic-core"
142
+ version = "2.46.4"
143
+ source = { registry = "https://pypi.org/simple" }
144
+ dependencies = [
145
+ { name = "typing-extensions" },
146
+ ]
147
+ sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" }
148
+ wheels = [
149
+ { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" },
150
+ { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" },
151
+ { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" },
152
+ { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" },
153
+ { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" },
154
+ { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" },
155
+ { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" },
156
+ { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" },
157
+ { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" },
158
+ { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" },
159
+ { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" },
160
+ { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" },
161
+ { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" },
162
+ { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" },
163
+ { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" },
164
+ { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" },
165
+ { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" },
166
+ { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" },
167
+ { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" },
168
+ { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" },
169
+ { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" },
170
+ { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" },
171
+ { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" },
172
+ { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" },
173
+ { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" },
174
+ { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" },
175
+ { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" },
176
+ { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" },
177
+ { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" },
178
+ { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" },
179
+ { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" },
180
+ { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" },
181
+ { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" },
182
+ { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" },
183
+ { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" },
184
+ { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" },
185
+ { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" },
186
+ { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" },
187
+ { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" },
188
+ { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" },
189
+ { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" },
190
+ { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" },
191
+ { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" },
192
+ { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" },
193
+ { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" },
194
+ ]
195
+
196
+ [[package]]
197
+ name = "soupsieve"
198
+ version = "2.8.4"
199
+ source = { registry = "https://pypi.org/simple" }
200
+ sdist = { url = "https://files.pythonhosted.org/packages/47/2c/0a5f6f8ee0d5589e48c7640213ed5175d52cf540a06725b628cc1a45d6ce/soupsieve-2.8.4.tar.gz", hash = "sha256:e121fd02e975c695e4e9e8774a5ee35d74714b59307868dcc5319ad2d9e3328e", size = 121110, upload-time = "2026-05-24T13:55:57.154Z" }
201
+ wheels = [
202
+ { url = "https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl", hash = "sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65", size = 37304, upload-time = "2026-05-24T13:55:55.406Z" },
203
+ ]
204
+
205
+ [[package]]
206
+ name = "typed-clap"
207
+ version = "0.11.1"
208
+ source = { registry = "https://pypi.org/simple" }
209
+ sdist = { url = "https://files.pythonhosted.org/packages/b6/29/aaa42495a529928db46e9394d1bb195fbf6c0084e965f3558758baee2577/typed_clap-0.11.1.tar.gz", hash = "sha256:f9e91ea7396d7b0e13e4ede1e382f9948502e9fe215d36c09e73939fd5e1331b", size = 23401, upload-time = "2026-01-31T18:06:42.618Z" }
210
+ wheels = [
211
+ { url = "https://files.pythonhosted.org/packages/05/df/ecb643841e36f28fd333632be9a7dae67e8c66e71541cc9f7a630089d137/typed_clap-0.11.1-py3-none-any.whl", hash = "sha256:e4985777e0492a59da6b05318ed7f3c06b711fdf2378e43e8897d370f6764418", size = 26867, upload-time = "2026-01-31T18:06:43.309Z" },
212
+ ]
213
+
214
+ [[package]]
215
+ name = "typing-extensions"
216
+ version = "4.15.0"
217
+ source = { registry = "https://pypi.org/simple" }
218
+ sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
219
+ wheels = [
220
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
221
+ ]
222
+
223
+ [[package]]
224
+ name = "typing-inspection"
225
+ version = "0.4.2"
226
+ source = { registry = "https://pypi.org/simple" }
227
+ dependencies = [
228
+ { name = "typing-extensions" },
229
+ ]
230
+ sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
231
+ wheels = [
232
+ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
233
+ ]