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}) => ( + +
View Source on Github
+
+) 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}) => ( +
+

Returns

+
+
+
+
+ {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"