Spaces:
Sleeping
Sleeping
updated configuration to pydantic
Browse files- README.md +23 -0
- docs/DEVELOPER.md +25 -0
- pstuts_rag/pstuts_rag/configuration.py +88 -62
- pyproject.toml +1 -0
- uv.lock +2 -0
README.md
CHANGED
|
@@ -61,3 +61,26 @@ chainlit run app.py
|
|
| 61 |
- Web search integration via Tavily
|
| 62 |
- Semantic chunking for better context retrieval
|
| 63 |
- Interactive chat interface through Chainlit
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
- Web search integration via Tavily
|
| 62 |
- Semantic chunking for better context retrieval
|
| 63 |
- Interactive chat interface through Chainlit
|
| 64 |
+
|
| 65 |
+
## βοΈ Configuration Options
|
| 66 |
+
|
| 67 |
+
You can customize the behavior of PsTuts RAG using environment variables. Set these in your shell, `.env` file, or deployment environment. Here are the available options:
|
| 68 |
+
|
| 69 |
+
| Env Var | Description |
|
| 70 |
+
|---------|-------------|
|
| 71 |
+
| `EVA_WORKFLOW_NAME` | π·οΈ Name of the EVA workflow. Default: `EVA_workflow` |
|
| 72 |
+
| `EVA_LOG_LEVEL` | πͺ΅ Logging level for EVA. Default: `INFO` |
|
| 73 |
+
| `TRANSCRIPT_GLOB` | π Glob pattern for transcript JSON files (supports multiple files separated by `:`). Default: `data/test.json` |
|
| 74 |
+
| `EMBEDDING_MODEL` | π§ Name of the embedding model to use (default: custom fine-tuned snowflake model). Default: `mbudisic/snowflake-arctic-embed-s-ft-pstuts` |
|
| 75 |
+
| `EVA_STRIP_THINK` | π If set (present in env), strips 'think' steps from EVA output. |
|
| 76 |
+
| `EMBEDDING_API` | π API provider for embeddings (`OPENAI`, `HUGGINGFACE`, or `OLLAMA`). Default: `HUGGINGFACE` |
|
| 77 |
+
| `LLM_API` | π€ API provider for LLM (`OPENAI`, `HUGGINGFACE`, or `OLLAMA`). Default: `OLLAMA` |
|
| 78 |
+
| `MAX_RESEARCH_LOOPS` | π Maximum number of research loops to perform. Default: `3` |
|
| 79 |
+
| `LLM_TOOL_MODEL` | π οΈ Name of the LLM model to use for tool calling. Default: `smollm2:1.7b-instruct-q2_K` |
|
| 80 |
+
| `N_CONTEXT_DOCS` | π Number of context documents to retrieve for RAG. Default: `2` |
|
| 81 |
+
| `EVA_SEARCH_PERMISSION` | π Permission for search (`yes`, `no`, or `ask`). Default: `no` |
|
| 82 |
+
| `EVA_DB_PERSIST` | πΎ Path or flag for DB persistence. Default: unset |
|
| 83 |
+
| `EVA_REINITIALIZE` | π If true, reinitializes EVA DB. Default: `False` |
|
| 84 |
+
| `THREAD_ID` | π§΅ Thread ID for the current session. Default: unset |
|
| 85 |
+
|
| 86 |
+
Set these variables to control model selection, logging, search permissions, and more. For advanced usage, see the developer documentation.
|
docs/DEVELOPER.md
CHANGED
|
@@ -165,6 +165,31 @@ This feature enables controlled access to external resources while maintaining a
|
|
| 165 |
- **`evaluator_utils.py`**: RAG evaluation utilities using RAGAS framework
|
| 166 |
- **Notebook-based evaluation**: `evaluate_rag.ipynb` for systematic testing
|
| 167 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
## π¨ UI Customization & Theming
|
| 169 |
|
| 170 |
### Sepia Theme Implementation πΌοΈ
|
|
|
|
| 165 |
- **`evaluator_utils.py`**: RAG evaluation utilities using RAGAS framework
|
| 166 |
- **Notebook-based evaluation**: `evaluate_rag.ipynb` for systematic testing
|
| 167 |
|
| 168 |
+
### βοΈ Configuration Reference
|
| 169 |
+
|
| 170 |
+
The `Configuration` class (in `pstuts_rag/configuration.py`) is powered by Pydantic and supports environment variable overrides for all fields. Below is a reference for all configuration options:
|
| 171 |
+
|
| 172 |
+
| Field | Env Var | Type | Default | Description |
|
| 173 |
+
|-------|---------|------|---------|-------------|
|
| 174 |
+
| `eva_workflow_name` | `EVA_WORKFLOW_NAME` | `str` | `EVA_workflow` | π·οΈ Name of the EVA workflow |
|
| 175 |
+
| `eva_log_level` | `EVA_LOG_LEVEL` | `str` | `INFO` | πͺ΅ Logging level for EVA |
|
| 176 |
+
| `transcript_glob` | `TRANSCRIPT_GLOB` | `str` | `data/test.json` | π Glob pattern for transcript JSON files (supports `:` for multiple) |
|
| 177 |
+
| `embedding_model` | `EMBEDDING_MODEL` | `str` | `mbudisic/snowflake-arctic-embed-s-ft-pstuts` | π§ Embedding model name (default: custom fine-tuned snowflake) |
|
| 178 |
+
| `eva_strip_think` | `EVA_STRIP_THINK` | `bool` | `False` | π If set (present in env), strips 'think' steps from EVA output |
|
| 179 |
+
| `embedding_api` | `EMBEDDING_API` | `ModelAPI` | `HUGGINGFACE` | π API provider for embeddings (`OPENAI`, `HUGGINGFACE`, `OLLAMA`) |
|
| 180 |
+
| `llm_api` | `LLM_API` | `ModelAPI` | `OLLAMA` | π€ API provider for LLM (`OPENAI`, `HUGGINGFACE`, `OLLAMA`) |
|
| 181 |
+
| `max_research_loops` | `MAX_RESEARCH_LOOPS` | `int` | `3` | π Maximum number of research loops to perform |
|
| 182 |
+
| `llm_tool_model` | `LLM_TOOL_MODEL` | `str` | `smollm2:1.7b-instruct-q2_K` | π οΈ LLM model for tool calling |
|
| 183 |
+
| `n_context_docs` | `N_CONTEXT_DOCS` | `int` | `2` | π Number of context documents to retrieve for RAG |
|
| 184 |
+
| `search_permission` | `EVA_SEARCH_PERMISSION` | `str` | `no` | π Permission for search (`yes`, `no`, `ask`) |
|
| 185 |
+
| `db_persist` | `EVA_DB_PERSIST` | `str or None` | `None` | πΎ Path or flag for DB persistence |
|
| 186 |
+
| `eva_reinitialize` | `EVA_REINITIALIZE` | `bool` | `False` | π If true, reinitializes EVA DB |
|
| 187 |
+
| `thread_id` | `THREAD_ID` | `str` | `""` | π§΅ Thread ID for the current session |
|
| 188 |
+
|
| 189 |
+
- All fields can be set via environment variables (see [Pydantic BaseSettings docs](https://docs.pydantic.dev/latest/usage/settings/)).
|
| 190 |
+
- Types are enforced at runtime. Defaults are shown above.
|
| 191 |
+
- For advanced usage, see the `Configuration` class in `pstuts_rag/configuration.py`.
|
| 192 |
+
|
| 193 |
## π¨ UI Customization & Theming
|
| 194 |
|
| 195 |
### Sepia Theme Implementation πΌοΈ
|
pstuts_rag/pstuts_rag/configuration.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
| 1 |
import os
|
| 2 |
import logging
|
| 3 |
-
from dataclasses import dataclass, fields
|
| 4 |
from typing import Any, Optional
|
| 5 |
from enum import Enum
|
|
|
|
|
|
|
| 6 |
|
| 7 |
from langchain_core.runnables import RunnableConfig
|
| 8 |
|
|
@@ -21,60 +22,94 @@ class ModelAPI(Enum):
|
|
| 21 |
OLLAMA = "OLLAMA"
|
| 22 |
|
| 23 |
|
| 24 |
-
|
| 25 |
-
class Configuration:
|
| 26 |
"""
|
| 27 |
-
Configuration parameters for the application.
|
| 28 |
-
|
| 29 |
-
Attributes:
|
| 30 |
-
transcript_glob: Glob pattern for transcript JSON files (supports multiple files separated by ':')
|
| 31 |
-
embedding_model: Name of the embedding model to use (default: custom fine-tuned snowflake model)
|
| 32 |
-
embedding_api: API provider for embeddings (OPENAI or HUGGINGFACE)
|
| 33 |
-
max_research_loops: Maximum number of research loops to perform
|
| 34 |
-
llm_tool_model: Name of the LLM model to use for tool calling
|
| 35 |
-
n_context_docs: Number of context documents to retrieve for RAG
|
| 36 |
"""
|
| 37 |
|
| 38 |
-
eva_workflow_name: str =
|
| 39 |
-
os.environ.get(
|
|
|
|
|
|
|
|
|
|
| 40 |
)
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
)
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
"EMBEDDING_MODEL", "mbudisic/snowflake-arctic-embed-s-ft-pstuts"
|
| 51 |
-
)
|
|
|
|
| 52 |
)
|
| 53 |
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
|
|
|
| 58 |
)
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
|
|
|
|
|
|
| 62 |
)
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
)
|
| 69 |
-
n_context_docs: int = int(os.environ.get("N_CONTEXT_DOCS", "2"))
|
| 70 |
-
|
| 71 |
-
search_permission: str = str(os.environ.get("EVA_SEARCH_PERMISSION", "no"))
|
| 72 |
-
|
| 73 |
-
db_persist: str | None = os.environ.get("EVA_DB_PERSIST", None)
|
| 74 |
|
| 75 |
-
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
-
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
@classmethod
|
| 80 |
def from_runnable_config(
|
|
@@ -96,16 +131,14 @@ class Configuration:
|
|
| 96 |
if config and "configurable" in config
|
| 97 |
else {}
|
| 98 |
)
|
| 99 |
-
# Map each
|
| 100 |
# Priority: environment variables > configurable dict values > field defaults
|
| 101 |
values: dict[str, Any] = {
|
| 102 |
-
|
| 103 |
-
for
|
| 104 |
-
if f.init
|
| 105 |
}
|
| 106 |
logging.info("Configuration:\n%s", values)
|
| 107 |
-
|
| 108 |
-
return cls(**{k: v for k, v in values.items() if v})
|
| 109 |
|
| 110 |
def print(self, print_like_function=logging.info) -> None:
|
| 111 |
"""Print all configuration parameters using the provided logging function.
|
|
@@ -117,10 +150,9 @@ class Configuration:
|
|
| 117 |
None
|
| 118 |
"""
|
| 119 |
print_like_function("Configuration parameters:")
|
| 120 |
-
for field in
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
print_like_function(" %s: %s", field.name, value)
|
| 124 |
|
| 125 |
def to_runnable_config(self) -> RunnableConfig:
|
| 126 |
"""Convert Configuration instance to RunnableConfig format.
|
|
@@ -129,16 +161,10 @@ class Configuration:
|
|
| 129 |
RunnableConfig: Properly formatted configuration for LangGraph
|
| 130 |
"""
|
| 131 |
configurable_dict = {}
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
value = getattr(self, field.name)
|
| 137 |
-
if value: # Only include non-empty values
|
| 138 |
-
configurable_dict[field.name] = value
|
| 139 |
-
|
| 140 |
-
# Ensure thread_id is included if set
|
| 141 |
if self.thread_id:
|
| 142 |
configurable_dict["thread_id"] = self.thread_id
|
| 143 |
-
|
| 144 |
return RunnableConfig(configurable=configurable_dict)
|
|
|
|
| 1 |
import os
|
| 2 |
import logging
|
|
|
|
| 3 |
from typing import Any, Optional
|
| 4 |
from enum import Enum
|
| 5 |
+
from pydantic_settings import BaseSettings
|
| 6 |
+
from pydantic import Field
|
| 7 |
|
| 8 |
from langchain_core.runnables import RunnableConfig
|
| 9 |
|
|
|
|
| 22 |
OLLAMA = "OLLAMA"
|
| 23 |
|
| 24 |
|
| 25 |
+
class Configuration(BaseSettings):
|
|
|
|
| 26 |
"""
|
| 27 |
+
Configuration parameters for the application. All fields can be set via environment variables.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
"""
|
| 29 |
|
| 30 |
+
eva_workflow_name: str = Field(
|
| 31 |
+
default_factory=lambda: os.environ.get(
|
| 32 |
+
"EVA_WORKFLOW_NAME", "EVA_workflow"
|
| 33 |
+
),
|
| 34 |
+
description="Name of the EVA workflow. Set via EVA_WORKFLOW_NAME.",
|
| 35 |
)
|
| 36 |
+
eva_log_level: str = Field(
|
| 37 |
+
default_factory=lambda: os.environ.get(
|
| 38 |
+
"EVA_LOG_LEVEL", "INFO"
|
| 39 |
+
).upper(),
|
| 40 |
+
description="Logging level for EVA. Set via EVA_LOG_LEVEL.",
|
| 41 |
)
|
| 42 |
+
transcript_glob: str = Field(
|
| 43 |
+
default_factory=lambda: os.environ.get(
|
| 44 |
+
"TRANSCRIPT_GLOB", "data/test.json"
|
| 45 |
+
),
|
| 46 |
+
description="Glob pattern for transcript JSON files (supports multiple files separated by ':'). Set via TRANSCRIPT_GLOB.",
|
| 47 |
+
)
|
| 48 |
+
embedding_model: str = Field(
|
| 49 |
+
default_factory=lambda: os.environ.get(
|
| 50 |
"EMBEDDING_MODEL", "mbudisic/snowflake-arctic-embed-s-ft-pstuts"
|
| 51 |
+
),
|
| 52 |
+
description="Name of the embedding model to use (default: custom fine-tuned snowflake model). Set via EMBEDDING_MODEL.",
|
| 53 |
)
|
| 54 |
|
| 55 |
+
embedding_api: ModelAPI = Field(
|
| 56 |
+
default_factory=lambda: ModelAPI(
|
| 57 |
+
os.environ.get("EMBEDDING_API", ModelAPI.HUGGINGFACE.value)
|
| 58 |
+
),
|
| 59 |
+
description="API provider for embeddings (OPENAI, HUGGINGFACE, or OLLAMA). Set via EMBEDDING_API.",
|
| 60 |
)
|
| 61 |
+
llm_api: ModelAPI = Field(
|
| 62 |
+
default_factory=lambda: ModelAPI(
|
| 63 |
+
os.environ.get("LLM_API", ModelAPI.OLLAMA.value)
|
| 64 |
+
),
|
| 65 |
+
description="API provider for LLM (OPENAI, HUGGINGFACE, or OLLAMA). Set via LLM_API.",
|
| 66 |
)
|
| 67 |
+
max_research_loops: int = Field(
|
| 68 |
+
default_factory=lambda: int(os.environ.get("MAX_RESEARCH_LOOPS", "3")),
|
| 69 |
+
description="Maximum number of research loops to perform. Set via MAX_RESEARCH_LOOPS.",
|
| 70 |
+
)
|
| 71 |
+
llm_tool_model: str = Field(
|
| 72 |
+
default_factory=lambda: os.environ.get(
|
| 73 |
+
"LLM_TOOL_MODEL", "smollm2:1.7b-instruct-q2_K"
|
| 74 |
+
),
|
| 75 |
+
description="Name of the LLM model to use for tool calling. Set via LLM_TOOL_MODEL.",
|
| 76 |
+
)
|
| 77 |
+
n_context_docs: int = Field(
|
| 78 |
+
default_factory=lambda: int(os.environ.get("N_CONTEXT_DOCS", "2")),
|
| 79 |
+
description="Number of context documents to retrieve for RAG. Set via N_CONTEXT_DOCS.",
|
| 80 |
+
)
|
| 81 |
+
search_permission: str = Field(
|
| 82 |
+
default_factory=lambda: os.environ.get("EVA_SEARCH_PERMISSION", "no"),
|
| 83 |
+
description="Permission for search (yes/no). Set via EVA_SEARCH_PERMISSION.",
|
| 84 |
+
)
|
| 85 |
+
db_persist: Optional[str] = Field(
|
| 86 |
+
default_factory=lambda: os.environ.get("EVA_DB_PERSIST", None),
|
| 87 |
+
description="Path or flag for DB persistence. Set via EVA_DB_PERSIST.",
|
| 88 |
+
)
|
| 89 |
+
eva_reinitialize: bool = Field(
|
| 90 |
+
default_factory=lambda: os.environ.get(
|
| 91 |
+
"EVA_REINITIALIZE", "False"
|
| 92 |
+
).lower()
|
| 93 |
+
in ("true", "1", "yes"),
|
| 94 |
+
description="If true, reinitializes EVA DB. Set via EVA_REINITIALIZE.",
|
| 95 |
+
)
|
| 96 |
+
eva_strip_think: bool = Field(
|
| 97 |
+
default_factory=lambda: os.environ.get(
|
| 98 |
+
"EVA_STRIP_THINK", "True"
|
| 99 |
+
).lower()
|
| 100 |
+
in ("true", "1", "yes"),
|
| 101 |
+
description="If true (default) strips thinking tags from LLM responses. Set via EVA_STRIP_THINK.",
|
| 102 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
|
| 104 |
+
thread_id: str = Field(
|
| 105 |
+
default="",
|
| 106 |
+
description="Thread ID for the current session. Set via THREAD_ID.",
|
| 107 |
+
)
|
| 108 |
|
| 109 |
+
class Config:
|
| 110 |
+
env_file = ".env"
|
| 111 |
+
env_file_encoding = "utf-8"
|
| 112 |
+
extra = "ignore" # Allow extra env vars in .env/environment
|
| 113 |
|
| 114 |
@classmethod
|
| 115 |
def from_runnable_config(
|
|
|
|
| 131 |
if config and "configurable" in config
|
| 132 |
else {}
|
| 133 |
)
|
| 134 |
+
# Map each field to environment variables or configurable values
|
| 135 |
# Priority: environment variables > configurable dict values > field defaults
|
| 136 |
values: dict[str, Any] = {
|
| 137 |
+
name: os.environ.get(name.upper(), configurable.get(name))
|
| 138 |
+
for name in cls.__fields__
|
|
|
|
| 139 |
}
|
| 140 |
logging.info("Configuration:\n%s", values)
|
| 141 |
+
return cls(**{k: v for k, v in values.items() if v is not None})
|
|
|
|
| 142 |
|
| 143 |
def print(self, print_like_function=logging.info) -> None:
|
| 144 |
"""Print all configuration parameters using the provided logging function.
|
|
|
|
| 150 |
None
|
| 151 |
"""
|
| 152 |
print_like_function("Configuration parameters:")
|
| 153 |
+
for name, field in self.__fields__.items():
|
| 154 |
+
value = getattr(self, name)
|
| 155 |
+
print_like_function(" %s: %s", name, value)
|
|
|
|
| 156 |
|
| 157 |
def to_runnable_config(self) -> RunnableConfig:
|
| 158 |
"""Convert Configuration instance to RunnableConfig format.
|
|
|
|
| 161 |
RunnableConfig: Properly formatted configuration for LangGraph
|
| 162 |
"""
|
| 163 |
configurable_dict = {}
|
| 164 |
+
for name in self.__fields__:
|
| 165 |
+
value = getattr(self, name)
|
| 166 |
+
if value:
|
| 167 |
+
configurable_dict[name] = value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
if self.thread_id:
|
| 169 |
configurable_dict["thread_id"] = self.thread_id
|
|
|
|
| 170 |
return RunnableConfig(configurable=configurable_dict)
|
pyproject.toml
CHANGED
|
@@ -52,6 +52,7 @@ dependencies = [
|
|
| 52 |
"langchain-tavily>=0.2.0",
|
| 53 |
"beautifulsoup4>=4.13.4",
|
| 54 |
"pathvalidate>=3.2.3",
|
|
|
|
| 55 |
]
|
| 56 |
authors = [{ name = "Marko Budisic", email = "[email protected]" }]
|
| 57 |
license = "MIT"
|
|
|
|
| 52 |
"langchain-tavily>=0.2.0",
|
| 53 |
"beautifulsoup4>=4.13.4",
|
| 54 |
"pathvalidate>=3.2.3",
|
| 55 |
+
"pydantic-settings>=2.9.1",
|
| 56 |
]
|
| 57 |
authors = [{ name = "Marko Budisic", email = "[email protected]" }]
|
| 58 |
license = "MIT"
|
uv.lock
CHANGED
|
@@ -3777,6 +3777,7 @@ dependencies = [
|
|
| 3777 |
{ name = "pandas" },
|
| 3778 |
{ name = "pathvalidate" },
|
| 3779 |
{ name = "pyarrow" },
|
|
|
|
| 3780 |
{ name = "python-dotenv" },
|
| 3781 |
{ name = "qdrant-client" },
|
| 3782 |
{ name = "ragas" },
|
|
@@ -3850,6 +3851,7 @@ requires-dist = [
|
|
| 3850 |
{ name = "pandas", specifier = ">=2.0.0" },
|
| 3851 |
{ name = "pathvalidate", specifier = ">=3.2.3" },
|
| 3852 |
{ name = "pyarrow", specifier = ">=19.0.0" },
|
|
|
|
| 3853 |
{ name = "pylint-venv", marker = "extra == 'dev'", specifier = ">=3.0.4" },
|
| 3854 |
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0.0" },
|
| 3855 |
{ name = "python-dotenv", specifier = ">=0.9.9" },
|
|
|
|
| 3777 |
{ name = "pandas" },
|
| 3778 |
{ name = "pathvalidate" },
|
| 3779 |
{ name = "pyarrow" },
|
| 3780 |
+
{ name = "pydantic-settings" },
|
| 3781 |
{ name = "python-dotenv" },
|
| 3782 |
{ name = "qdrant-client" },
|
| 3783 |
{ name = "ragas" },
|
|
|
|
| 3851 |
{ name = "pandas", specifier = ">=2.0.0" },
|
| 3852 |
{ name = "pathvalidate", specifier = ">=3.2.3" },
|
| 3853 |
{ name = "pyarrow", specifier = ">=19.0.0" },
|
| 3854 |
+
{ name = "pydantic-settings", specifier = ">=2.9.1" },
|
| 3855 |
{ name = "pylint-venv", marker = "extra == 'dev'", specifier = ">=3.0.4" },
|
| 3856 |
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0.0" },
|
| 3857 |
{ name = "python-dotenv", specifier = ">=0.9.9" },
|