llmcall 0.1.0rc1__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.
- llmcall-0.1.0rc1/PKG-INFO +111 -0
- llmcall-0.1.0rc1/README.md +84 -0
- llmcall-0.1.0rc1/llmcall/__init__.py +5 -0
- llmcall-0.1.0rc1/llmcall/core.py +26 -0
- llmcall-0.1.0rc1/llmcall/extract.py +53 -0
- llmcall-0.1.0rc1/llmcall/extract_utils.py +20 -0
- llmcall-0.1.0rc1/llmcall/generate.py +125 -0
- llmcall-0.1.0rc1/pyproject.toml +40 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: llmcall
|
|
3
|
+
Version: 0.1.0rc1
|
|
4
|
+
Summary: A lite abstraction layer for LLM calls
|
|
5
|
+
Home-page: https://github.com/rihoneailabs/llmcall
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Keywords: llm,ai,litellm,structure-outputs,openai,pydantic
|
|
8
|
+
Author: Ndamulelo Nemakhavhani
|
|
9
|
+
Author-email: info@rihonegroup.com
|
|
10
|
+
Requires-Python: >=3.11,<4.0
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Requires-Dist: environs (>=11.2.1,<12.0.0)
|
|
19
|
+
Requires-Dist: litellm (>=1.56.4,<2.0.0)
|
|
20
|
+
Requires-Dist: openai (>=1.58.1,<2.0.0)
|
|
21
|
+
Requires-Dist: pydantic (>=2.0.0)
|
|
22
|
+
Requires-Dist: pydantic-settings (>=2.0.0)
|
|
23
|
+
Requires-Dist: tenacity (>=9.0.0,<10.0.0)
|
|
24
|
+
Project-URL: Repository, https://github.com/rihoneailabs/llmcall.git
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# LLMCall
|
|
28
|
+
|
|
29
|
+
A lite abstraction layer for LLM calls.
|
|
30
|
+
|
|
31
|
+
## Motivation
|
|
32
|
+
|
|
33
|
+
As AI becomes more prevalent in software development, there's a growing need for simple and intuitive APIs for interacting with AI for quick text generation, decision making, and more. This is especially important now that we have structured outputs, which allow us to seamlessly integrate AI into our application flow.
|
|
34
|
+
|
|
35
|
+
`llmcall` provides a minimal, batteries-included interface for common LLM operations without unnecessary complexity.
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install llmcall
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Example Usage
|
|
44
|
+
|
|
45
|
+
### Generation
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from llmcall import generate, generate_decision
|
|
49
|
+
from pydantic import BaseModel
|
|
50
|
+
|
|
51
|
+
# i. Basic generation
|
|
52
|
+
response = generate("Write a story about a fictional holiday to the sun.")
|
|
53
|
+
|
|
54
|
+
# ii. Structured generation
|
|
55
|
+
class ResponseSchema(BaseModel):
|
|
56
|
+
story: str
|
|
57
|
+
tags: list[str]
|
|
58
|
+
|
|
59
|
+
response: ResponseSchema = generate("Create a rare story about the history of civilisation.", output_schema=schema)
|
|
60
|
+
|
|
61
|
+
# iii. Decision making
|
|
62
|
+
decision = generate_decision(
|
|
63
|
+
"Which is bigger?",
|
|
64
|
+
options=["apple", "berry", "pumpkin"]
|
|
65
|
+
)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Extraction
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from llmcall import extract
|
|
72
|
+
from pydantic import BaseModel
|
|
73
|
+
|
|
74
|
+
class ResponseSchema(BaseModel):
|
|
75
|
+
email_subject: str
|
|
76
|
+
email_body: str
|
|
77
|
+
email_topic: str
|
|
78
|
+
email_sentiment: str
|
|
79
|
+
|
|
80
|
+
text = """To whom it may concern,
|
|
81
|
+
|
|
82
|
+
Request for Admission at Harvard University
|
|
83
|
+
|
|
84
|
+
I write to plead with the admission board to consider my application for the 2022/2023 academic year. I am a dedicated student with a passion for computer science and a strong desire to make a difference in the world. I believe that Harvard University is the perfect place for me to achieve my dreams and make a positive impact on society."""
|
|
85
|
+
|
|
86
|
+
response: ResponseSchema = extract(text=text, output_schema=ResponseSchema)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Configuration
|
|
90
|
+
|
|
91
|
+
Set environment variables:
|
|
92
|
+
- LLMCALL_API_KEY: Your API key
|
|
93
|
+
- LLMCALL_MODEL: Model to use (default: `openai/gpt-4o-2024-08-06`)
|
|
94
|
+
|
|
95
|
+
> **Note**: We recommend using `Open AI` as the model provider due to their robust support for structured outputs. You can use other providers by setting the `LLMCALL_MODEL` or changing the [config](./llmcall/core.py) directly. Any model supported by `LiteLLM` can be used.
|
|
96
|
+
|
|
97
|
+
## Roadmap
|
|
98
|
+
|
|
99
|
+
- [x] Simple API for generating unstructured text
|
|
100
|
+
- [x] Structured output generation using `Pydantic`
|
|
101
|
+
- [x] Decision making
|
|
102
|
+
- [x] Custom model selection (via `LiteLLM` - See [documentation](https://docs.litellm.ai/docs/providers))
|
|
103
|
+
- [x] Structured text extraction
|
|
104
|
+
- [ ] Structured text extraction from PDF, Docx, etc.
|
|
105
|
+
- [ ] Structured text extraction from Images
|
|
106
|
+
- [ ] Structured text extraction from Websites
|
|
107
|
+
|
|
108
|
+
## Documentation
|
|
109
|
+
|
|
110
|
+
Please refer to our comprehensive [documentation](./docs/index.md) to learn more about this tool.
|
|
111
|
+
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# LLMCall
|
|
2
|
+
|
|
3
|
+
A lite abstraction layer for LLM calls.
|
|
4
|
+
|
|
5
|
+
## Motivation
|
|
6
|
+
|
|
7
|
+
As AI becomes more prevalent in software development, there's a growing need for simple and intuitive APIs for interacting with AI for quick text generation, decision making, and more. This is especially important now that we have structured outputs, which allow us to seamlessly integrate AI into our application flow.
|
|
8
|
+
|
|
9
|
+
`llmcall` provides a minimal, batteries-included interface for common LLM operations without unnecessary complexity.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install llmcall
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Example Usage
|
|
18
|
+
|
|
19
|
+
### Generation
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from llmcall import generate, generate_decision
|
|
23
|
+
from pydantic import BaseModel
|
|
24
|
+
|
|
25
|
+
# i. Basic generation
|
|
26
|
+
response = generate("Write a story about a fictional holiday to the sun.")
|
|
27
|
+
|
|
28
|
+
# ii. Structured generation
|
|
29
|
+
class ResponseSchema(BaseModel):
|
|
30
|
+
story: str
|
|
31
|
+
tags: list[str]
|
|
32
|
+
|
|
33
|
+
response: ResponseSchema = generate("Create a rare story about the history of civilisation.", output_schema=schema)
|
|
34
|
+
|
|
35
|
+
# iii. Decision making
|
|
36
|
+
decision = generate_decision(
|
|
37
|
+
"Which is bigger?",
|
|
38
|
+
options=["apple", "berry", "pumpkin"]
|
|
39
|
+
)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Extraction
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from llmcall import extract
|
|
46
|
+
from pydantic import BaseModel
|
|
47
|
+
|
|
48
|
+
class ResponseSchema(BaseModel):
|
|
49
|
+
email_subject: str
|
|
50
|
+
email_body: str
|
|
51
|
+
email_topic: str
|
|
52
|
+
email_sentiment: str
|
|
53
|
+
|
|
54
|
+
text = """To whom it may concern,
|
|
55
|
+
|
|
56
|
+
Request for Admission at Harvard University
|
|
57
|
+
|
|
58
|
+
I write to plead with the admission board to consider my application for the 2022/2023 academic year. I am a dedicated student with a passion for computer science and a strong desire to make a difference in the world. I believe that Harvard University is the perfect place for me to achieve my dreams and make a positive impact on society."""
|
|
59
|
+
|
|
60
|
+
response: ResponseSchema = extract(text=text, output_schema=ResponseSchema)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Configuration
|
|
64
|
+
|
|
65
|
+
Set environment variables:
|
|
66
|
+
- LLMCALL_API_KEY: Your API key
|
|
67
|
+
- LLMCALL_MODEL: Model to use (default: `openai/gpt-4o-2024-08-06`)
|
|
68
|
+
|
|
69
|
+
> **Note**: We recommend using `Open AI` as the model provider due to their robust support for structured outputs. You can use other providers by setting the `LLMCALL_MODEL` or changing the [config](./llmcall/core.py) directly. Any model supported by `LiteLLM` can be used.
|
|
70
|
+
|
|
71
|
+
## Roadmap
|
|
72
|
+
|
|
73
|
+
- [x] Simple API for generating unstructured text
|
|
74
|
+
- [x] Structured output generation using `Pydantic`
|
|
75
|
+
- [x] Decision making
|
|
76
|
+
- [x] Custom model selection (via `LiteLLM` - See [documentation](https://docs.litellm.ai/docs/providers))
|
|
77
|
+
- [x] Structured text extraction
|
|
78
|
+
- [ ] Structured text extraction from PDF, Docx, etc.
|
|
79
|
+
- [ ] Structured text extraction from Images
|
|
80
|
+
- [ ] Structured text extraction from Websites
|
|
81
|
+
|
|
82
|
+
## Documentation
|
|
83
|
+
|
|
84
|
+
Please refer to our comprehensive [documentation](./docs/index.md) to learn more about this tool.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ModelConfig(BaseSettings):
|
|
6
|
+
temperature: float = 0.2
|
|
7
|
+
stream: bool = False
|
|
8
|
+
n: Optional[int] = 1
|
|
9
|
+
max_tokens: int = 1024
|
|
10
|
+
num_retries: int = 3
|
|
11
|
+
seed: Optional[int] = 47
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class LLMConfig(BaseSettings):
|
|
15
|
+
model_config = SettingsConfigDict(
|
|
16
|
+
case_sensitive=False,
|
|
17
|
+
env_prefix="LLMCALL_",
|
|
18
|
+
extra="ignore",
|
|
19
|
+
)
|
|
20
|
+
api_key: str
|
|
21
|
+
model: str = "openai/gpt-4o-2024-08-06"
|
|
22
|
+
debug: bool = False
|
|
23
|
+
llm: ModelConfig = ModelConfig()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
config = LLMConfig()
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import time
|
|
4
|
+
from typing import Optional, Union
|
|
5
|
+
from typing_extensions import Annotated
|
|
6
|
+
|
|
7
|
+
from litellm import completion
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
from llmcall.core import config
|
|
11
|
+
|
|
12
|
+
_logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def extract(
|
|
16
|
+
text: Annotated[str, "The unstructured text to extract information from."],
|
|
17
|
+
output_schema: Annotated[BaseModel, "The Pydantic model to use for response structure validation."],
|
|
18
|
+
instructions: Annotated[Optional[str], "System metaprompt to condition the model."] = None,
|
|
19
|
+
) -> Union[str, BaseModel]:
|
|
20
|
+
"""Extract structured information from unstructured text using configured LLM."""
|
|
21
|
+
DEFAULT_SYSTEM_PROMPT = """You are a specialist in organising unstructured data. Given the document below, \
|
|
22
|
+
your task is to extract the requested information as accurately as you can. \
|
|
23
|
+
###
|
|
24
|
+
<document>{text}</document>
|
|
25
|
+
###"""
|
|
26
|
+
|
|
27
|
+
if not text:
|
|
28
|
+
raise ValueError("Text cannot be empty.")
|
|
29
|
+
|
|
30
|
+
start = time.perf_counter()
|
|
31
|
+
_logger.info(f"Extracting information from text: {text[:50]}")
|
|
32
|
+
|
|
33
|
+
if instructions:
|
|
34
|
+
messages = [
|
|
35
|
+
{"content": instructions, "role": "system"},
|
|
36
|
+
{"content": text, "role": "user"},
|
|
37
|
+
]
|
|
38
|
+
else:
|
|
39
|
+
messages = [
|
|
40
|
+
{"content": DEFAULT_SYSTEM_PROMPT.format(text=text), "role": "system"},
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
response = completion(
|
|
44
|
+
api_key=config.api_key,
|
|
45
|
+
model=config.model,
|
|
46
|
+
messages=messages,
|
|
47
|
+
response_format=output_schema,
|
|
48
|
+
**config.llm.model_dump(),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
_logger.info(f"Extraction completed in {time.perf_counter() - start:.2f} seconds.")
|
|
52
|
+
|
|
53
|
+
return output_schema.model_validate(json.loads(response.choices[0].message.content), strict=True)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
|
|
2
|
+
def extract_from_doc(pdf_path, page_num, pdf_type):
|
|
3
|
+
"""
|
|
4
|
+
Extracts text from a PDF file.
|
|
5
|
+
"""
|
|
6
|
+
raise NotImplementedError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def extract_from_image(image_path):
|
|
10
|
+
"""
|
|
11
|
+
Extracts text from an image file.
|
|
12
|
+
"""
|
|
13
|
+
raise NotImplementedError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def extract_from_webpage(url):
|
|
17
|
+
"""
|
|
18
|
+
Extracts text from a webpage.
|
|
19
|
+
"""
|
|
20
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import time
|
|
4
|
+
from typing import Optional, Union
|
|
5
|
+
from typing_extensions import Annotated
|
|
6
|
+
|
|
7
|
+
import litellm
|
|
8
|
+
from litellm import completion
|
|
9
|
+
from litellm import supports_response_schema
|
|
10
|
+
from pydantic import BaseModel
|
|
11
|
+
|
|
12
|
+
from llmcall.core import config
|
|
13
|
+
|
|
14
|
+
_logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Decision(BaseModel):
|
|
18
|
+
selection: Annotated[str, "The selected option - MUST be one of the provided options."]
|
|
19
|
+
prompt: Optional[str] = None
|
|
20
|
+
options: Optional[list[str]] = None
|
|
21
|
+
reason: Optional[str] = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def generate(
|
|
25
|
+
prompt: Annotated[str, "The user prompt which tells the model what to generate."],
|
|
26
|
+
output_schema: Annotated[
|
|
27
|
+
Optional[BaseModel], "The Pydantic model to use for response structure validation(optional)"
|
|
28
|
+
] = None,
|
|
29
|
+
instructions: Annotated[Optional[str], "System metaprompt to condition the model."] = None,
|
|
30
|
+
) -> Union[str, BaseModel]:
|
|
31
|
+
"""Generate content using configured LLM."""
|
|
32
|
+
|
|
33
|
+
if not prompt:
|
|
34
|
+
raise ValueError("Prompt cannot be empty.")
|
|
35
|
+
|
|
36
|
+
DEFAULT_SYSTEM_PROMPT = "Generate content based on the following: <prompt>{prompt}</prompt>. \
|
|
37
|
+
Return only the content with no additional information or comments."
|
|
38
|
+
|
|
39
|
+
_logger.debug(f"Generating content for prompt: {prompt[:20]}..")
|
|
40
|
+
start = time.perf_counter()
|
|
41
|
+
|
|
42
|
+
if output_schema:
|
|
43
|
+
if not supports_response_schema(
|
|
44
|
+
model=config.model.split("/")[1], custom_llm_provider=config.model.split("/")[0]
|
|
45
|
+
):
|
|
46
|
+
raise ValueError(
|
|
47
|
+
f"Response schema is not supported by the configured model: {config.model}. "
|
|
48
|
+
"Please use a different model(e.g. openai/gpt-4o-2024-08-06) or remove the output schema."
|
|
49
|
+
)
|
|
50
|
+
litellm.enable_json_schema_validation = True
|
|
51
|
+
|
|
52
|
+
response = completion(
|
|
53
|
+
api_key=config.api_key,
|
|
54
|
+
model=config.model,
|
|
55
|
+
messages=[
|
|
56
|
+
{"content": instructions or DEFAULT_SYSTEM_PROMPT, "role": "system"},
|
|
57
|
+
{"content": prompt, "role": "user"},
|
|
58
|
+
],
|
|
59
|
+
response_format=output_schema,
|
|
60
|
+
**config.llm.model_dump(),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
_logger.debug(f"Generated content in {time.perf_counter() - start:.2f}s")
|
|
64
|
+
|
|
65
|
+
if output_schema:
|
|
66
|
+
return output_schema.model_validate(json.loads(response.choices[0].message.content), strict=True)
|
|
67
|
+
|
|
68
|
+
return response.choices[0].message.content
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def generate_decision(
|
|
72
|
+
prompt: Annotated[str, "The context to consider when making the decision."],
|
|
73
|
+
options: Annotated[list[str], "List of options to choose from."],
|
|
74
|
+
instructions: Annotated[Optional[str], "System metaprompt to condition the model."] = None,
|
|
75
|
+
) -> Decision:
|
|
76
|
+
"""Generate a decision from a list of options."""
|
|
77
|
+
|
|
78
|
+
if not prompt:
|
|
79
|
+
raise ValueError("Prompt cannot be empty.")
|
|
80
|
+
|
|
81
|
+
DEFAULT_SYSTEM_PROMPT = """You are a specialized computer algorithm designed to make decisions in Control Flow scenarios. \
|
|
82
|
+
Your task is to analyze the given context and options, then select the most appropriate option based on the context. Here is the context you need to consider: \
|
|
83
|
+
<context>
|
|
84
|
+
{{CONTEXT}}
|
|
85
|
+
</context>
|
|
86
|
+
|
|
87
|
+
Here are the options you can choose from:
|
|
88
|
+
<options>
|
|
89
|
+
{{OPTIONS}}
|
|
90
|
+
</options>"""
|
|
91
|
+
|
|
92
|
+
_logger.debug(f"Generating decision given options: {options} and prompt: {prompt[:20]}..")
|
|
93
|
+
start = time.perf_counter()
|
|
94
|
+
|
|
95
|
+
if instructions:
|
|
96
|
+
messages = [
|
|
97
|
+
{
|
|
98
|
+
"content": instructions,
|
|
99
|
+
"role": "system",
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"content": "Pick one of the following options: <options>{options}</options>, given the following query:\n<query>{prompt}</query>.",
|
|
103
|
+
"role": "user",
|
|
104
|
+
},
|
|
105
|
+
]
|
|
106
|
+
else:
|
|
107
|
+
messages = [
|
|
108
|
+
{
|
|
109
|
+
"content": DEFAULT_SYSTEM_PROMPT.strip()
|
|
110
|
+
.replace("{{CONTEXT}}", prompt)
|
|
111
|
+
.replace("{{OPTIONS}}", "\n".join(options)),
|
|
112
|
+
"role": "user",
|
|
113
|
+
},
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
response = completion(
|
|
117
|
+
api_key=config.api_key,
|
|
118
|
+
model=config.model,
|
|
119
|
+
messages=messages,
|
|
120
|
+
response_format=Decision,
|
|
121
|
+
**config.llm.model_dump(),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
_logger.debug(f"Generated decision in {time.perf_counter() - start:.2f}s")
|
|
125
|
+
return Decision.model_validate(json.loads(response.choices[0].message.content), strict=True)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "llmcall"
|
|
3
|
+
version = "0.1.0rc1"
|
|
4
|
+
description = "A lite abstraction layer for LLM calls"
|
|
5
|
+
authors = ["Ndamulelo Nemakhavhani <info@rihonegroup.com>"]
|
|
6
|
+
license = "Apache-2.0"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
keywords = ["llm", "ai", "litellm", "structure-outputs", "openai", "pydantic"]
|
|
9
|
+
repository = "https://github.com/rihoneailabs/llmcall.git"
|
|
10
|
+
homepage = "https://github.com/rihoneailabs/llmcall"
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 3 - Alpha",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"License :: OSI Approved :: Apache Software License",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.11",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Programming Language :: Python :: 3.13",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[tool.poetry.dependencies]
|
|
22
|
+
python = ">=3.11, <4.0"
|
|
23
|
+
pydantic = ">=2.0.0"
|
|
24
|
+
pydantic-settings = ">=2.0.0"
|
|
25
|
+
openai = "^1.58.1"
|
|
26
|
+
litellm = "^1.56.4"
|
|
27
|
+
environs = "^11.2.1"
|
|
28
|
+
tenacity = "^9.0.0"
|
|
29
|
+
|
|
30
|
+
[tool.poetry.group.dev.dependencies]
|
|
31
|
+
pytest = "^8.3.4"
|
|
32
|
+
black = "^24.10.0"
|
|
33
|
+
isort = "^5.13.2"
|
|
34
|
+
mypy = "^1.14.0"
|
|
35
|
+
tox = "^4.23.2"
|
|
36
|
+
pytest-cov = "^6.0.0"
|
|
37
|
+
|
|
38
|
+
[build-system]
|
|
39
|
+
requires = ["poetry-core"]
|
|
40
|
+
build-backend = "poetry.core.masonry.api"
|