diff --git a/docs/mint.json b/docs/mint.json
index 6d2f38396..f41a8d355 100644
--- a/docs/mint.json
+++ b/docs/mint.json
@@ -157,7 +157,6 @@
"api-reference/core/ExpressionStatement",
"api-reference/core/ExternalModule",
"api-reference/core/File",
- "api-reference/core/FlagKwargs",
"api-reference/core/ForLoopStatement",
"api-reference/core/Function",
"api-reference/core/FunctionCall",
diff --git a/docs/snippets/Attribute.mdx b/docs/snippets/Attribute.mdx
new file mode 100644
index 000000000..13f3118e2
--- /dev/null
+++ b/docs/snippets/Attribute.mdx
@@ -0,0 +1,8 @@
+export const Attribute = ({ type, description }) => (
+
+
+ {type}
+
+
{description}
+
+)
\ No newline at end of file
diff --git a/docs/snippets/GithubLinkNote.mdx b/docs/snippets/GithubLinkNote.mdx
new file mode 100644
index 000000000..fe8b1f1d7
--- /dev/null
+++ b/docs/snippets/GithubLinkNote.mdx
@@ -0,0 +1,5 @@
+export const GithubLinkNote = ({link}) => (
+
+
+
+)
diff --git a/docs/snippets/HorizontalDivider.mdx b/docs/snippets/HorizontalDivider.mdx
new file mode 100644
index 000000000..a8b829fc2
--- /dev/null
+++ b/docs/snippets/HorizontalDivider.mdx
@@ -0,0 +1,6 @@
+export const HorizontalDivider = ({light=false}) => (
+
+)
\ No newline at end of file
diff --git a/docs/snippets/Parameter.mdx b/docs/snippets/Parameter.mdx
new file mode 100644
index 000000000..888701304
--- /dev/null
+++ b/docs/snippets/Parameter.mdx
@@ -0,0 +1,28 @@
+export const Parameter = ({name, type, description, defaultValue}) => (
+
+
+
+
+ {name}
+
+
+ {type}
+
+
+
+
+ {defaultValue ? (
+
+ default:
+ {defaultValue}
+
+ ) : (
+
+ required
+
+ )}
+
+
+
{description}
+
+)
\ No newline at end of file
diff --git a/docs/snippets/ParameterWrapper.mdx b/docs/snippets/ParameterWrapper.mdx
new file mode 100644
index 000000000..9866604e5
--- /dev/null
+++ b/docs/snippets/ParameterWrapper.mdx
@@ -0,0 +1,8 @@
+export const ParameterWrapper = ({ children }) => (
+
+
Parameters
+
+ {children}
+
+
+)
\ No newline at end of file
diff --git a/docs/snippets/Return.mdx b/docs/snippets/Return.mdx
new file mode 100644
index 000000000..9404f569e
--- /dev/null
+++ b/docs/snippets/Return.mdx
@@ -0,0 +1,15 @@
+export const Return = ({return_type, description}) => (
+
+)
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index 12560ba39..8afe8a439 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -65,6 +65,7 @@ dependencies = [
"psutil>=5.8.0",
"fastapi[standard]<1.0.0,>=0.115.2",
"starlette<1.0.0,>=0.16.0",
+ "tqdm>=4.67.1",
]
license = {file = "LICENSE"}
classifiers = [
diff --git a/src/codegen/git/utils/clone_url.py b/src/codegen/git/utils/clone_url.py
index 630075041..63bb8711e 100644
--- a/src/codegen/git/utils/clone_url.py
+++ b/src/codegen/git/utils/clone_url.py
@@ -7,7 +7,7 @@
def url_to_github(url: str, branch: str) -> str:
clone_url = url.removesuffix(".git").replace("git@github.com:", "github.com/")
- return f"https://{clone_url}/blob/{branch}"
+ return f"{clone_url}/blob/{branch}"
def get_clone_url_for_repo_config(repo_config: RepoConfig, github_type: GithubType = GithubType.GithubEnterprise) -> str:
diff --git a/src/codegen/gscli/generate/commands.py b/src/codegen/gscli/generate/commands.py
index a6a504da6..e1cdd5a83 100644
--- a/src/codegen/gscli/generate/commands.py
+++ b/src/codegen/gscli/generate/commands.py
@@ -10,13 +10,10 @@
from codegen.gscli.generate.runner_imports import _generate_runner_imports
from codegen.gscli.generate.utils import LanguageType, generate_builtins_file
-from codegen.sdk.code_generation.doc_utils.canonicals import get_canonical_codemod_class_mdx, get_canonical_codemod_classes
+from codegen.sdk.code_generation.codegen_sdk_codebase import get_codegen_sdk_codebase
+from codegen.sdk.code_generation.doc_utils.generate_docs_json import generate_docs_json
from codegen.sdk.code_generation.doc_utils.skills import format_all_skills
-from codegen.sdk.code_generation.doc_utils.utils import get_all_classes_to_document
-from codegen.sdk.code_generation.mdx_docs_generation import render_mdx_for_codebase_page, render_mdx_page_for_class
-from codegen.sdk.code_generation.prompts.api_docs import get_codegen_sdk_codebase
-from codegen.sdk.core.codebase import PyCodebaseType
-from codegen.sdk.enums import ProgrammingLanguage
+from codegen.sdk.code_generation.mdx_docs_generation import render_mdx_page_for_class
from codegen.sdk.python import PyClass
from codegen.sdk.skills.core.utils import get_all_skills, get_guide_skills_dict
@@ -110,18 +107,17 @@ def generate_docs(docs_dir: str) -> None:
This will generate docs using the codebase locally, including any unstaged changes
"""
- codebase = get_codegen_sdk_codebase()
- generate_codegen_sdk_docs(docs_dir, codebase)
+ generate_codegen_sdk_docs(docs_dir)
# generate_canonical_codemod_docs(docs_dir, codebase)
- generate_skills_docs(docs_dir)
- generate_guides(docs_dir)
+ # generate_skills_docs(docs_dir)
+ # generate_guides(docs_dir)
def generate_guides(docs_dir: str):
"""Updates code snippets in the guides with the latest skill implementations"""
guide_skills = get_guide_skills_dict()
for guide_relative_path in guide_skills:
- guide_file_path = os.path.join(docs_dir, "codebase-sdk", str(guide_relative_path) + ".mdx")
+ guide_file_path = os.path.join(docs_dir, "api-reference", str(guide_relative_path) + ".mdx")
with open(guide_file_path) as f:
file_content = f.read()
@@ -143,23 +139,20 @@ def get_snippet_pattern(target_name: str) -> str:
return pattern
-def generate_codegen_sdk_docs(docs_dir: str, codebase: PyCodebaseType) -> None:
+def generate_codegen_sdk_docs(docs_dir: str) -> None:
"""Generate the docs for the codegen_sdk API and update the mint.json"""
print(colored("Generating codegen_sdk docs", "green"))
# Generate docs page for codebase api and write to the file system
- mdx_page = render_mdx_for_codebase_page(codebase)
- os.makedirs(os.path.join(docs_dir, "codebase-sdk"), exist_ok=True)
- file_path = os.path.join(docs_dir, "codebase-sdk", "codebase.mdx")
- with open(file_path, "w") as f:
- f.write(mdx_page)
+ codebase = get_codegen_sdk_codebase()
+ gs_docs = generate_docs_json(codebase, "HEAD")
# Prepare the directories for the new docs
# Delete existing documentation directories if they exist
# So we remove generated docs for any classes which no longer exist
- python_docs_dir = os.path.join(docs_dir, "codebase-sdk", "python")
- typescript_docs_dir = os.path.join(docs_dir, "codebase-sdk", "typescript")
- core_dir = os.path.join(docs_dir, "codebase-sdk", "core")
+ python_docs_dir = os.path.join(docs_dir, "api-reference", "python")
+ typescript_docs_dir = os.path.join(docs_dir, "api-reference", "typescript")
+ core_dir = os.path.join(docs_dir, "api-reference", "core")
for dir_path in [python_docs_dir, typescript_docs_dir, core_dir]:
if os.path.exists(dir_path):
@@ -170,7 +163,6 @@ def generate_codegen_sdk_docs(docs_dir: str, codebase: PyCodebaseType) -> None:
os.makedirs(core_dir, exist_ok=True)
# Generate the docs pages for core, python, and typescript classes
- classes = get_all_classes_to_document(codebase)
# Write the generated docs to the file system, splitting between core, python, and typescript
# keep track of where we put each one so we can update the mint.json
@@ -178,19 +170,20 @@ def generate_codegen_sdk_docs(docs_dir: str, codebase: PyCodebaseType) -> None:
typescript_set = set()
core_set = set()
# TODO replace this with new `get_mdx_for_class` function
- for class_name, class_obj in classes.items():
+ for class_doc in gs_docs.classes:
+ class_name = class_doc.title
lower_class_name = class_name.lower()
if lower_class_name.startswith("py"):
file_path = os.path.join(python_docs_dir, f"{class_name}.mdx")
- python_set.add(f"codebase-sdk/python/{class_name}")
+ python_set.add(f"api-reference/python/{class_name}")
elif lower_class_name.startswith(("ts", "jsx")):
file_path = os.path.join(typescript_docs_dir, f"{class_name}.mdx")
- typescript_set.add(f"codebase-sdk/typescript/{class_name}")
+ typescript_set.add(f"api-reference/typescript/{class_name}")
else:
file_path = os.path.join(core_dir, f"{class_name}.mdx")
- core_set.add(f"codebase-sdk/core/{class_name}")
+ core_set.add(f"api-reference/core/{class_name}")
- mdx_page = render_mdx_page_for_class(cls=class_obj, codebase=codebase)
+ mdx_page = render_mdx_page_for_class(cls_doc=class_doc)
with open(file_path, "w") as f:
f.write(mdx_page)
print(colored("Finished writing new .mdx files", "green"))
@@ -201,7 +194,7 @@ def generate_codegen_sdk_docs(docs_dir: str, codebase: PyCodebaseType) -> None:
mint_data = json.load(mint_file)
# Find the "Codebase SDK" group where we want to add the pages
- codebase_sdk_group = next(group for group in mint_data["navigation"] if group["group"] == "GraphSitter Reference")
+ codebase_sdk_group = next(group for group in mint_data["navigation"] if group["group"] == "API Reference")
# Update the pages for each language group
for group in codebase_sdk_group["pages"]:
@@ -243,26 +236,3 @@ def filter_class(cls: PyClass):
if decorator.name == "skill_impl" and "external=True" in decorator.source:
return True
return False
-
-
-def generate_canonical_codemod_docs(docs_dir: str, codebase: PyCodebaseType) -> None:
- """Generates docs for all canonical codemods"""
- print(colored("Generating canonical codemod docs", "green"))
-
- print(colored("> Grabbing canonicals", "green"))
- classes = get_canonical_codemod_classes(codebase, language=ProgrammingLanguage.PYTHON)
-
- # =====[ Write the canonical docs to the file system ]=====
- examples_dir = os.path.join(docs_dir, "codebase-sdk", "examples")
- print(colored(f"> Generating docs in {examples_dir}", "green"))
- docstrings = {k: get_canonical_codemod_class_mdx(v) for k, v in classes.items()}
- for k, v in docstrings.items():
- file_path = os.path.join(examples_dir, f"{k}.mdx")
- with open(file_path, "w") as f:
- f.write(v)
-
- print(colored("> Writing to disk", "green"))
-
-
-if __name__ == "__main__":
- generate_skills_docs("docs")
diff --git a/src/codegen/sdk/code_generation/doc_utils/canonicals.py b/src/codegen/sdk/code_generation/doc_utils/canonicals.py
deleted file mode 100644
index edcd0081f..000000000
--- a/src/codegen/sdk/code_generation/doc_utils/canonicals.py
+++ /dev/null
@@ -1,86 +0,0 @@
-import textwrap
-
-from codegen.sdk.code_generation.doc_utils.utils import (
- format_python_codeblock,
-)
-from codegen.sdk.code_generation.enums import DocumentationDecorators
-from codegen.sdk.core.codebase import Codebase
-from codegen.sdk.enums import ProgrammingLanguage
-from codegen.sdk.python.class_definition import PyClass
-
-
-def get_canonical_codemod_classes(codebase: Codebase, language: ProgrammingLanguage) -> dict[str, PyClass]:
- classes = {}
- target_decorator = DocumentationDecorators.CODEMOD.value
- for cls in codebase.classes:
- if target_decorator in [decorator.name for decorator in cls.decorators]:
- if cls.get_attribute("language"):
- if cls.get_attribute("language").assignment.value.source.split(".")[1] == language.value:
- classes[cls.name] = cls
- return classes
-
-
-def get_canonical_codemod_class_docstring(symbol: PyClass) -> str:
- """Returns a markdown-formatted string for a single codemod class."""
- # =====[ Docstring ]=====
- title = symbol.name
- docstring = symbol.docstring
- if docstring:
- docstring = docstring.text
- else:
- docstring = "No docstring provided."
-
- # =====[ Source ]=====
- exec_method = symbol.get_method("execute")
- source = "\n".join(exec_method.source.split("\n")[1:])
- source = textwrap.dedent(source)
-
- # =====[ Language ]=====
- language = symbol.get_attribute("language")
- if not language:
- raise AttributeError(f"Language attribute not found for {symbol.name}")
- else:
- language_name = symbol.get_attribute("language").assignment.value.source.split(".")[1]
- lang_str = f"(language: `{language_name}`)"
-
- return f"""
-### {title} {lang_str}
-
-{docstring}
-
-{format_python_codeblock(source)}
-"""
-
-
-def get_canonical_codemod_class_mdx(symbol: PyClass) -> str:
- """Returns a markdown-formatted string for a single codemod class."""
- # =====[ Docstring ]=====
- title = symbol.name
- docstring = symbol.docstring
- if docstring:
- docstring = docstring.text
- else:
- docstring = "No docstring provided."
-
- # =====[ Source ]=====
- exec_method = symbol.get_method("execute")
- source = "\n".join(exec_method.source.split("\n")[1:])
- source = textwrap.dedent(source)
-
- # =====[ Language ]=====
- language = symbol.get_attribute("language")
- if not language:
- raise AttributeError(f"Language attribute not found for {symbol.name}")
- else:
- language_name = symbol.get_attribute("language").assignment.value.source.split(".")[1]
- lang_str = f"(language: `{language_name}`)"
-
- return f"""---
-title: {title}
-sidebarTitle: {title}
----
-
-{docstring}
-
-{format_python_codeblock(source)}
-"""
diff --git a/src/codegen/sdk/code_generation/doc_utils/generate_docs_json.py b/src/codegen/sdk/code_generation/doc_utils/generate_docs_json.py
new file mode 100644
index 000000000..d7d091248
--- /dev/null
+++ b/src/codegen/sdk/code_generation/doc_utils/generate_docs_json.py
@@ -0,0 +1,174 @@
+from typing import Any
+
+from loguru import logger
+from tqdm import tqdm
+
+from codegen.sdk.code_generation.doc_utils.parse_docstring import parse_docstring
+from codegen.sdk.code_generation.doc_utils.schemas import ClassDoc, GSDocs, MethodDoc
+from codegen.sdk.code_generation.doc_utils.utils import create_path, get_langauge, get_type, get_type_str, has_documentation, is_settter, replace_multiple_types
+from codegen.sdk.core.class_definition import Class
+from codegen.sdk.core.codebase import Codebase
+
+ATTRIBUTES_TO_IGNORE = ["G", "node_id", "angular"]
+
+
+def generate_docs_json(codebase: Codebase, head_commit: str) -> dict[str, dict[str, Any]]:
+ """Update documentation table for classes, methods and attributes in the codebase.
+
+ Args:
+ codebase (Codebase): the codebase to update the docs for
+ head_commit (str): the head commit hash
+ Returns:
+ dict[str, dict[str, Any]]: the documentation for the codebase
+ """
+ codegen_sdk_docs = GSDocs(classes=[])
+ types_cache = {}
+ attr_cache = {}
+
+ def process_class_doc(cls):
+ """Update or create documentation for a class."""
+ description = cls.docstring.source.strip('"""') if cls.docstring else None
+ parent_classes = [f"<{create_path(parent)}>" for parent in cls.superclasses if isinstance(parent, Class) and has_documentation(parent)]
+
+ cls_doc = ClassDoc(
+ title=cls.name,
+ description=description,
+ content=" ",
+ path=create_path(cls),
+ inherits_from=parent_classes,
+ language=get_langauge(cls),
+ version=str(head_commit),
+ github_url=cls.github_url,
+ )
+
+ return cls_doc
+
+ def process_method(method, cls, cls_doc, seen_methods):
+ """Process a single method and update its documentation."""
+ if any(dec.name == "noapidoc" for dec in method.decorators):
+ return
+
+ if method.name in seen_methods and not is_settter(method):
+ return
+
+ if not method.docstring:
+ logger.info(f"Method {cls.name}.{method.name} does not have a docstring")
+ return
+
+ method_path = create_path(method, cls)
+ parameters = []
+
+ parsed = parse_docstring(method.docstring.source)
+ if parsed is None:
+ raise ValueError(f"Method {cls.name}.{method.name} does not have a docstring")
+
+ # Update parameter types
+ for param, parsed_param in zip(method.parameters[1:], parsed["arguments"]):
+ if param.name == parsed_param.name:
+ parsed_param.type = replace_multiple_types(
+ codebase=codebase, input_str=parsed_param.type, resolved_types=param.type.resolved_types, parent_class=cls, parent_symbol=method, types_cache=types_cache
+ )
+ if param.default:
+ parsed_param.default = param.default
+
+ parameters.append(parsed_param)
+ # Update return type
+ from codegen.sdk.python.placeholder.placeholder_return_type import PyReturnTypePlaceholder
+
+ if not isinstance(method.return_type, PyReturnTypePlaceholder):
+ return_type = replace_multiple_types(
+ codebase=codebase, input_str=method.return_type.source, resolved_types=method.return_type.resolved_types, parent_class=cls, parent_symbol=method, types_cache=types_cache
+ )
+ else:
+ return_type = None
+ parsed["return_types"] = [return_type]
+
+ meta_data = {"parent": create_path(method.parent_class), "path": method.file.filepath}
+ return MethodDoc(
+ name=method.name,
+ description=parsed["description"],
+ parameters=parsed["arguments"],
+ return_type=parsed["return_types"],
+ return_description=parsed["return_description"],
+ method_type=get_type(method),
+ code=method.function_signature,
+ path=method_path,
+ raises=parsed["raises"],
+ metainfo=meta_data,
+ version=str(head_commit),
+ github_url=method.github_url,
+ )
+
+ def process_attribute(attr, cls, cls_doc, seen_methods):
+ """Process a single attribute and update its documentation."""
+ if attr.name in seen_methods or attr.name in ATTRIBUTES_TO_IGNORE:
+ return
+
+ attr_path = create_path(attr, cls)
+ original_attr_path = create_path(attr)
+
+ if original_attr_path not in attr_cache:
+ description = attr.docstring(cls)
+ attr_return_type = []
+ if r_type := get_type_str(attr):
+ r_type_source = replace_multiple_types(codebase=codebase, input_str=r_type.source, resolved_types=r_type.resolved_types, parent_class=cls, parent_symbol=attr, types_cache=types_cache)
+ attr_return_type.append(r_type_source)
+
+ attr_cache[original_attr_path] = {"description": description, "attr_return_type": attr_return_type}
+
+ attr_info = attr_cache[original_attr_path]
+ meta_data = {"parent": create_path(attr.parent_class), "path": attr.file.filepath}
+
+ return MethodDoc(
+ name=attr.name,
+ description=attr_info["description"],
+ parameters=[],
+ return_type=attr_info["attr_return_type"],
+ return_description=None,
+ method_type="attribute",
+ code=attr.attribute_docstring,
+ path=attr_path,
+ raises=[],
+ metainfo=meta_data,
+ version=str(head_commit),
+ github_url=attr.github_url,
+ )
+
+ # Process all documented classes
+ documented_classes = [cls for cls in codebase.classes if has_documentation(cls)]
+
+ for cls in tqdm(documented_classes):
+ try:
+ cls_doc = process_class_doc(cls)
+ codegen_sdk_docs.classes.append(cls_doc)
+ seen_methods = set()
+
+ # Process methods
+ for method in cls.methods(max_depth=None, private=False, magic=False):
+ try:
+ method_doc = process_method(method, cls, cls_doc, seen_methods)
+ if not method_doc:
+ continue
+ seen_methods.add(method_doc.name)
+ cls_doc.methods.append(method_doc)
+ except Exception as e:
+ logger.info(f"Failed to parse method: {method} - {e}")
+
+ # Process attributes
+ for attr in cls.attributes(max_depth=None, private=False):
+ if attr.name in ATTRIBUTES_TO_IGNORE:
+ continue
+ try:
+ attr_doc = process_attribute(attr, cls, cls_doc, seen_methods)
+ if not attr_doc:
+ continue
+ seen_methods.add(attr_doc.name)
+ cls_doc.attributes.append(attr_doc)
+ except Exception as e:
+ logger.info(f"Failed to parse attribute: {attr} - {e}")
+
+ except Exception as e:
+ logger.error(f"Error processing class {cls.name}: {e}")
+ continue
+
+ return codegen_sdk_docs
diff --git a/src/codegen/sdk/code_generation/doc_utils/parse_docstring.py b/src/codegen/sdk/code_generation/doc_utils/parse_docstring.py
new file mode 100644
index 000000000..d367ad7b3
--- /dev/null
+++ b/src/codegen/sdk/code_generation/doc_utils/parse_docstring.py
@@ -0,0 +1,68 @@
+import re
+
+from codegen.sdk.code_generation.doc_utils.schemas import ParameterDoc
+
+SECTION_PATTERN = re.compile(r"(Args|Returns|Raises|Note):\s*(.+?)(?=(?:Args|Returns|Raises|Note):|$)", re.DOTALL)
+ARG_PATTERN = re.compile(r"\s*(\w+)\s*\(([^)]+)\):\s*([^\n]+)")
+
+
+def parse_docstring(docstring: str) -> dict | None:
+ """Parse a docstring into its components with optimized performance.
+
+ Args:
+ docstring (str): The docstring to parse
+
+ Returns:
+ dict | None: Parsed docstring components or None if parsing fails
+ """
+ # Strip once at the start
+ docstring = docstring.strip().strip('"""').strip("'''")
+
+ # Initialize result dictionary
+ result = {"description": "", "arguments": [], "return_description": None, "raises": [], "note": None}
+
+ # Find all sections
+ sections = {match.group(1): match.group(2).strip() for match in SECTION_PATTERN.finditer(docstring)}
+
+ # Get description (everything before first section)
+ first_section = docstring.find(":")
+ if first_section != -1:
+ result["description"] = docstring[:first_section].split("\n")[0].strip()
+ else:
+ result["description"] = docstring.split("\n")[0].strip()
+
+ # Parse Args section
+ if "Args" in sections:
+ args_text = sections["Args"]
+ if args_text.lower() != "none":
+ result["arguments"] = [ParameterDoc(name=m.group(1), type=m.group(2), description=m.group(3).strip()) for m in ARG_PATTERN.finditer(args_text)]
+
+ # Parse Returns section
+ if "Returns" in sections:
+ returns_text = sections["Returns"]
+ # Split on colon to separate type and description
+ parts = returns_text.split(":", 1)
+ if len(parts) > 1:
+ # Only keep the description part after the colon
+ result["return_description"] = " ".join(line.strip() for line in parts[1].split("\n") if line.strip())
+ else:
+ # If there's no colon, check if it's just a plain description without types
+ # Remove any type-like patterns (words followed by brackets or vertical bars)
+ cleaned_text = re.sub(r"^[^:]*?(?=\s*[A-Za-z].*:|\s*$)", "", returns_text)
+ if cleaned_text:
+ result["return_description"] = " ".join(line.strip() for line in cleaned_text.split("\n") if line.strip())
+
+ # Parse Raises section
+ if "Raises" in sections:
+ raises_text = sections["Raises"]
+ for line in raises_text.split("\n"):
+ if ":" in line:
+ exc_type, desc = line.split(":", 1)
+ if exc_type.strip():
+ result["raises"].append({"type": exc_type.strip(), "description": desc.strip()})
+
+ # Parse Note section
+ if "Note" in sections:
+ result["note"] = " ".join(line.strip() for line in sections["Note"].split("\n") if line.strip())
+
+ return result
diff --git a/src/codegen/sdk/code_generation/doc_utils/schemas.py b/src/codegen/sdk/code_generation/doc_utils/schemas.py
new file mode 100644
index 000000000..77b39d58a
--- /dev/null
+++ b/src/codegen/sdk/code_generation/doc_utils/schemas.py
@@ -0,0 +1,42 @@
+from typing import Literal
+
+from pydantic import BaseModel, Field
+
+
+class ParameterDoc(BaseModel):
+ name: str = Field(..., description="The name of the parameter")
+ description: str = Field(..., description="The description of the parameter")
+ type: str = Field(..., description="The type of the parameter")
+ default: str = Field(default="", description="The default value of the parameter")
+
+
+class MethodDoc(BaseModel):
+ name: str = Field(..., description="The name of the method")
+ description: str | None = Field(..., description="The description of the method")
+ parameters: list[ParameterDoc] = Field(..., description="The parameters of the method")
+ return_type: list[str] | None = Field(default=None, description="The return types of the method")
+ return_description: str | None = Field(default=None, description="The return description of the method")
+ method_type: Literal["method", "property", "attribute"] = Field(..., description="The type of the method")
+ code: str = Field(..., description="The signature of the method or attribute")
+ path: str = Field(..., description="The path of the method that indicates its parent class //")
+ raises: list[dict] | None = Field(..., description="The raises of the method")
+ metainfo: dict = Field(..., description="Information about the method's true parent class and path")
+ version: str = Field(..., description="The commit hash of the git commit that generated the docs")
+ github_url: str = Field(..., description="The github url of the method")
+
+
+class ClassDoc(BaseModel):
+ title: str = Field(..., description="The title of the class")
+ description: str = Field(..., description="The description of the class")
+ content: str = Field(..., description="The content of the class")
+ path: str = Field(..., description="The path of the class")
+ inherits_from: list[str] = Field(..., description="The classes that the class inherits from")
+ language: Literal["PYTHON", "TYPESCRIPT", "ALL", "NONE"] = Field(..., description="The language of the class")
+ version: str = Field(..., description="The commit hash of the git commit that generated the docs")
+ methods: list[MethodDoc] = Field(default=[], description="The methods of the class")
+ attributes: list[MethodDoc] = Field(default=[], description="The attributes of the class")
+ github_url: str = Field(..., description="The github url of the class")
+
+
+class GSDocs(BaseModel):
+ classes: list[ClassDoc] = Field(..., description="The classes to document")
diff --git a/src/codegen/sdk/code_generation/doc_utils/utils.py b/src/codegen/sdk/code_generation/doc_utils/utils.py
index 959f907f3..2b4d36834 100644
--- a/src/codegen/sdk/code_generation/doc_utils/utils.py
+++ b/src/codegen/sdk/code_generation/doc_utils/utils.py
@@ -1,94 +1,39 @@
+import logging
import re
-from copy import deepcopy
+import textwrap
-from codegen.sdk.code_generation.enums import DocumentationDecorators
+from codegen.sdk.core.class_definition import Class
from codegen.sdk.core.codebase import Codebase
+from codegen.sdk.core.detached_symbols.function_call import FunctionCall
+from codegen.sdk.core.expressions.type import Type
from codegen.sdk.core.function import Function
from codegen.sdk.core.symbol import Symbol
-from codegen.sdk.enums import NodeType, ProgrammingLanguage
-from codegen.sdk.python.class_definition import PyClass
-from codegen.sdk.python.function import PyFunction
-
-
-def get_api_classes_by_decorator(
- codebase: Codebase,
- language: ProgrammingLanguage = ProgrammingLanguage.PYTHON,
- docs: bool = True,
-) -> dict[str, PyClass]:
- """Returns all classes in a directory that have a specific decorator."""
- classes = {}
- language_specific_decorator = get_decorator_for_language(language).value
- general_decorator = DocumentationDecorators.GENERAL_API.value
- # get language specific classes
- for cls in codebase.classes:
- class_decorators = [decorator.name for decorator in cls.decorators]
- if language_specific_decorator in class_decorators:
- classes[cls.name] = cls
- for cls in codebase.classes:
- class_decorators = [decorator.name for decorator in cls.decorators]
- if general_decorator in class_decorators and cls.name not in classes.keys():
- classes[cls.name] = cls
- return classes
-
-
-def get_all_classes_to_document(codebase: Codebase) -> dict[str, PyClass]:
- """Returns all classes in a directory that have a specific decorator."""
- python_classes = get_api_classes_by_decorator(codebase=codebase, language=ProgrammingLanguage.PYTHON)
- typescript_classes = get_api_classes_by_decorator(codebase=codebase, language=ProgrammingLanguage.TYPESCRIPT)
- classes = {**typescript_classes, **python_classes} # Python values will overwrite TypeScript values in case of collision
- return classes
-
-
-def get_nearest_parent_docstring(method: PyFunction, cls: PyClass) -> str:
- """Returns the PyFunction of the first parent who has a docstring for it"""
- for parent in cls.superclasses():
- if not isinstance(parent, Symbol):
- continue
- for _method in parent.methods():
- if _method.name == method.name:
- if hasattr(_method, "docstring") and hasattr(_method.docstring, "text") and _method.docstring.text != "":
- return _method.docstring.source
- return ""
-
-
-def get_language_specific_classes(codebase: Codebase, language: ProgrammingLanguage) -> dict[str, PyClass]:
- classes = {}
- for cls in codebase.classes:
- if cls.get_attribute("language"):
- if cls.get_attribute("language").assignment.value.source.split(".")[1] == language.value:
- classes[cls.name] = cls
- return classes
-
-
-def get_codemod_classes(codebase: Codebase, language: ProgrammingLanguage) -> dict[str, PyClass]:
- classes = {}
- target_decorator = DocumentationDecorators.CODEMOD.value
- for cls in codebase.classes:
- if target_decorator in [decorator.name for decorator in cls.decorators]:
- if cls.get_attribute("language"):
- if cls.get_attribute("language").assignment.value.source.split(".")[1] == language.value:
- classes[cls.name] = cls
- return classes
-
-
-def is_property(method: PyFunction) -> bool:
- """Returns True if the method is a property (denoted by @property decorator)"""
- return method.is_property
-
-
-def get_parent(cls: PyClass) -> str | None:
- parents = [parent for parent in cls.parent_class_names if parent.source in cls.name]
- if len(parents) > 1:
- raise ValueError(f"More than one parent found for {cls.name}")
- if len(parents) == 0:
- return
- return parents[0].source
-
-
-def sanitize_mdx_mintlify_desscription(content: str) -> str:
+from codegen.sdk.enums import ProgrammingLanguage
+from codegen.sdk.python.statements.attribute import PyAttribute
+
+logger = logging.getLogger(__name__)
+
+
+def sanitize_docstring_for_markdown(docstring: str | None) -> str:
+ """Sanitize the docstring for MDX"""
+ if docstring is None:
+ return ""
+ docstring_lines = docstring.splitlines()
+ if len(docstring_lines) > 1:
+ docstring_lines[1:] = [textwrap.dedent(line) for line in docstring_lines[1:]]
+ docstring = "\n".join(docstring_lines)
+ if docstring.startswith('"""'):
+ docstring = docstring[3:]
+ if docstring.endswith('"""'):
+ docstring = docstring[:-3]
+ return docstring
+
+
+def sanitize_mdx_mintlify_description(content: str) -> str:
"""Mintlify description field needs to have string escaped, which content doesn't need.
the must be parsing the description differently or something
"""
+ content = sanitize_docstring_for_markdown(content)
# make sure all `< />` components are properly escaped with a `` inline-block
# if the string already has the single-quote then this is a no-op
content = re.sub(r"(?]+>)(?!`)", r"`\1`", content)
@@ -99,116 +44,292 @@ def sanitize_mdx_mintlify_desscription(content: str) -> str:
return re.sub(r'(")', r"\\\1", content)
-def filter_undocumented_methods_list(doc_methods: list[Function]) -> list[Function]:
- """Returns a list of methods for a given class that should be documented."""
- filtered_doc_methods = [m for m in doc_methods if not m.name.startswith("_")]
- filtered_doc_methods = [m for m in filtered_doc_methods if not any("noapidoc" in d.name for d in m.decorators)]
- return filtered_doc_methods
-
-
-def get_codegen_sdk_class_docstring(cls: PyClass, codebase: Codebase) -> str:
- """Get the documentation for a single GraphSitter class and its methods."""
- # =====[ Parent classes ]=====
- parent_classes = cls.parent_class_names
- parent_class_names = [parent.source for parent in parent_classes if parent.source not in ("Generic", "ABC", "Expression")]
- superclasses = ", ".join([name for name in parent_class_names])
- if len(superclasses) > 0:
- superclasses = f"({superclasses})"
-
- # =====[ Name + docstring ]=====
- source = f"class {cls.name}{superclasses}:"
- if cls.docstring is not None:
- source += set_indent(string=f'\n"""{cls.docstring.text}"""', indent=1)
- source += "\n"
-
- # =====[ Attributes ]=====
- if cls.is_subclass_of("Enum"):
- for attribute in cls.attributes:
- source += set_indent(f"\n{attribute.source}", 1)
- else:
- for attribute in cls.attributes(private=False, max_depth=None):
- # Only document attributes which have docstrings
- if docstring := attribute.docstring(cls):
- source += set_indent(f"\n{attribute.attribute_docstring}", 1)
- source += set_indent(string=f'\n"""{docstring}"""', indent=2)
- source += set_indent("\n...\n", 2)
-
- # =====[ Get inherited method ]=====
- def get_inherited_method(superclasses, method):
- """Returns True if the method is inherited"""
- for s in superclasses:
- for m in s.methods:
- if m.name == method.name:
- if m.docstring == method.docstring or method.docstring is None:
- return m
+def sanitize_html_for_mdx(html_string: str) -> str:
+ """Sanitize HTML string for MDX by escaping double quotes in attribute values.
+
+ Args:
+ html_string (str): The input HTML string to sanitize
+
+ Returns:
+ str: The sanitized HTML string with escaped quotes
+ """
+ # Replace double quotes with " but only in HTML attributes
+ return re.sub(r'"', """, html_string)
+
+
+def get_type_str(parent, curr_depth=0, max_depth=5):
+ """Returns the type node for an attribute."""
+ if curr_depth >= max_depth:
return None
+ if isinstance(parent, Type):
+ return parent
+ for child in parent.children:
+ if attr_type := get_type_str(child, curr_depth=curr_depth + 1):
+ return attr_type
+ return None
+
+
+def is_language_base_class(cls_obj: Class):
+ """Returns true if `cls_obj` is a direct parent of a language specific class.
+
+ For example, `Symbol` which is a direct parent of `PySymbol` and `TsSymbol` is a language base class
+ and `Editable` is not.
+
+ Args:
+ cls_obj (Class): the class object to check
+
+ Returns:
+ bool: if `cls_obj` is a language base class
+ """
+ sub_classes = cls_obj.subclasses(max_depth=1)
+ base_name = cls_obj.name.lower()
+ return any(sub_class.name.lower() in [f"py{base_name}", f"ts{base_name}"] for sub_class in sub_classes)
+
+
+def get_section(symbol: Symbol, parent_class: Class | None = None):
+ if parent_class:
+ doc_section = parent_class.filepath.split("/")[1]
+ else:
+ doc_section = symbol.filepath.split("/")[1]
+ return doc_section
+
+
+def get_langauge(symbol: Class | Function | PyAttribute) -> str:
+ """Gets the language of which the symbol is an abstract representation.
+
+ Args:
+ symbol (Class | Function | PyAttribute): the symbol to get the langauge of
+ Returns:
+ str: the language of the symbol
+ """
+ if ProgrammingLanguage.PYTHON.value.lower() in symbol.filepath:
+ return ProgrammingLanguage.PYTHON.value
+ elif ProgrammingLanguage.TYPESCRIPT.value.lower() in symbol.filepath:
+ return ProgrammingLanguage.TYPESCRIPT.value
+ elif isinstance(symbol, Class) and is_language_base_class(symbol):
+ return "NONE"
+ elif isinstance(symbol.parent_class, Class) and is_language_base_class(symbol.parent_class):
+ return "NONE"
+ else:
+ return "ALL"
+
+
+def get_type(method: Function):
+ """Return the type of method.
- # =====[ Get superclasses ]=====
- superclasses = cls.superclasses
- superclasses = list({s.name: s for s in superclasses}.values())
- superclasses = [x for x in superclasses if x.node_type != NodeType.EXTERNAL]
-
- # TODO use new filter_methods_list function here
- # =====[ Get methods to be documented ]=====
- doc_methods = cls.methods
- doc_methods = [m for m in doc_methods if not m.name.startswith("_")]
- doc_methods = [m for m in doc_methods if not any("noapidoc" in d.name for d in m.decorators)]
- doc_methods = [m for m in doc_methods if get_inherited_method(superclasses, m) is None]
-
- # =====[ Methods ]=====
- for method in doc_methods:
- if "property" in [decorator.name for decorator in method.decorators]:
- source += set_indent(f"\n@property\n{method.function_signature}", 1)
- else:
- source += set_indent(f"\n{method.function_signature}", 1)
- if method.docstring is not None:
- source += set_indent(string=f'\n"""{method.docstring.text}"""', indent=2)
- source += set_indent("\n...\n", 2)
-
- # =====[ Format markdown ]=====
- return f"""### {cls.name}\n\n{format_python_codeblock(source)}"""
-
-
-def remove_first_indent(text):
- lines = text.split("\n")
- first_line = lines[0]
- rest = "\n".join(lines[1:])
- set_indent(rest, 1)
- return first_line + "\n" + rest
-
-
-def format_python_codeblock(source: str) -> str:
- """A python codeblock in markdown format."""
- # USE 4 backticks instead of 3 so backticks inside the codeblock are handled properly
- cb = f"````python\n{source}\n````"
- return cb
-
-
-def set_indent(string: str, indent: int) -> str:
- """Sets the indentation of a string."""
- tab = "\t"
- return "\n".join([f"{tab * indent}{line}" for line in string.split("\n")])
-
-
-def sort_docstrings(docstrings: dict[str, str], preferred_order: list[str]) -> list[str]:
- """Sorts docstrings to a preferred order, putting un-referenced ones last."""
- docstrings = deepcopy(docstrings)
- # ======[ Sort docstrings ]=====
- # Puts un-referenced docstrings last
- sorted_docstrings = []
- for class_name in preferred_order:
- if class_name in docstrings:
- sorted_docstrings.append(docstrings[class_name])
- del docstrings[class_name]
- for class_name in docstrings:
- sorted_docstrings.append(docstrings[class_name])
- return sorted_docstrings
-
-
-def get_decorator_for_language(
- language: ProgrammingLanguage = ProgrammingLanguage.PYTHON,
-) -> DocumentationDecorators:
- if language == ProgrammingLanguage.PYTHON:
- return DocumentationDecorators.PYTHON
- elif language == ProgrammingLanguage.TYPESCRIPT:
- return DocumentationDecorators.TYPESCRIPT
+ Args:
+ method (Function): the method to check the type of.
+
+ Returns:
+ str: `property` if the method is a property, `method` otherwise.
+ """
+ if method.is_property:
+ return "property"
+ else:
+ return "method"
+
+
+def is_settter(m: Function):
+ """Checks if `m` is a setter method
+ Args:
+ m (Function): the function (method) to check
+ Returns:
+ bool: `True` if `m` is a setter method, `False` otherwise
+ """
+ return any([dec.name == f"{m.name}.setter" for dec in m.decorators])
+
+
+def create_path(symbol: Class | Function | PyAttribute, parent_class: Class | None = None) -> str:
+ """Creates a route path for `symbol` that will be used in the frontend
+
+ Args:
+ symbol (Class | Function | PyAttribute): the object for which a path should be created
+ parent_class (Class | None): optional parent class of the method
+ Returns:
+ str: route path of `symbol`
+ """
+ name = symbol.name
+ language = get_langauge(symbol)
+
+ if language == ProgrammingLanguage.PYTHON.value:
+ doc_section = ProgrammingLanguage.PYTHON.value.lower()
+ elif language == ProgrammingLanguage.TYPESCRIPT.value:
+ doc_section = ProgrammingLanguage.TYPESCRIPT.value.lower()
+ else:
+ doc_section = "core"
+
+ if isinstance(symbol, Class):
+ return f"api-reference/{doc_section}/{name}"
+
+ if parent_class:
+ parent_name = parent_class.name
+ else:
+ parent_name = symbol.parent_class.name
+
+ if isinstance(symbol, Function) and is_settter(symbol):
+ return f"api-reference/{doc_section}/{parent_name}/set_{name}"
+
+ return f"api-reference/{doc_section}/{parent_name}/{name}"
+
+
+def has_documentation(c: Class):
+ """If the class c is meant to be documented.
+
+ Args:
+ c (Class): the class to check
+ Returns:
+ bool: `True` if the class is meant to be documented, `False` otherwise
+ """
+ return any([dec.name == "ts_apidoc" or dec.name == "py_apidoc" or dec.name == "apidoc" for dec in c.decorators])
+
+
+def find_symbol(codebase: Codebase, symbol_name: str, resolved_types: list[Type], parent_class: Class, parent_symbol: Symbol, types_cache: dict):
+ """Find the symbol in the codebase.
+
+ Args:
+ codebase (Codebase): the codebase to search in
+ symbol_name (str): the name of the symbol to resolve
+ resolved_types (list[Type]): the resolved types of the symbol
+ parent_class (Class): the parent class of the symbol
+ types_cache (dict): the cache to store the results in
+ Returns:
+ str: the route path of the symbol
+ """
+ if symbol_name in ["list", "tuple", "int", "str", "dict", "set", "None", "bool", "optional", "Union"]:
+ return symbol_name
+ if symbol_name.lower() == "self":
+ return f"<{create_path(parent_class)}>"
+ if symbol_name in types_cache:
+ return types_cache[symbol_name]
+ # if symbol_name in [resolved_type.value for resolved_type in resolved_types]:
+ # return symbol_name
+
+ try:
+ trgt_symbol = None
+ cls_obj = codebase.get_class(symbol_name, optional=True)
+ if cls_obj:
+ trgt_symbol = cls_obj
+
+ if not trgt_symbol:
+ if symbol := parent_symbol.file.get_symbol(symbol_name):
+ for resolved_type in symbol.resolved_types:
+ if isinstance(resolved_type, FunctionCall) and len(resolved_type.args) >= 2:
+ bound_arg = resolved_type.args[1]
+ bound_name = bound_arg.value
+ if cls_obj := codebase.get_class(bound_name, optional=True):
+ trgt_symbol = cls_obj
+ break
+
+ elif symbol := codebase.get_symbol(symbol_name, optional=True):
+ if len(symbol.resolved_types) == 1:
+ trgt_symbol = symbol.resolved_types[0]
+
+ if trgt_symbol and has_documentation(trgt_symbol):
+ trgt_path = f"<{create_path(trgt_symbol)}>"
+ types_cache[symbol_name] = trgt_path
+ return trgt_path
+ except Exception as e:
+ logger.warning(f"Unable to resolve {symbol_name}. Received error: {e}.")
+
+ return symbol_name
+
+
+def replace_multiple_types(codebase: Codebase, input_str: str, resolved_types: list[Type], parent_class: Class, parent_symbol: Symbol, types_cache: dict) -> str:
+ """Replace multiple types in a string.
+
+ Args:
+ codebase (Codebase): the codebase to search in
+ input_str (str): the string to replace the types in
+ parent_class (Class): the parent class of the symbol
+ types_cache (dict): the cache to store the results in
+ Returns:
+ str: the string with the types replaced
+ """
+ # Remove outer quotes if present
+ input_str = input_str.replace('"', "")
+
+ def process_parts(content):
+ # Handle nested brackets recursively
+ stack = []
+ current = ""
+ parts = []
+ separators = []
+ in_quotes = False
+ quote_char = None
+
+ i = 0
+ while i < len(content):
+ char = content[i]
+
+ # Handle quotes
+ if char in "\"'":
+ if not in_quotes:
+ in_quotes = True
+ quote_char = char
+ elif char == quote_char:
+ in_quotes = False
+ current += char
+ # Only process special characters if we're not in quotes
+ elif not in_quotes:
+ if char == "[":
+ stack.append("[")
+ current += char
+ elif char == "]":
+ if stack:
+ stack.pop()
+ current += char
+ elif (char in ",|") and not stack: # Only split when not inside brackets
+ if current.strip():
+ parts.append(current.strip())
+ separators.append(char)
+ current = ""
+ else:
+ current += char
+ else:
+ current += char
+ i += 1
+
+ if current.strip():
+ parts.append(current.strip())
+
+ # Process each part
+ processed_parts = []
+ for part in parts:
+ # Check if the part is quoted
+ if part.startswith('"') and part.endswith('"'):
+ processed_parts.append(part) # Keep quoted parts as-is
+ continue
+
+ # Check if the part itself contains brackets
+ if "[" in part:
+ base_type = part[: part.index("[")]
+ bracket_content = part[part.index("[") :].strip("[]")
+ processed_bracket = process_parts(bracket_content)
+ replacement = find_symbol(codebase=codebase, symbol_name=base_type, resolved_types=resolved_types, parent_class=parent_class, parent_symbol=parent_symbol, types_cache=types_cache)
+ processed_part = replacement + "[" + processed_bracket + "]"
+ else:
+ replacement = find_symbol(codebase=codebase, symbol_name=part, resolved_types=resolved_types, parent_class=parent_class, parent_symbol=parent_symbol, types_cache=types_cache)
+ processed_part = replacement
+ processed_parts.append(processed_part)
+
+ # Reconstruct with original separators
+ result = processed_parts[0]
+ for i in range(len(separators)):
+ result += f"{separators[i]} {processed_parts[i + 1]}"
+
+ return result
+
+ # Check if the input contains any separators
+ if any(sep in input_str for sep in ",|"):
+ return process_parts(input_str)
+ # Handle bracketed input
+ elif "[" in input_str:
+ base_type = input_str[: input_str.index("[")]
+ bracket_content = input_str[input_str.index("[") :].strip("[]")
+ processed_content = process_parts(bracket_content)
+ replacement = find_symbol(codebase=codebase, symbol_name=base_type, resolved_types=resolved_types, parent_class=parent_class, parent_symbol=parent_symbol, types_cache=types_cache)
+ return replacement + "[" + processed_content + "]"
+ # Handle simple input
+ else:
+ replacement = find_symbol(codebase=codebase, symbol_name=input_str, resolved_types=resolved_types, parent_class=parent_class, parent_symbol=parent_symbol, types_cache=types_cache)
+ return replacement
diff --git a/src/codegen/sdk/code_generation/mdx_docs_generation.py b/src/codegen/sdk/code_generation/mdx_docs_generation.py
index 5484eb330..8a1ddb399 100644
--- a/src/codegen/sdk/code_generation/mdx_docs_generation.py
+++ b/src/codegen/sdk/code_generation/mdx_docs_generation.py
@@ -1,162 +1,90 @@
-import textwrap
-
-from codegen.sdk.code_generation.doc_utils.utils import (
- filter_undocumented_methods_list,
- get_all_classes_to_document,
- get_nearest_parent_docstring,
- is_property,
- sanitize_mdx_mintlify_desscription,
-)
-from codegen.sdk.core.codebase import Codebase
-from codegen.sdk.python.class_definition import PyClass
-from codegen.sdk.python.detached_symbols.parameter import PyParameter
-from codegen.sdk.python.function import PyFunction
-from codegen.sdk.python.statements.attribute import PyAttribute
-
-
-def render_mdx_for_codebase_page(codebase: Codebase) -> str:
- """Renders the MDX for the `Codebase` page"""
- cls = codebase.get_symbol("Codebase")
-
- return f"""{render_mdx_page_title(cls, icon="brain-circuit")}
-{render_mdx_inheritence_section(cls, codebase)}
-{render_mdx_properties_section(cls)}
-{render_mdx_methods_section(cls)}
-"""
+import re
+
+from codegen.sdk.code_generation.doc_utils.schemas import ClassDoc, MethodDoc, ParameterDoc
+from codegen.sdk.code_generation.doc_utils.utils import sanitize_html_for_mdx, sanitize_mdx_mintlify_description
-def render_mdx_page_for_class(cls: PyClass, codebase: Codebase) -> str:
+def render_mdx_page_for_class(cls_doc: ClassDoc) -> str:
"""Renders the MDX for a single class"""
- return f"""{render_mdx_page_title(cls)}
-{render_mdx_inheritence_section(cls, codebase)}
-{render_mdx_properties_section(cls)}
-{render_mdx_attributes_section(cls)}
-{render_mdx_methods_section(cls)}
+ return f"""{render_mdx_page_title(cls_doc)}
+{render_mdx_inheritence_section(cls_doc)}
+{render_mdx_attributes_section(cls_doc)}
+{render_mdx_methods_section(cls_doc)}
"""
-def render_mdx_page_title(cls: PyClass, icon: str | None = None) -> str:
+def render_mdx_page_title(cls_doc: ClassDoc, icon: str | None = None) -> str:
"""Renders the MDX for the page title"""
- page_desc = cls.docstring.text if hasattr(cls, "docstring") and hasattr(cls.docstring, "text") else ""
+ page_desc = cls_doc.description if hasattr(cls_doc, "description") else ""
return f"""---
-title: "{cls.name}"
-sidebarTitle: "{cls.name}"
+title: "{cls_doc.title}"
+sidebarTitle: "{cls_doc.title}"
icon: "{icon if icon else ""}"
-description: "{sanitize_mdx_mintlify_desscription(page_desc)}"
+description: "{sanitize_mdx_mintlify_description(page_desc)}"
---
+import {{Parameter}} from '/snippets/Parameter.mdx';
+import {{ParameterWrapper}} from '/snippets/ParameterWrapper.mdx';
+import {{Return}} from '/snippets/Return.mdx';
+import {{HorizontalDivider}} from '/snippets/HorizontalDivider.mdx';
+import {{GithubLinkNote}} from '/snippets/GithubLinkNote.mdx';
+import {{Attribute}} from '/snippets/Attribute.mdx';
+
+
"""
-def render_mdx_inheritence_section(cls: PyClass, codebase: Codebase) -> str:
+def render_mdx_inheritence_section(cls_doc: ClassDoc) -> str:
"""Renders the MDX for the inheritence section"""
# Filter on parents who we have docs for
- all_classes_to_document = get_all_classes_to_document(codebase)
- parents = cls.superclasses()
- parents_to_document = []
- for parent in parents:
- if parent.name in all_classes_to_document.keys():
- parents_to_document.append(parent)
- if len(parents_to_document) <= 0:
+ parents = cls_doc.inherits_from
+ if not parents:
return ""
- parents_string = ", ".join([f"[{parent.name}](/{get_mdx_route_for_class(parent)})" for parent in parents_to_document])
+ parents_string = ", ".join([parse_link(parent) for parent in parents])
return f"""### Inherits from
{parents_string}
"""
-def render_mdx_attributes_section(cls: PyClass) -> str:
+def render_mdx_attributes_section(cls_doc: ClassDoc) -> str:
"""Renders the MDX for the attributes section"""
- filtered_attributes = cls.attributes(private=False, max_depth=None)
- # filter for only properties
- filtered_attributes = [attribute for attribute in filtered_attributes if attribute.docstring(cls) is not None]
- sorted_attributes = sorted(filtered_attributes, key=lambda x: x.name)
+ sorted_attributes = sorted(cls_doc.attributes + [method for method in cls_doc.methods if method.method_type == "property"], key=lambda x: x.name)
if len(sorted_attributes) <= 0:
return ""
- attributes_mdx_string = "\n".join([render_mdx_for_attribute(attribute, cls) for attribute in sorted_attributes])
+ attributes_mdx_string = "\n".join([render_mdx_for_attribute(attribute) for attribute in sorted_attributes])
return f"""## Attributes
----
+
{attributes_mdx_string}
"""
-def render_mdx_properties_section(cls: PyClass) -> str:
- """Renders the MDX for the properties section"""
- filtered_methods = filter_undocumented_methods_list(cls.methods(private=False, max_depth=None))
- # filter for only properties
- filtered_methods = [method for method in filtered_methods if is_property(method)]
- sorted_methods = sorted(filtered_methods, key=lambda x: x.name)
- if len(sorted_methods) <= 0:
- return ""
- properties_mdx_string = "\n".join([render_mdx_for_property(property, cls) for property in sorted_methods])
-
- return f"""## Properties
----
-{properties_mdx_string}
-"""
-
-
-def render_mdx_methods_section(cls: PyClass) -> str:
+def render_mdx_methods_section(cls_doc: ClassDoc) -> str:
"""Renders the MDX for the methods section"""
- filtered_methods = filter_undocumented_methods_list(cls.methods(private=False, max_depth=None))
- # filter properties out of here
- filtered_methods = [method for method in filtered_methods if not is_property(method)]
- sorted_methods = sorted(filtered_methods, key=lambda x: x.name)
+ sorted_methods = sorted(cls_doc.methods, key=lambda x: x.name)
if len(sorted_methods) <= 0:
return ""
- methods_mdx_string = "\n".join([render_mdx_for_method(method, cls) for method in sorted_methods])
+ methods_mdx_string = "\n".join([render_mdx_for_method(method) for method in sorted_methods if method.method_type == "method"])
return f"""## Methods
----
+
{methods_mdx_string}
"""
-def sanitize_docstring_for_markdown(docstring: str) -> str:
- """Sanitize the docstring for MDX"""
- docstring_lines = docstring.splitlines()
- if len(docstring_lines) > 1:
- docstring_lines[1:] = [textwrap.dedent(line) for line in docstring_lines[1:]]
- docstring = "\n".join(docstring_lines)
- if docstring.startswith('"""'):
- docstring = docstring[3:]
- if docstring.endswith('"""'):
- docstring = docstring[:-3]
- return docstring
-
-
-def render_mdx_for_attribute(attribute: PyAttribute, cls: PyClass) -> str:
- """Renders the MDX for a single property"""
- attribute_docstring = attribute.docstring(cls)
- attribute_docstring = sanitize_docstring_for_markdown(attribute_docstring)
-
- return f"""### `{attribute.name}`
-{attribute_docstring}
-
-```python
-{attribute.attribute_docstring}
-```
-
-""".strip()
-
-
-def render_mdx_for_property(property: PyFunction, cls: PyClass) -> str:
- """Renders the MDX for a single property"""
- property_docstring = property.docstring.source if hasattr(property, "docstring") and hasattr(property.docstring, "source") else ""
- if property_docstring == "":
- property_docstring = get_nearest_parent_docstring(property, cls)
- property_docstring = sanitize_docstring_for_markdown(property_docstring)
-
- return f"""### `{property.name}`
-{property_docstring}
-
-```python
-{property.function_signature}
- ...
-```
-
-""".strip()
+def render_mdx_for_attribute(attribute: MethodDoc) -> str:
+ """Renders the MDX for a single attribute"""
+ attribute_docstring = sanitize_mdx_mintlify_description(attribute.description)
+ if len(attribute.return_type) > 0:
+ return_type = f"{resolve_type_string(attribute.return_type[0])}"
+ else:
+ return_type = ""
+ if not attribute_docstring:
+ attribute_docstring = "\n"
+ return f"""### {attribute.name}
+
+"} }} description="{attribute_docstring}" />
+"""
########################################################################################################################
@@ -164,47 +92,113 @@ def render_mdx_for_property(property: PyFunction, cls: PyClass) -> str:
########################################################################################################################
-def format_parameter_for_mdx(parameter: PyParameter) -> str:
+def format_parameter_for_mdx(parameter: ParameterDoc) -> str:
+ type_string = resolve_type_string(parameter.type)
return f"""
-
-""".strip()
+
+""".strip()
-def format_parameters_for_mdx(parameters: list[PyParameter]) -> str:
- params = [x for x in parameters if not (x.name.startswith("_") or x.name == "self")]
- return "\n".join([format_parameter_for_mdx(parameter) for parameter in params])
+def format_parameters_for_mdx(parameters: list[ParameterDoc]) -> str:
+ return "\n".join([format_parameter_for_mdx(parameter) for parameter in parameters])
-def render_mdx_for_method(method: PyFunction, cls: PyClass) -> str:
- method_docstring = method.docstring.source if hasattr(method, "docstring") and hasattr(method.docstring, "source") else ""
+def format_return_for_mdx(return_type: list[str], return_description: str) -> str:
+ description = sanitize_html_for_mdx(return_description) if return_description else ""
+ return_type = resolve_type_string(return_type[0])
- if method_docstring == "":
- method_docstring = get_nearest_parent_docstring(method, cls)
+ return f"""
+
+"""
- method_docstring = sanitize_docstring_for_markdown(method_docstring)
+def render_mdx_for_method(method: MethodDoc) -> str:
+ description = sanitize_mdx_mintlify_description(method.description)
# =====[ RENDER ]=====
# TODO add links here
# TODO add inheritence info here
- mdx_string = f"""### `{method.name}`
-{method_docstring}
-```python
-{method.function_signature}
- ...
-```
+ mdx_string = f"""### {method.name}
+{description}
+
+"""
+ if method.parameters:
+ mdx_string += f"""
+
+{format_parameters_for_mdx(method.parameters)}
+
+"""
+ if method.return_type:
+ mdx_string += f"""
+{format_return_for_mdx(method.return_type, method.return_description)}
"""
return mdx_string
-def get_mdx_route_for_class(cls: PyClass) -> str:
+def get_mdx_route_for_class(cls_doc: ClassDoc) -> str:
"""Get the expected MDX route for a class
split by /core, /python, and /typescript
"""
- lower_class_name = cls.name.lower()
+ lower_class_name = cls_doc.title.lower()
if lower_class_name.startswith("py"):
- return f"codebase-sdk/python/{cls.name}"
+ return f"codebase-sdk/python/{cls_doc.title}"
elif lower_class_name.startswith(("ts", "jsx")):
- return f"codebase-sdk/typescript/{cls.name}"
+ return f"codebase-sdk/typescript/{cls_doc.title}"
+ else:
+ return f"codebase-sdk/core/{cls_doc.title}"
+
+
+def format_type_string(type_string: str) -> str:
+ type_string = type_string.split("|")
+ return " | ".join([type_str.strip() for type_str in type_string])
+
+
+def resolve_type_string(type_string: str) -> str:
+ if "<" in type_string:
+ return f"<>{parse_link(type_string, href=True)}>"
else:
- return f"codebase-sdk/core/{cls.name}"
+ return f'{format_type_string(type_string)}'
+
+
+def format_builtin_type_string(type_string: str) -> str:
+ if "|" in type_string:
+ type_strings = type_string.split("|")
+ return " | ".join([type_str.strip() for type_str in type_strings])
+ return type_string
+
+
+def span_type_string_by_pipe(type_string: str) -> str:
+ if "|" in type_string:
+ type_strings = type_string.split("|")
+ return " | ".join([f"{type_str.strip()}" for type_str in type_strings])
+ return type_string
+
+
+def parse_link(type_string: str, href: bool = False) -> str:
+ # Match components with angle brackets, handling nested structures
+
+ parts = [p for p in re.split(r"(<[^>]+>)", type_string) if p]
+
+ result = []
+ for part in parts:
+ if part.startswith("<") and part.endswith(">"):
+ # Extract the path from between angle brackets
+ path = part[1:-1]
+ symbol = path.split("/")[-1]
+
+ # Create a Link object
+ link = f'{symbol}' if href else f"[{symbol}](/{path})"
+ result.append(link)
+ else:
+ part = format_builtin_type_string(part)
+ if href:
+ result.append(f'{part.strip()}')
+ else:
+ result.append(part.strip())
+
+ return " ".join(result)
diff --git a/src/codegen/sdk/code_generation/prompts/api_docs.py b/src/codegen/sdk/code_generation/prompts/api_docs.py
index ce5567d87..6c12d513f 100644
--- a/src/codegen/sdk/code_generation/prompts/api_docs.py
+++ b/src/codegen/sdk/code_generation/prompts/api_docs.py
@@ -1,10 +1,7 @@
import logging
from codegen.sdk.code_generation.codegen_sdk_codebase import get_codegen_sdk_codebase
-from codegen.sdk.code_generation.doc_utils.utils import (
- get_api_classes_by_decorator,
- get_codegen_sdk_class_docstring,
-)
+from codegen.sdk.code_generation.prompts.utils import get_api_classes_by_decorator, get_codegen_sdk_class_docstring
from codegen.sdk.core.codebase import Codebase
from codegen.sdk.enums import ProgrammingLanguage
diff --git a/src/codegen/sdk/code_generation/prompts/utils.py b/src/codegen/sdk/code_generation/prompts/utils.py
new file mode 100644
index 000000000..bbf5cce3e
--- /dev/null
+++ b/src/codegen/sdk/code_generation/prompts/utils.py
@@ -0,0 +1,109 @@
+from codegen.sdk.code_generation.enums import DocumentationDecorators
+from codegen.sdk.core.codebase import Codebase
+from codegen.sdk.enums import NodeType, ProgrammingLanguage
+from codegen.sdk.python.class_definition import PyClass
+
+
+def get_decorator_for_language(
+ language: ProgrammingLanguage = ProgrammingLanguage.PYTHON,
+) -> DocumentationDecorators:
+ if language == ProgrammingLanguage.PYTHON:
+ return DocumentationDecorators.PYTHON
+ elif language == ProgrammingLanguage.TYPESCRIPT:
+ return DocumentationDecorators.TYPESCRIPT
+
+
+def get_api_classes_by_decorator(
+ codebase: Codebase,
+ language: ProgrammingLanguage = ProgrammingLanguage.PYTHON,
+) -> dict[str, PyClass]:
+ """Returns all classes in a directory that have a specific decorator."""
+ classes = {}
+ language_specific_decorator = get_decorator_for_language(language).value
+ general_decorator = DocumentationDecorators.GENERAL_API.value
+ # get language specific classes
+ for cls in codebase.classes:
+ class_decorators = [decorator.name for decorator in cls.decorators]
+ if language_specific_decorator in class_decorators:
+ classes[cls.name] = cls
+ for cls in codebase.classes:
+ class_decorators = [decorator.name for decorator in cls.decorators]
+ if general_decorator in class_decorators and cls.name not in classes.keys():
+ classes[cls.name] = cls
+ return classes
+
+
+def format_python_codeblock(source: str) -> str:
+ """A python codeblock in markdown format."""
+ # USE 4 backticks instead of 3 so backticks inside the codeblock are handled properly
+ cb = f"````python\n{source}\n````"
+ return cb
+
+
+def set_indent(string: str, indent: int) -> str:
+ """Sets the indentation of a string."""
+ tab = "\t"
+ return "\n".join([f"{tab * indent}{line}" for line in string.split("\n")])
+
+
+def get_codegen_sdk_class_docstring(cls: PyClass, codebase: Codebase) -> str:
+ """Get the documentation for a single GraphSitter class and its methods."""
+ # =====[ Parent classes ]=====
+ parent_classes = cls.parent_class_names
+ parent_class_names = [parent.source for parent in parent_classes if parent.source not in ("Generic", "ABC", "Expression")]
+ superclasses = ", ".join([name for name in parent_class_names])
+ if len(superclasses) > 0:
+ superclasses = f"({superclasses})"
+
+ # =====[ Name + docstring ]=====
+ source = f"class {cls.name}{superclasses}:"
+ if cls.docstring is not None:
+ source += set_indent(string=f'\n"""{cls.docstring.text}"""', indent=1)
+ source += "\n"
+
+ # =====[ Attributes ]=====
+ if cls.is_subclass_of("Enum"):
+ for attribute in cls.attributes:
+ source += set_indent(f"\n{attribute.source}", 1)
+ else:
+ for attribute in cls.attributes(private=False, max_depth=None):
+ # Only document attributes which have docstrings
+ if docstring := attribute.docstring(cls):
+ source += set_indent(f"\n{attribute.attribute_docstring}", 1)
+ source += set_indent(string=f'\n"""{docstring}"""', indent=2)
+ source += set_indent("\n...\n", 2)
+
+ # =====[ Get inherited method ]=====
+ def get_inherited_method(superclasses, method):
+ """Returns True if the method is inherited"""
+ for s in superclasses:
+ for m in s.methods:
+ if m.name == method.name:
+ if m.docstring == method.docstring or method.docstring is None:
+ return m
+ return None
+
+ # =====[ Get superclasses ]=====
+ superclasses = cls.superclasses
+ superclasses = list({s.name: s for s in superclasses}.values())
+ superclasses = [x for x in superclasses if x.node_type != NodeType.EXTERNAL]
+
+ # TODO use new filter_methods_list function here
+ # =====[ Get methods to be documented ]=====
+ doc_methods = cls.methods
+ doc_methods = [m for m in doc_methods if not m.name.startswith("_")]
+ doc_methods = [m for m in doc_methods if not any("noapidoc" in d.name for d in m.decorators)]
+ doc_methods = [m for m in doc_methods if get_inherited_method(superclasses, m) is None]
+
+ # =====[ Methods ]=====
+ for method in doc_methods:
+ if "property" in [decorator.name for decorator in method.decorators]:
+ source += set_indent(f"\n@property\n{method.function_signature}", 1)
+ else:
+ source += set_indent(f"\n{method.function_signature}", 1)
+ if method.docstring is not None:
+ source += set_indent(string=f'\n"""{method.docstring.text}"""', indent=2)
+ source += set_indent("\n...\n", 2)
+
+ # =====[ Format markdown ]=====
+ return f"""### {cls.name}\n\n{format_python_codeblock(source)}"""
diff --git a/tests/unit/api_doc_generation/test_api_doc_generation.py b/tests/unit/api_doc_generation/test_api_doc_generation.py
index 6816eef87..247e264bd 100644
--- a/tests/unit/api_doc_generation/test_api_doc_generation.py
+++ b/tests/unit/api_doc_generation/test_api_doc_generation.py
@@ -1,7 +1,6 @@
import pytest
from codegen.sdk.ai.helpers import count_tokens
-from codegen.sdk.code_generation.doc_utils.utils import get_decorator_for_language
from codegen.sdk.code_generation.prompts.api_docs import get_codegen_sdk_codebase, get_codegen_sdk_docs
from codegen.sdk.core.symbol import Symbol
from codegen.sdk.enums import ProgrammingLanguage
@@ -57,14 +56,3 @@ def test_get_codegen_sdk_codebase(codebase, language) -> None:
superclasses = func.superclasses()
callable = [x for x in superclasses if isinstance(x, Symbol) and x.name == "Callable"]
assert len(callable) == 1
-
-
-@pytest.mark.xdist_group("codegen")
-@pytest.mark.parametrize("language", [ProgrammingLanguage.PYTHON, ProgrammingLanguage.TYPESCRIPT])
-def test_api_doc_generation(codebase, language) -> None:
- api_docs = get_codegen_sdk_docs(language=language, codebase=codebase)
- decorator = get_decorator_for_language(language).value
-
- for cls in codebase.classes:
- if decorator in [decorator.name for decorator in cls.decorators]:
- assert f"class {cls.name}" in api_docs, f"Documentation for class '{cls.name}' not found in {language.value} API docs"
diff --git a/tests/verified_codemods/codemod_data/YTY2NWE0NT.json b/tests/verified_codemods/codemod_data/YTY2NWE0NT.json
index 11cad1d92..ad0532bb1 100644
--- a/tests/verified_codemods/codemod_data/YTY2NWE0NT.json
+++ b/tests/verified_codemods/codemod_data/YTY2NWE0NT.json
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:615e0ddf0aa36fb987c2033fc3db9077d4ca66be8c903b3b20678b58946fe8ee
-size 15512725
+oid sha256:c7adb21bd213dfec5311ea1d1e9426b41fd410fbe7a7ba2b3029dcb40b67f7ec
+size 15513609
diff --git a/uv.lock b/uv.lock
index d7fc4977e..5b7e2988b 100644
--- a/uv.lock
+++ b/uv.lock
@@ -381,6 +381,7 @@ dependencies = [
{ name = "dicttoxml" },
{ name = "docstring-parser" },
{ name = "emoji" },
+ { name = "fastapi", extra = ["standard"] },
{ name = "gitpython" },
{ name = "giturlparse" },
{ name = "hatch-vcs" },
@@ -412,6 +413,7 @@ dependencies = [
{ name = "rich-click" },
{ name = "rustworkx" },
{ name = "sentry-sdk" },
+ { name = "starlette" },
{ name = "tabulate" },
{ name = "tenacity" },
{ name = "termcolor" },
@@ -475,6 +477,7 @@ requires-dist = [
{ name = "dicttoxml", specifier = ">=1.7.16,<2.0.0" },
{ name = "docstring-parser", specifier = ">=0.16,<1.0" },
{ name = "emoji", specifier = ">=2.14.0" },
+ { name = "fastapi", extras = ["standard"], specifier = ">=0.115.2,<1.0.0" },
{ name = "gitpython", specifier = "==3.1.44" },
{ name = "giturlparse" },
{ name = "hatch-vcs", specifier = ">=0.4.0" },
@@ -506,6 +509,7 @@ requires-dist = [
{ name = "rich-click", specifier = ">=1.8.5" },
{ name = "rustworkx", specifier = ">=0.15.1" },
{ name = "sentry-sdk", specifier = "==2.20.0" },
+ { name = "starlette", specifier = ">=0.16.0,<1.0.0" },
{ name = "tabulate", specifier = ">=0.9.0,<1.0.0" },
{ name = "tenacity", specifier = ">=9.0.0" },
{ name = "termcolor", specifier = ">=2.4.0" },
@@ -839,6 +843,49 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/43/09/2aea36ff60d16dd8879bdb2f5b3ee0ba8d08cbbdcdfe870e695ce3784385/execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc", size = 40612 },
]
+[[package]]
+name = "fastapi"
+version = "0.115.7"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pydantic" },
+ { name = "starlette" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a2/f5/3f921e59f189e513adb9aef826e2841672d50a399fead4e69afdeb808ff4/fastapi-0.115.7.tar.gz", hash = "sha256:0f106da6c01d88a6786b3248fb4d7a940d071f6f488488898ad5d354b25ed015", size = 293177 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e6/7f/bbd4dcf0faf61bc68a01939256e2ed02d681e9334c1a3cef24d5f77aba9f/fastapi-0.115.7-py3-none-any.whl", hash = "sha256:eb6a8c8bf7f26009e8147111ff15b5177a0e19bb4a45bc3486ab14804539d21e", size = 94777 },
+]
+
+[package.optional-dependencies]
+standard = [
+ { name = "email-validator" },
+ { name = "fastapi-cli", extra = ["standard"] },
+ { name = "httpx" },
+ { name = "jinja2" },
+ { name = "python-multipart" },
+ { name = "uvicorn", extra = ["standard"] },
+]
+
+[[package]]
+name = "fastapi-cli"
+version = "0.0.7"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "rich-toolkit" },
+ { name = "typer" },
+ { name = "uvicorn", extra = ["standard"] },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fe/73/82a5831fbbf8ed75905bacf5b2d9d3dfd6f04d6968b29fe6f72a5ae9ceb1/fastapi_cli-0.0.7.tar.gz", hash = "sha256:02b3b65956f526412515907a0793c9094abd4bfb5457b389f645b0ea6ba3605e", size = 16753 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a1/e6/5daefc851b514ce2287d8f5d358ae4341089185f78f3217a69d0ce3a390c/fastapi_cli-0.0.7-py3-none-any.whl", hash = "sha256:d549368ff584b2804336c61f192d86ddea080c11255f375959627911944804f4", size = 10705 },
+]
+
+[package.optional-dependencies]
+standard = [
+ { name = "uvicorn", extra = ["standard"] },
+]
+
[[package]]
name = "filelock"
version = "3.17.0"
@@ -949,6 +996,28 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 },
]
+[[package]]
+name = "httptools"
+version = "0.6.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a7/9a/ce5e1f7e131522e6d3426e8e7a490b3a01f39a6696602e1c4f33f9e94277/httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c", size = 240639 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bb/0e/d0b71465c66b9185f90a091ab36389a7352985fe857e352801c39d6127c8/httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2", size = 200683 },
+ { url = "https://files.pythonhosted.org/packages/e2/b8/412a9bb28d0a8988de3296e01efa0bd62068b33856cdda47fe1b5e890954/httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44", size = 104337 },
+ { url = "https://files.pythonhosted.org/packages/9b/01/6fb20be3196ffdc8eeec4e653bc2a275eca7f36634c86302242c4fbb2760/httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1", size = 508796 },
+ { url = "https://files.pythonhosted.org/packages/f7/d8/b644c44acc1368938317d76ac991c9bba1166311880bcc0ac297cb9d6bd7/httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2", size = 510837 },
+ { url = "https://files.pythonhosted.org/packages/52/d8/254d16a31d543073a0e57f1c329ca7378d8924e7e292eda72d0064987486/httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81", size = 485289 },
+ { url = "https://files.pythonhosted.org/packages/5f/3c/4aee161b4b7a971660b8be71a92c24d6c64372c1ab3ae7f366b3680df20f/httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f", size = 489779 },
+ { url = "https://files.pythonhosted.org/packages/12/b7/5cae71a8868e555f3f67a50ee7f673ce36eac970f029c0c5e9d584352961/httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970", size = 88634 },
+ { url = "https://files.pythonhosted.org/packages/94/a3/9fe9ad23fd35f7de6b91eeb60848986058bd8b5a5c1e256f5860a160cc3e/httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660", size = 197214 },
+ { url = "https://files.pythonhosted.org/packages/ea/d9/82d5e68bab783b632023f2fa31db20bebb4e89dfc4d2293945fd68484ee4/httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083", size = 102431 },
+ { url = "https://files.pythonhosted.org/packages/96/c1/cb499655cbdbfb57b577734fde02f6fa0bbc3fe9fb4d87b742b512908dff/httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3", size = 473121 },
+ { url = "https://files.pythonhosted.org/packages/af/71/ee32fd358f8a3bb199b03261f10921716990808a675d8160b5383487a317/httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071", size = 473805 },
+ { url = "https://files.pythonhosted.org/packages/8a/0a/0d4df132bfca1507114198b766f1737d57580c9ad1cf93c1ff673e3387be/httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5", size = 448858 },
+ { url = "https://files.pythonhosted.org/packages/1e/6a/787004fdef2cabea27bad1073bf6a33f2437b4dbd3b6fb4a9d71172b1c7c/httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0", size = 452042 },
+ { url = "https://files.pythonhosted.org/packages/4d/dc/7decab5c404d1d2cdc1bb330b1bf70e83d6af0396fd4fc76fc60c0d522bf/httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8", size = 87682 },
+]
+
[[package]]
name = "httpx"
version = "0.28.1"
@@ -1992,6 +2061,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0f/d7/03e0453719ed89724664f781f0255949408118093dbf77a2aa2a1198b38e/python_Levenshtein-0.26.1-py3-none-any.whl", hash = "sha256:8ef5e529dd640fb00f05ee62d998d2ee862f19566b641ace775d5ae16167b2ef", size = 9426 },
]
+[[package]]
+name = "python-multipart"
+version = "0.0.20"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546 },
+]
+
[[package]]
name = "python-slugify"
version = "8.0.4"
@@ -2170,6 +2248,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/aa/0b/e2de98c538c0ee9336211d260f88b7e69affab44969750aaca0b48a697c8/rich_click-1.8.5-py3-none-any.whl", hash = "sha256:0fab7bb5b66c15da17c210b4104277cd45f3653a7322e0098820a169880baee0", size = 35081 },
]
+[[package]]
+name = "rich-toolkit"
+version = "0.13.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "rich" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5b/8a/71cfbf6bf6257ea785d1f030c22468f763eea1b3e5417620f2ba9abd6dca/rich_toolkit-0.13.2.tar.gz", hash = "sha256:fea92557530de7c28f121cbed572ad93d9e0ddc60c3ca643f1b831f2f56b95d3", size = 72288 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/1b/1c2f43af46456050b27810a7a013af8a7e12bc545a0cdc00eb0df55eb769/rich_toolkit-0.13.2-py3-none-any.whl", hash = "sha256:f3f6c583e5283298a2f7dbd3c65aca18b7f818ad96174113ab5bec0b0e35ed61", size = 13566 },
+]
+
[[package]]
name = "ruff"
version = "0.9.3"
@@ -2338,6 +2430,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/49/58/97655efdfeb5b4eeab85b1fc5d3fa1023661246c2ab2a26ea8e47402d4f2/sseclient_py-1.8.0-py2.py3-none-any.whl", hash = "sha256:4ecca6dc0b9f963f8384e9d7fd529bf93dd7d708144c4fb5da0e0a1a926fee83", size = 8828 },
]
+[[package]]
+name = "starlette"
+version = "0.45.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/90/4f/e1c9f4ec3dae67a94c9285ed275355d5f7cf0f3a5c34538c8ae5412af550/starlette-0.45.2.tar.gz", hash = "sha256:bba1831d15ae5212b22feab2f218bab6ed3cd0fc2dc1d4442443bb1ee52260e0", size = 2574026 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/aa/ab/fe4f57c83620b39dfc9e7687ebad59129ff05170b99422105019d9a65eec/starlette-0.45.2-py3-none-any.whl", hash = "sha256:4daec3356fb0cb1e723a5235e5beaf375d2259af27532958e2d79df549dad9da", size = 71505 },
+]
+
[[package]]
name = "sybil"
version = "9.0.0"
@@ -2643,6 +2747,50 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fd/fa/ef855062da16bd3b3fd8cc86fc266405fd8ac68e9976150d1f2926dbc8ce/uv-0.5.23-py3-none-win_amd64.whl", hash = "sha256:adc152448bc0bb1df3b9de5ff727a651abf0ec73b18365f40ef9ab7c719c12bb", size = 16799376 },
]
+[[package]]
+name = "uvicorn"
+version = "0.34.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 },
+]
+
+[package.optional-dependencies]
+standard = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "httptools" },
+ { name = "python-dotenv" },
+ { name = "pyyaml" },
+ { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" },
+ { name = "watchfiles" },
+ { name = "websockets" },
+]
+
+[[package]]
+name = "uvloop"
+version = "0.21.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", size = 2492741 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8c/4c/03f93178830dc7ce8b4cdee1d36770d2f5ebb6f3d37d354e061eefc73545/uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c", size = 1471284 },
+ { url = "https://files.pythonhosted.org/packages/43/3e/92c03f4d05e50f09251bd8b2b2b584a2a7f8fe600008bcc4523337abe676/uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2", size = 821349 },
+ { url = "https://files.pythonhosted.org/packages/a6/ef/a02ec5da49909dbbfb1fd205a9a1ac4e88ea92dcae885e7c961847cd51e2/uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d", size = 4580089 },
+ { url = "https://files.pythonhosted.org/packages/06/a7/b4e6a19925c900be9f98bec0a75e6e8f79bb53bdeb891916609ab3958967/uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc", size = 4693770 },
+ { url = "https://files.pythonhosted.org/packages/ce/0c/f07435a18a4b94ce6bd0677d8319cd3de61f3a9eeb1e5f8ab4e8b5edfcb3/uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb", size = 4451321 },
+ { url = "https://files.pythonhosted.org/packages/8f/eb/f7032be105877bcf924709c97b1bf3b90255b4ec251f9340cef912559f28/uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f", size = 4659022 },
+ { url = "https://files.pythonhosted.org/packages/3f/8d/2cbef610ca21539f0f36e2b34da49302029e7c9f09acef0b1c3b5839412b/uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281", size = 1468123 },
+ { url = "https://files.pythonhosted.org/packages/93/0d/b0038d5a469f94ed8f2b2fce2434a18396d8fbfb5da85a0a9781ebbdec14/uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af", size = 819325 },
+ { url = "https://files.pythonhosted.org/packages/50/94/0a687f39e78c4c1e02e3272c6b2ccdb4e0085fda3b8352fecd0410ccf915/uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6", size = 4582806 },
+ { url = "https://files.pythonhosted.org/packages/d2/19/f5b78616566ea68edd42aacaf645adbf71fbd83fc52281fba555dc27e3f1/uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816", size = 4701068 },
+ { url = "https://files.pythonhosted.org/packages/47/57/66f061ee118f413cd22a656de622925097170b9380b30091b78ea0c6ea75/uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc", size = 4454428 },
+ { url = "https://files.pythonhosted.org/packages/63/9a/0962b05b308494e3202d3f794a6e85abe471fe3cafdbcf95c2e8c713aabd/uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", size = 4660018 },
+]
+
[[package]]
name = "virtualenv"
version = "20.29.1"
@@ -2693,6 +2841,37 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f0/e5/96b8e55271685ddbadc50ce8bc53aa2dff278fb7ac4c2e473df890def2dc/watchfiles-1.0.4-cp313-cp313-win_amd64.whl", hash = "sha256:d6097538b0ae5c1b88c3b55afa245a66793a8fec7ada6755322e465fb1a0e8cc", size = 285216 },
]
+[[package]]
+name = "websockets"
+version = "14.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/94/54/8359678c726243d19fae38ca14a334e740782336c9f19700858c4eb64a1e/websockets-14.2.tar.gz", hash = "sha256:5059ed9c54945efb321f097084b4c7e52c246f2c869815876a69d1efc4ad6eb5", size = 164394 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c1/81/04f7a397653dc8bec94ddc071f34833e8b99b13ef1a3804c149d59f92c18/websockets-14.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1f20522e624d7ffbdbe259c6b6a65d73c895045f76a93719aa10cd93b3de100c", size = 163096 },
+ { url = "https://files.pythonhosted.org/packages/ec/c5/de30e88557e4d70988ed4d2eabd73fd3e1e52456b9f3a4e9564d86353b6d/websockets-14.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:647b573f7d3ada919fd60e64d533409a79dcf1ea21daeb4542d1d996519ca967", size = 160758 },
+ { url = "https://files.pythonhosted.org/packages/e5/8c/d130d668781f2c77d106c007b6c6c1d9db68239107c41ba109f09e6c218a/websockets-14.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6af99a38e49f66be5a64b1e890208ad026cda49355661549c507152113049990", size = 160995 },
+ { url = "https://files.pythonhosted.org/packages/a6/bc/f6678a0ff17246df4f06765e22fc9d98d1b11a258cc50c5968b33d6742a1/websockets-14.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:091ab63dfc8cea748cc22c1db2814eadb77ccbf82829bac6b2fbe3401d548eda", size = 170815 },
+ { url = "https://files.pythonhosted.org/packages/d8/b2/8070cb970c2e4122a6ef38bc5b203415fd46460e025652e1ee3f2f43a9a3/websockets-14.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b374e8953ad477d17e4851cdc66d83fdc2db88d9e73abf755c94510ebddceb95", size = 169759 },
+ { url = "https://files.pythonhosted.org/packages/81/da/72f7caabd94652e6eb7e92ed2d3da818626e70b4f2b15a854ef60bf501ec/websockets-14.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a39d7eceeea35db85b85e1169011bb4321c32e673920ae9c1b6e0978590012a3", size = 170178 },
+ { url = "https://files.pythonhosted.org/packages/31/e0/812725b6deca8afd3a08a2e81b3c4c120c17f68c9b84522a520b816cda58/websockets-14.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0a6f3efd47ffd0d12080594f434faf1cd2549b31e54870b8470b28cc1d3817d9", size = 170453 },
+ { url = "https://files.pythonhosted.org/packages/66/d3/8275dbc231e5ba9bb0c4f93144394b4194402a7a0c8ffaca5307a58ab5e3/websockets-14.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:065ce275e7c4ffb42cb738dd6b20726ac26ac9ad0a2a48e33ca632351a737267", size = 169830 },
+ { url = "https://files.pythonhosted.org/packages/a3/ae/e7d1a56755ae15ad5a94e80dd490ad09e345365199600b2629b18ee37bc7/websockets-14.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e9d0e53530ba7b8b5e389c02282f9d2aa47581514bd6049d3a7cffe1385cf5fe", size = 169824 },
+ { url = "https://files.pythonhosted.org/packages/b6/32/88ccdd63cb261e77b882e706108d072e4f1c839ed723bf91a3e1f216bf60/websockets-14.2-cp312-cp312-win32.whl", hash = "sha256:20e6dd0984d7ca3037afcb4494e48c74ffb51e8013cac71cf607fffe11df7205", size = 163981 },
+ { url = "https://files.pythonhosted.org/packages/b3/7d/32cdb77990b3bdc34a306e0a0f73a1275221e9a66d869f6ff833c95b56ef/websockets-14.2-cp312-cp312-win_amd64.whl", hash = "sha256:44bba1a956c2c9d268bdcdf234d5e5ff4c9b6dc3e300545cbe99af59dda9dcce", size = 164421 },
+ { url = "https://files.pythonhosted.org/packages/82/94/4f9b55099a4603ac53c2912e1f043d6c49d23e94dd82a9ce1eb554a90215/websockets-14.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6f1372e511c7409a542291bce92d6c83320e02c9cf392223272287ce55bc224e", size = 163102 },
+ { url = "https://files.pythonhosted.org/packages/8e/b7/7484905215627909d9a79ae07070057afe477433fdacb59bf608ce86365a/websockets-14.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4da98b72009836179bb596a92297b1a61bb5a830c0e483a7d0766d45070a08ad", size = 160766 },
+ { url = "https://files.pythonhosted.org/packages/a3/a4/edb62efc84adb61883c7d2c6ad65181cb087c64252138e12d655989eec05/websockets-14.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8a86a269759026d2bde227652b87be79f8a734e582debf64c9d302faa1e9f03", size = 160998 },
+ { url = "https://files.pythonhosted.org/packages/f5/79/036d320dc894b96af14eac2529967a6fc8b74f03b83c487e7a0e9043d842/websockets-14.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:86cf1aaeca909bf6815ea714d5c5736c8d6dd3a13770e885aafe062ecbd04f1f", size = 170780 },
+ { url = "https://files.pythonhosted.org/packages/63/75/5737d21ee4dd7e4b9d487ee044af24a935e36a9ff1e1419d684feedcba71/websockets-14.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b0f6c3ba3b1240f602ebb3971d45b02cc12bd1845466dd783496b3b05783a5", size = 169717 },
+ { url = "https://files.pythonhosted.org/packages/2c/3c/bf9b2c396ed86a0b4a92ff4cdaee09753d3ee389be738e92b9bbd0330b64/websockets-14.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:669c3e101c246aa85bc8534e495952e2ca208bd87994650b90a23d745902db9a", size = 170155 },
+ { url = "https://files.pythonhosted.org/packages/75/2d/83a5aca7247a655b1da5eb0ee73413abd5c3a57fc8b92915805e6033359d/websockets-14.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eabdb28b972f3729348e632ab08f2a7b616c7e53d5414c12108c29972e655b20", size = 170495 },
+ { url = "https://files.pythonhosted.org/packages/79/dd/699238a92761e2f943885e091486378813ac8f43e3c84990bc394c2be93e/websockets-14.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2066dc4cbcc19f32c12a5a0e8cc1b7ac734e5b64ac0a325ff8353451c4b15ef2", size = 169880 },
+ { url = "https://files.pythonhosted.org/packages/c8/c9/67a8f08923cf55ce61aadda72089e3ed4353a95a3a4bc8bf42082810e580/websockets-14.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ab95d357cd471df61873dadf66dd05dd4709cae001dd6342edafc8dc6382f307", size = 169856 },
+ { url = "https://files.pythonhosted.org/packages/17/b1/1ffdb2680c64e9c3921d99db460546194c40d4acbef999a18c37aa4d58a3/websockets-14.2-cp313-cp313-win32.whl", hash = "sha256:a9e72fb63e5f3feacdcf5b4ff53199ec8c18d66e325c34ee4c551ca748623bbc", size = 163974 },
+ { url = "https://files.pythonhosted.org/packages/14/13/8b7fc4cb551b9cfd9890f0fd66e53c18a06240319915533b033a56a3d520/websockets-14.2-cp313-cp313-win_amd64.whl", hash = "sha256:b439ea828c4ba99bb3176dc8d9b933392a2413c0f6b149fdcba48393f573377f", size = 164420 },
+ { url = "https://files.pythonhosted.org/packages/7b/c8/d529f8a32ce40d98309f4470780631e971a5a842b60aec864833b3615786/websockets-14.2-py3-none-any.whl", hash = "sha256:7a6ceec4ea84469f15cf15807a747e9efe57e369c384fa86e022b3bea679b79b", size = 157416 },
+]
+
[[package]]
name = "win32-setctime"
version = "1.2.0"