mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-13 08:15:59 +00:00
Towards 0 config setup: tools for adding/removing projects
Possibility to autogenerate project-config
This commit is contained in:
1 parent
10432473cf
commit
a22b3c2486
7 files changed
+340
-43
No files matched your search
@@ -28,6 +28,7 @@ dependencies = [
|
||||
"pydantic>=2.10.6",
|
||||
"types-pyyaml>=6.0.12.20241230",
|
||||
"pyyaml>=6.0.2",
|
||||
"ruamel.yaml>=0.18.0",
|
||||
"jinja2>=3.1.6",
|
||||
"dotenv>=0.9.9",
|
||||
"pathspec>=0.12.1",
|
||||
|
||||
+12
-10
@@ -7,17 +7,20 @@
|
||||
enable_project_activation: True
|
||||
|
||||
# Add your list of projects here (which you can switch between using "activate_project").
|
||||
# Every list item must be either
|
||||
# - [Recommended] a path to the project's root directory (e.g. "/path/to/myproject"), with the
|
||||
# configuration file being located in "/path/to/myproject/.serena/project.yml".
|
||||
# In this case, the name of the project will be the name of the directory ("myproject").
|
||||
# - or a path to a .yml file, absolute or relative to the directory this
|
||||
# configuration file is in (e.g. "myproject.yml" or "/path/to/wherever/myproject.yml").
|
||||
# In this case, the name of the project will be the base filename of the .yml file
|
||||
# (e.g. "myproject" for "myproject.yml").
|
||||
# Every list item must be a path to the project's root directory (e.g. "/path/to/myproject"), with the
|
||||
# configuration file being located in "/path/to/myproject/.serena/project.yml", or a dict mapping
|
||||
# project_name to project_path (to avoid name collisions).
|
||||
# You can activate a project by its name using the "activate_project" command.
|
||||
# If the name is not explicitly set, the name of the project will be the name of the directory
|
||||
# ("myproject" in the example above).
|
||||
# Note: comments in the block below are not preserved when saving the config file,
|
||||
# so we recommend to not have any comments directly below the "projects" key.
|
||||
projects:
|
||||
- /path/to/myproject
|
||||
- project_name: /path/to/myproject
|
||||
|
||||
|
||||
gui_log_window: False
|
||||
# Whether to open a graphical window with Serena's logs (not supported on macOS).
|
||||
# This is useful both for troubleshooting and for monitoring the tool calls,
|
||||
# especially when using the agno playground, since the tool calls are not always shown,
|
||||
@@ -31,7 +34,6 @@ projects:
|
||||
#
|
||||
# Limitations: doesn't seem to work with the community version of Claude Desktop for Linux
|
||||
# Might also cause problems with some MCP clients - if you have any issues, try disabling this
|
||||
gui_log_window: False
|
||||
|
||||
# minimum log level for the GUI log window (10 = debug, 20 = info, 30 = warning, 40 = error)
|
||||
gui_log_level: 20
|
||||
# minimum log level for the GUI log window (10 = debug, 20 = info, 30 = warning, 40 = error)
|
||||
+215
-33
@@ -11,13 +11,14 @@ import traceback
|
||||
from abc import ABC
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable, Generator, Iterable, Sequence
|
||||
from copy import copy
|
||||
from dataclasses import dataclass, field
|
||||
from copy import copy, deepcopy
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from logging import Logger
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Self, TypeVar, Union
|
||||
|
||||
import yaml
|
||||
from ruamel.yaml.comments import CommentedMap
|
||||
from sensai.util import logging
|
||||
from sensai.util.logging import FallbackHandler
|
||||
from sensai.util.string import ToStringMixin, dict_string
|
||||
@@ -32,7 +33,8 @@ from serena.prompt_factory import PromptFactory, SerenaPromptFactory
|
||||
from serena.symbol import SymbolLocation, SymbolManager
|
||||
from serena.text_utils import search_files
|
||||
from serena.util.file_system import scan_directory
|
||||
from serena.util.inspection import iter_subclasses
|
||||
from serena.util.general import load_yaml, save_yaml
|
||||
from serena.util.inspection import determine_programming_language_composition, iter_subclasses
|
||||
from serena.util.shell import execute_shell_command
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -78,10 +80,38 @@ class ProjectConfig(ToStringMixin):
|
||||
excluded_tools: set[str] = field(default_factory=set)
|
||||
read_only: bool = False
|
||||
ignore_all_files_in_gitignore: bool = True
|
||||
project_info_prompt: str = ""
|
||||
|
||||
SERENA_MANAGED_DIR = ".serena"
|
||||
SERENA_DEFAULT_PROJECT_FILE = "project.yml"
|
||||
|
||||
@classmethod
|
||||
def autogenerate(cls, project_root: str | Path) -> Self:
|
||||
"""
|
||||
Autogenerate a project configuration for a given project root.
|
||||
"""
|
||||
project_root = os.path.abspath(project_root)
|
||||
project_name = Path(project_root).name
|
||||
language_composition = determine_programming_language_composition(project_root)
|
||||
# find the language with the highest percentage
|
||||
dominant_language = max(language_composition.keys(), key=lambda lang: language_composition[lang])
|
||||
return cls(
|
||||
project_name=project_name,
|
||||
language=Language(dominant_language),
|
||||
project_root=project_root,
|
||||
)
|
||||
|
||||
def save(self) -> None:
|
||||
"""
|
||||
Save the project configuration to the project root.
|
||||
"""
|
||||
config_path = os.path.join(self.project_root, self.rel_path_to_project_yml())
|
||||
os.makedirs(os.path.dirname(config_path), exist_ok=True)
|
||||
serialized_self = asdict(self)
|
||||
serialized_self["language"] = self.language.value
|
||||
serialized_self["excluded_tools"] = list(self.excluded_tools)
|
||||
save_yaml(config_path, serialized_self)
|
||||
|
||||
@classmethod
|
||||
def rel_path_to_project_yml(cls) -> str:
|
||||
return os.path.join(cls.SERENA_MANAGED_DIR, cls.SERENA_DEFAULT_PROJECT_FILE)
|
||||
@@ -103,6 +133,7 @@ class ProjectConfig(ToStringMixin):
|
||||
ignored_paths = config_dict.get("ignored_paths", [])
|
||||
excluded_tools = set(config_dict.get("excluded_tools", []))
|
||||
read_only = config_dict.get("read_only", False)
|
||||
project_info_prompt = config_dict.get("project_info_prompt", "")
|
||||
|
||||
if "ignore_all_files_in_gitignore" not in config_dict:
|
||||
raise SerenaConfigError(
|
||||
@@ -127,6 +158,7 @@ class ProjectConfig(ToStringMixin):
|
||||
excluded_tools=excluded_tools,
|
||||
read_only=read_only,
|
||||
ignore_all_files_in_gitignore=ignore_all_files_in_gitignore,
|
||||
project_info_prompt=project_info_prompt,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -181,49 +213,60 @@ class SerenaConfig:
|
||||
gui_log_window_enabled: bool = False
|
||||
gui_log_window_level: int = logging.INFO
|
||||
enable_project_activation: bool = True
|
||||
loaded_original_yaml: dict | CommentedMap = field(default_factory=dict)
|
||||
|
||||
CONFIG_FILE = "serena_config.yml"
|
||||
|
||||
@classmethod
|
||||
def from_config_file(cls) -> "SerenaConfig":
|
||||
def get_config_file_path(cls) -> str:
|
||||
return os.path.join(serena_root_path(), cls.CONFIG_FILE)
|
||||
|
||||
@classmethod
|
||||
def from_config_file(cls, preserve_comments: bool = False) -> "SerenaConfig":
|
||||
"""
|
||||
Static constructor to create SerenaConfig from the configuration file
|
||||
"""
|
||||
config_file = os.path.join(serena_root_path(), cls.CONFIG_FILE)
|
||||
config_file = cls.get_config_file_path()
|
||||
if not os.path.exists(config_file):
|
||||
raise FileNotFoundError(f"Serena configuration file not found: {config_file}")
|
||||
|
||||
with open(config_file, encoding="utf-8") as f:
|
||||
try:
|
||||
log.info(f"Loading Serena configuration from {config_file}")
|
||||
config_yaml = yaml.safe_load(f)
|
||||
except Exception as e:
|
||||
raise ValueError(f"Error loading Serena configuration from {config_file}: {e}") from e
|
||||
log.info(f"Loading Serena configuration from {config_file}")
|
||||
try:
|
||||
loaded_original_yaml = load_yaml(config_file, preserve_comments)
|
||||
except Exception as e:
|
||||
raise ValueError(f"Error loading Serena configuration from {config_file}: {e}") from e
|
||||
|
||||
# Create instance
|
||||
instance = cls()
|
||||
|
||||
# read projects
|
||||
if "projects" not in config_yaml:
|
||||
if "projects" not in loaded_original_yaml:
|
||||
raise SerenaConfigError("`projects` key not found in Serena configuration. Please update your `serena_config.yml` file.")
|
||||
projects = {}
|
||||
for project_config_path in config_yaml["projects"]:
|
||||
project_config_path = Path(project_config_path)
|
||||
if not project_config_path.is_absolute():
|
||||
project_config_path = Path(serena_root_path()) / project_config_path
|
||||
if project_config_path.is_dir(): # assume project file in default location
|
||||
project_config_path = project_config_path / ProjectConfig.SERENA_MANAGED_DIR / ProjectConfig.SERENA_DEFAULT_PROJECT_FILE
|
||||
if not project_config_path.is_file():
|
||||
raise FileNotFoundError(f"Project file not found: {project_config_path}")
|
||||
log.info(f"Loading project configuration from {project_config_path}")
|
||||
project_config = ProjectConfig.from_path(project_config_path)
|
||||
projects[project_config.project_name] = project_config
|
||||
for project_entry in loaded_original_yaml["projects"]:
|
||||
if isinstance(project_entry, str):
|
||||
project_path = Path(project_entry).resolve()
|
||||
project_name = project_path.name
|
||||
elif isinstance(project_entry, dict):
|
||||
# should be a dict with a single entry mapping project name to project config path
|
||||
if len(project_entry) != 1:
|
||||
raise SerenaConfigError(
|
||||
f"Invalid project configuration: expected a single entry mapping project name to project config path but got: {project_entry}."
|
||||
)
|
||||
project_name, project_path = next(iter(project_entry.items()))
|
||||
else:
|
||||
raise SerenaConfigError(f"Invalid project configuration: expected a string or a dict, got {project_entry}.")
|
||||
project_path = Path(project_path).resolve()
|
||||
log.info(f"Loading project configuration from {project_path}")
|
||||
project_config = ProjectConfig.from_path(project_path)
|
||||
projects[project_name] = project_config
|
||||
|
||||
instance.projects = projects
|
||||
instance.project_names = list(projects.keys())
|
||||
instance.gui_log_window_enabled = config_yaml.get("gui_log_window", False)
|
||||
instance.gui_log_window_level = config_yaml.get("gui_log_level", logging.INFO)
|
||||
instance.enable_project_activation = config_yaml.get("enable_project_activation", True)
|
||||
instance.gui_log_window_enabled = loaded_original_yaml.get("gui_log_window", False)
|
||||
instance.gui_log_window_level = loaded_original_yaml.get("gui_log_level", logging.INFO)
|
||||
instance.enable_project_activation = loaded_original_yaml.get("enable_project_activation", True)
|
||||
instance.loaded_original_yaml = loaded_original_yaml
|
||||
|
||||
return instance
|
||||
|
||||
@@ -232,6 +275,80 @@ class SerenaConfig:
|
||||
raise ValueError(f"Project '{project_name}' not found in Serena configuration; valid project names: {self.project_names}")
|
||||
return self.projects[project_name]
|
||||
|
||||
def save(self, preserve_comments: bool = True) -> None:
|
||||
loaded_original_yaml = deepcopy(self.loaded_original_yaml)
|
||||
loaded_original_yaml["projects"] = [
|
||||
{project_name: str(project_config.project_root)} for project_name, project_config in self.projects.items()
|
||||
]
|
||||
save_yaml(self.get_config_file_path(), loaded_original_yaml, preserve_comments)
|
||||
|
||||
def add_project_from_path(
|
||||
self, path: Path | str, project_name: str | None = None, update_config_file: bool = True
|
||||
) -> tuple[str, ProjectConfig, bool]:
|
||||
"""
|
||||
Add a project to the Serena configuration from a given path.
|
||||
"""
|
||||
path = Path(path).resolve()
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"Error: Path does not exist: {path}")
|
||||
if not path.is_dir():
|
||||
raise FileNotFoundError(f"Error: Path is not a directory: {path}")
|
||||
|
||||
if project_name is None:
|
||||
project_name = path.name
|
||||
if project_name in self.projects.keys():
|
||||
project_config = self.projects[project_name]
|
||||
if os.path.abspath(project_config.project_root) == os.path.abspath(path):
|
||||
raise FileExistsError(f"Project '{project_name}' was already added and points to the correct path, you can activate it.")
|
||||
raise FileExistsError(
|
||||
f"Name collision: '{project_name=}' was already added, but points to a different path: {project_config.project_root}."
|
||||
+ "You can either use a different name or remove the existing project first before adding this one again."
|
||||
+ "If you are an LLM assistant, ask the user explicitly how they want to proceed before executing any further actions!"
|
||||
)
|
||||
|
||||
try:
|
||||
project_config = ProjectConfig.from_path(path)
|
||||
new_config_was_generated = False
|
||||
except FileNotFoundError:
|
||||
project_config = ProjectConfig.autogenerate(path)
|
||||
project_config.save()
|
||||
new_config_was_generated = True
|
||||
|
||||
self.add_project(project_config, project_name, update_config_file)
|
||||
return project_name, project_config, new_config_was_generated
|
||||
|
||||
def add_project(self, project_config: ProjectConfig, project_name: str | None = None, update_config_file: bool = True) -> None:
|
||||
if project_name is None:
|
||||
project_name = project_config.project_name
|
||||
existing_project_config = self.projects.get(project_name)
|
||||
if existing_project_config is not None:
|
||||
if existing_project_config.project_root == project_config.project_root:
|
||||
raise FileExistsError(f"Project '{project_name}' was already added and points to the correct path, you can activate it.")
|
||||
raise FileExistsError(
|
||||
f"Name collision: '{project_name=}' was already added, but points to a different path: {existing_project_config.project_root}."
|
||||
+ "You can either use a different name or remove the existing project first before adding this one again."
|
||||
)
|
||||
self.projects[project_name] = project_config
|
||||
self.project_names.append(project_name)
|
||||
|
||||
# Update the config file on disk and preserve comments
|
||||
if update_config_file:
|
||||
config_with_comments = SerenaConfig.from_config_file(preserve_comments=True)
|
||||
config_with_comments.projects[project_name] = project_config
|
||||
config_with_comments.save(preserve_comments=True)
|
||||
|
||||
def remove_project(self, project_name: str, update_config_file: bool = True) -> None:
|
||||
if project_name not in self.projects:
|
||||
raise ValueError(f"Project '{project_name}' not found in Serena configuration; valid project names: {self.project_names}")
|
||||
del self.projects[project_name]
|
||||
self.project_names.remove(project_name)
|
||||
|
||||
# Update the config file on disk and preserve comments
|
||||
if update_config_file:
|
||||
config_with_comments = SerenaConfig.from_config_file(preserve_comments=True)
|
||||
config_with_comments.projects.pop(project_name, None)
|
||||
config_with_comments.save(preserve_comments=True)
|
||||
|
||||
|
||||
class LinesRead:
|
||||
def __init__(self) -> None:
|
||||
@@ -324,7 +441,7 @@ class SerenaAgent:
|
||||
self._context = context
|
||||
self._modes = modes
|
||||
log.info(f"Loaded tools ({len(self._all_tools)}): {', '.join([tool.get_name() for tool in self._all_tools.values()])}")
|
||||
|
||||
|
||||
self._active_tools: dict[type[Tool], Tool] = {}
|
||||
self._update_active_tools()
|
||||
|
||||
@@ -416,7 +533,7 @@ class SerenaAgent:
|
||||
log.info(f"Activating {project_config}")
|
||||
self.project_config = project_config
|
||||
self._update_active_tools()
|
||||
|
||||
|
||||
# start the language server
|
||||
self.reset_language_server()
|
||||
assert self.language_server is not None
|
||||
@@ -1537,12 +1654,77 @@ class ActivateProjectTool(Tool, ToolMarkerDoesNotRequireActiveProject):
|
||||
|
||||
:param project_name: the name of the project to activate
|
||||
"""
|
||||
try:
|
||||
project_config = self.agent.serena_config.get_project_configuration(project_name)
|
||||
except ValueError as e:
|
||||
return str(e)
|
||||
project_config = self.agent.serena_config.get_project_configuration(project_name)
|
||||
self.agent.activate_project(project_config)
|
||||
return SUCCESS_RESULT
|
||||
return "Activated project "
|
||||
|
||||
|
||||
class ListProjectsTool(Tool, ToolMarkerDoesNotRequireActiveProject):
|
||||
"""
|
||||
Lists all configured projects in the Serena configuration.
|
||||
"""
|
||||
|
||||
def apply(self) -> str:
|
||||
"""
|
||||
List all configured projects with their details including names, paths, and status.
|
||||
"""
|
||||
if not self.agent.serena_config.projects:
|
||||
return "No projects configured."
|
||||
|
||||
result = []
|
||||
result.append("Configured projects:")
|
||||
result.append("")
|
||||
|
||||
for project_name, project_config in self.agent.serena_config.projects.items():
|
||||
status = "✓ Active" if (self.agent.project_config and self.agent.project_config.project_name == project_name) else " Available"
|
||||
result.append(f"{status} {project_name}")
|
||||
result.append(f" Path: {project_config.project_root}")
|
||||
result.append(f" Language: {project_config.language.value}")
|
||||
result.append(f" Read-only: {project_config.read_only}")
|
||||
result.append("")
|
||||
|
||||
return "\n".join(result)
|
||||
|
||||
|
||||
class AddProjectTool(Tool, ToolMarkerDoesNotRequireActiveProject):
|
||||
"""
|
||||
Adds a new project to the Serena configuration.
|
||||
If the project is not already configured, this tool will autogenerate a project configuration.
|
||||
"""
|
||||
|
||||
def apply(self, project_path: str, project_name: str | None = None) -> str:
|
||||
"""
|
||||
Adds a new project to the Serena configuration.
|
||||
|
||||
:param project_path: Path to the project directory
|
||||
:param project_name: Name of the project to add. If not provided, the directory name will be used.
|
||||
"""
|
||||
project_name, project_config, was_autogenerated = self.agent.serena_config.add_project_from_path(
|
||||
project_path, project_name, update_config_file=True
|
||||
)
|
||||
result_msg = f"Successfully added project '{project_name}' to configuration."
|
||||
if was_autogenerated:
|
||||
result_msg += (
|
||||
f"\nNote: A new project configuration was autogenerated because the given path did not contain a {ProjectConfig.SERENA_DEFAULT_PROJECT_FILE} file."
|
||||
+ f"You can now edit the project configuration in the file {project_config.rel_path_to_project_yml()}."
|
||||
+ f"Identified project programming language: {project_config.language.value}"
|
||||
)
|
||||
return result_msg
|
||||
|
||||
|
||||
class RemoveProjectTool(Tool, ToolMarkerDoesNotRequireActiveProject):
|
||||
"""
|
||||
Removes a project from the Serena configuration.
|
||||
"""
|
||||
|
||||
def apply(self, project_name: str) -> str:
|
||||
"""
|
||||
Removes a project from the Serena configuration.
|
||||
|
||||
:param project_name: Name of the project to remove
|
||||
"""
|
||||
self.agent.serena_config.remove_project(project_name, update_config_file=True)
|
||||
return f"Successfully removed project '{project_name}' from configuration."
|
||||
|
||||
|
||||
class SwitchModesTool(Tool):
|
||||
|
||||
@@ -144,8 +144,10 @@ def create_mcp_server_and_agent(
|
||||
@asynccontextmanager
|
||||
async def server_lifespan(mcp_server: FastMCP) -> AsyncIterator[None]:
|
||||
"""Manage server startup and shutdown lifecycle."""
|
||||
nonlocal agent
|
||||
mark_used(mcp_server)
|
||||
yield
|
||||
del agent
|
||||
|
||||
mcp_settings = Settings(lifespan=server_lifespan, host=host, port=port)
|
||||
mcp = FastMCP(**mcp_settings.model_dump())
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
from ruamel.yaml import YAML
|
||||
from ruamel.yaml.comments import CommentedMap
|
||||
|
||||
|
||||
def create_YAML(preserve_comments: bool = False) -> YAML:
|
||||
"""
|
||||
Creates a YAML that can load/save with comments if preserve_comments is True.
|
||||
"""
|
||||
typ = None if preserve_comments else "safe"
|
||||
result = YAML(typ=typ)
|
||||
result.preserve_quotes = preserve_comments
|
||||
return result
|
||||
|
||||
|
||||
def load_yaml(path: str, preserve_comments: bool = False) -> dict:
|
||||
with open(path, encoding="utf-8") as f:
|
||||
yaml = create_YAML(preserve_comments)
|
||||
return yaml.load(f)
|
||||
|
||||
|
||||
def save_yaml(path: str, data: dict | CommentedMap, preserve_comments: bool = False) -> None:
|
||||
yaml = create_YAML(preserve_comments)
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
yaml.dump(data, f)
|
||||
@@ -1,6 +1,10 @@
|
||||
import os
|
||||
from collections.abc import Generator
|
||||
from typing import TypeVar
|
||||
|
||||
from multilspy.multilspy_config import Language
|
||||
from serena.util.file_system import scan_directory
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
@@ -10,3 +14,54 @@ def iter_subclasses(cls: type[T], recursive: bool = True) -> Generator[type[T],
|
||||
yield subclass
|
||||
if recursive:
|
||||
yield from iter_subclasses(subclass, recursive)
|
||||
|
||||
|
||||
def determine_programming_language_composition(repo_path: str) -> dict[str, float]:
|
||||
"""
|
||||
Determine the programming language composition of a repository.
|
||||
|
||||
:param repo_path: Path to the repository to analyze
|
||||
:return: Dictionary mapping language names to percentages of files matching each language
|
||||
"""
|
||||
if not os.path.exists(repo_path):
|
||||
raise FileNotFoundError(f"Repository path does not exist: {repo_path}")
|
||||
|
||||
if not os.path.isdir(repo_path):
|
||||
raise ValueError(f"Repository path is not a directory: {repo_path}")
|
||||
|
||||
# Scan all files in the repository recursively
|
||||
_, all_files = scan_directory(
|
||||
path=repo_path,
|
||||
recursive=True,
|
||||
relative_to=repo_path,
|
||||
is_ignored_dir=lambda dirname: os.path.basename(dirname).startswith("."), # Ignore hidden directories
|
||||
is_ignored_file=lambda filename: os.path.basename(filename).startswith("."), # Ignore hidden files
|
||||
)
|
||||
|
||||
if not all_files:
|
||||
return {}
|
||||
|
||||
# Count files for each language
|
||||
language_counts: dict[str, int] = {}
|
||||
total_files = len(all_files)
|
||||
|
||||
for language in Language:
|
||||
matcher = language.get_source_fn_matcher()
|
||||
count = 0
|
||||
|
||||
for file_path in all_files:
|
||||
# Use just the filename for matching, not the full path
|
||||
filename = os.path.basename(file_path)
|
||||
if matcher.is_relevant_filename(filename):
|
||||
count += 1
|
||||
|
||||
if count > 0:
|
||||
language_counts[str(language)] = count
|
||||
|
||||
# Convert counts to percentages
|
||||
language_percentages: dict[str, float] = {}
|
||||
for language_name, count in language_counts.items():
|
||||
percentage = (count / total_files) * 100
|
||||
language_percentages[language_name] = round(percentage, 2)
|
||||
|
||||
return language_percentages
|
||||
@@ -890,6 +890,35 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/49/97/fa78e3d2f65c02c8e1268b9aba606569fe97f6c8f7c2d74394553347c145/rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7", size = 34315, upload-time = "2022-07-20T10:28:34.978Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruamel-yaml"
|
||||
version = "0.18.10"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "ruamel-yaml-clib", marker = "platform_python_implementation == 'CPython'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ea/46/f44d8be06b85bc7c4d8c95d658be2b68f27711f279bf9dd0612a5e4794f5/ruamel.yaml-0.18.10.tar.gz", hash = "sha256:20c86ab29ac2153f80a428e1254a8adf686d3383df04490514ca3b79a362db58", size = 143447, upload-time = "2025-01-06T14:08:51.334Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/36/dfc1ebc0081e6d39924a2cc53654497f967a084a436bb64402dfce4254d9/ruamel.yaml-0.18.10-py3-none-any.whl", hash = "sha256:30f22513ab2301b3d2b577adc121c6471f28734d3d9728581245f1e76468b4f1", size = 117729, upload-time = "2025-01-06T14:08:47.471Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruamel-yaml-clib"
|
||||
version = "0.2.12"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/20/84/80203abff8ea4993a87d823a5f632e4d92831ef75d404c9fc78d0176d2b5/ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f", size = 225315, upload-time = "2024-10-20T10:10:56.22Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/8f/683c6ad562f558cbc4f7c029abcd9599148c51c54b5ef0f24f2638da9fbb/ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6", size = 132224, upload-time = "2024-10-20T10:12:45.162Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/d2/b79b7d695e2f21da020bd44c782490578f300dd44f0a4c57a92575758a76/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:d84318609196d6bd6da0edfa25cedfbabd8dbde5140a0a23af29ad4b8f91fb1e", size = 641480, upload-time = "2024-10-20T10:12:46.758Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/6e/264c50ce2a31473a9fdbf4fa66ca9b2b17c7455b31ef585462343818bd6c/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb43a269eb827806502c7c8efb7ae7e9e9d0573257a46e8e952f4d4caba4f31e", size = 739068, upload-time = "2024-10-20T10:12:48.605Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/29/88c2567bc893c84d88b4c48027367c3562ae69121d568e8a3f3a8d363f4d/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52", size = 703012, upload-time = "2024-10-20T10:12:51.124Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/46/879763c619b5470820f0cd6ca97d134771e502776bc2b844d2adb6e37753/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642", size = 704352, upload-time = "2024-10-21T11:26:41.438Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/80/ece7e6034256a4186bbe50dee28cd032d816974941a6abf6a9d65e4228a7/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2", size = 737344, upload-time = "2024-10-21T11:26:43.62Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/ca/e4106ac7e80efbabdf4bf91d3d32fc424e41418458251712f5672eada9ce/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3", size = 714498, upload-time = "2024-12-11T19:58:15.592Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/58/b1f60a1d591b771298ffa0428237afb092c7f29ae23bad93420b1eb10703/ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4", size = 100205, upload-time = "2024-10-20T10:12:52.865Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/4f/b52f634c9548a9291a70dfce26ca7ebce388235c93588a1068028ea23fcc/ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb", size = 118185, upload-time = "2024-10-20T10:12:54.652Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.11.5"
|
||||
@@ -946,6 +975,7 @@ dependencies = [
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "requests" },
|
||||
{ name = "ruamel-yaml" },
|
||||
{ name = "sensai-utils" },
|
||||
{ name = "types-pyyaml" },
|
||||
]
|
||||
@@ -998,6 +1028,7 @@ requires-dist = [
|
||||
{ name = "python-dotenv", specifier = ">=1.0.0,<2" },
|
||||
{ name = "pyyaml", specifier = ">=6.0.2" },
|
||||
{ name = "requests", specifier = ">=2.32.3,<3" },
|
||||
{ name = "ruamel-yaml", specifier = ">=0.18.0" },
|
||||
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.0.285" },
|
||||
{ name = "sensai-utils", specifier = ">=1.4.0" },
|
||||
{ name = "sqlalchemy", marker = "extra == 'agno'", specifier = ">=2.0.40" },
|
||||
|
||||
Reference in new issue
Block a user