From b2a354c9e541e63439e7e46f99728573f8dd30c4 Mon Sep 17 00:00:00 2001 From: Gwyn Uttmark Date: Wed, 23 Oct 2024 17:21:17 -0600 Subject: [PATCH] Add new docs, final updates --- .gitignore | 2 + docs/.buildinfo | 2 +- .../brassy/actions/build_release_notes.html | 353 +++++ docs/_modules/brassy/actions/init.html | 139 ++ docs/_modules/brassy/actions/prune_yaml.html | 238 ++++ docs/_modules/brassy/brassy.html | 131 ++ .../templates/release_yaml_template.html | 237 ++++ .../brassy/templates/settings_template.html | 208 +++ docs/_modules/brassy/utils/CLI.html | 437 +++++++ docs/_modules/brassy/utils/file_handler.html | 291 +++++ docs/_modules/brassy/utils/git_handler.html | 190 +++ docs/_modules/brassy/utils/messages.html | 219 ++++ .../brassy/utils/settings_manager.html | 393 ++++++ docs/_modules/index.html | 110 ++ docs/_sources/api.rst.txt | 105 +- docs/_sources/index.rst.txt | 4 +- docs/_sources/releases/0.0.3.rst.txt | 238 ++++ docs/_sources/releases/index.rst.txt | 8 + .../_sources/using-brassy.rst.txt | 52 +- docs/_static/alabaster.css | 115 +- docs/_static/basic.css | 2 +- docs/_static/doctools.js | 2 +- docs/_static/documentation_options.js | 2 +- docs/_static/github-banner.svg | 5 + docs/_static/language_data.js | 4 +- docs/_static/searchtools.js | 170 ++- docs/api.html | 1161 ++++++++++++++++- docs/genindex.html | 516 +++++++- docs/index.html | 55 +- docs/objects.inv | Bin 344 -> 1452 bytes docs/py-modindex.html | 174 +++ docs/releases/0.0.2.html | 43 +- docs/releases/0.0.3.html | 326 +++++ docs/releases/index.html | 51 +- docs/search.html | 27 +- docs/searchindex.js | 2 +- ...getting-started.html => using-brassy.html} | 266 ++-- sphinx/Makefile | 2 +- sphinx/source/api.rst | 105 +- .../source/examples/basic-usage/pruned.yaml | 8 + .../basic-usage/to-prune.yaml} | 12 +- sphinx/source/index.rst | 4 +- sphinx/source/releases/0.0.3.rst | 238 ++++ .../1-brassy-fails-if-run-on-blank-file.yaml | 0 ...internal-repos-with-inaccessible-urls.yaml | 0 .../archive-0.0.3/39-release-v003.yaml | 78 ++ .../archive-0.0.3/6-add-date-field.yaml | 15 + .../9-add-default-template-write-path.yaml | 7 + .../releases/archive-0.0.3/add-init.yaml | 6 + .../fix-docs-yaml.yaml | 0 .../fix-no-build-no-title.yaml | 4 +- .../fix-template-arg-error.yaml | 2 +- .../releases/archive-0.0.3/int-tests.yaml | 37 + .../put-title-above-desc-in-temp.yaml | 0 sphinx/source/releases/index.rst | 8 + sphinx/source/releases/latest.rst | 150 --- .../releases/latest/6-add-date-field.yaml | 114 -- .../9-add-default-template-write-path.yaml | 108 -- sphinx/source/releases/latest/int-tests.yaml | 139 -- .../source/using-brassy.rst | 152 ++- src/brassy/templates/release_yaml_template.py | 14 +- 61 files changed, 6487 insertions(+), 994 deletions(-) create mode 100644 docs/_modules/brassy/actions/build_release_notes.html create mode 100644 docs/_modules/brassy/actions/init.html create mode 100644 docs/_modules/brassy/actions/prune_yaml.html create mode 100644 docs/_modules/brassy/brassy.html create mode 100644 docs/_modules/brassy/templates/release_yaml_template.html create mode 100644 docs/_modules/brassy/templates/settings_template.html create mode 100644 docs/_modules/brassy/utils/CLI.html create mode 100644 docs/_modules/brassy/utils/file_handler.html create mode 100644 docs/_modules/brassy/utils/git_handler.html create mode 100644 docs/_modules/brassy/utils/messages.html create mode 100644 docs/_modules/brassy/utils/settings_manager.html create mode 100644 docs/_modules/index.html create mode 100644 docs/_sources/releases/0.0.3.rst.txt rename sphinx/source/getting-started.rst => docs/_sources/using-brassy.rst.txt (79%) create mode 100644 docs/_static/github-banner.svg create mode 100644 docs/py-modindex.html create mode 100644 docs/releases/0.0.3.html rename docs/{getting-started.html => using-brassy.html} (51%) create mode 100644 sphinx/source/examples/basic-usage/pruned.yaml rename sphinx/source/{releases/latest/add-init.yaml => examples/basic-usage/to-prune.yaml} (51%) create mode 100644 sphinx/source/releases/0.0.3.rst rename sphinx/source/releases/{latest => archive-0.0.3}/1-brassy-fails-if-run-on-blank-file.yaml (100%) rename sphinx/source/releases/{latest => archive-0.0.3}/30-accept-strings-in-related-issue-for-compatibility-with-internal-repos-with-inaccessible-urls.yaml (100%) create mode 100644 sphinx/source/releases/archive-0.0.3/39-release-v003.yaml create mode 100644 sphinx/source/releases/archive-0.0.3/6-add-date-field.yaml create mode 100644 sphinx/source/releases/archive-0.0.3/9-add-default-template-write-path.yaml create mode 100644 sphinx/source/releases/archive-0.0.3/add-init.yaml rename sphinx/source/releases/{latest => archive-0.0.3}/fix-docs-yaml.yaml (100%) rename sphinx/source/releases/{latest => archive-0.0.3}/fix-no-build-no-title.yaml (90%) rename sphinx/source/releases/{latest => archive-0.0.3}/fix-template-arg-error.yaml (64%) create mode 100644 sphinx/source/releases/archive-0.0.3/int-tests.yaml rename sphinx/source/releases/{latest => archive-0.0.3}/put-title-above-desc-in-temp.yaml (100%) delete mode 100644 sphinx/source/releases/latest.rst delete mode 100644 sphinx/source/releases/latest/6-add-date-field.yaml delete mode 100644 sphinx/source/releases/latest/9-add-default-template-write-path.yaml delete mode 100644 sphinx/source/releases/latest/int-tests.yaml rename docs/_sources/getting-started.rst.txt => sphinx/source/using-brassy.rst (50%) diff --git a/.gitignore b/.gitignore index 633568b..d45f125 100644 --- a/.gitignore +++ b/.gitignore @@ -163,3 +163,5 @@ cython_debug/ .vscode/settings.json .brassy + +.buildinfo diff --git a/docs/.buildinfo b/docs/.buildinfo index e6945e1..9bdfed4 100644 --- a/docs/.buildinfo +++ b/docs/.buildinfo @@ -1,4 +1,4 @@ # Sphinx build info version 1 # This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. -config: 012e053f98c36652452d36c572f46ce6 +config: a7850f5afc0219d7d5902826f4f7f68c tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/docs/_modules/brassy/actions/build_release_notes.html b/docs/_modules/brassy/actions/build_release_notes.html new file mode 100644 index 0000000..5946ec8 --- /dev/null +++ b/docs/_modules/brassy/actions/build_release_notes.html @@ -0,0 +1,353 @@ + + + + + + + brassy.actions.build_release_notes — brassy 0.0.3 documentation + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for brassy.actions.build_release_notes

+from datetime import datetime
+
+import brassy
+from brassy.brassy import Settings
+from brassy.utils.messages import RichConsole as console
+
+
+
+
+
+
+
+[docs] +def find_duplicate_titles(data): + """ + Check if there are any duplicate titles in dictionaries of lists of dictionaries. + + Args: + data (dict): A dictionary containing lists of dictionaries with items + indexed by "title". + + Returns: + bool: True if there are duplicate "title" values, False otherwise. + """ + titles = [entry["title"] for category in data for entry in data[category]] + return not len(set(titles)) == len(titles)
+ + + +
+[docs] +def format_files_changed_entry(detailed, entry): + files_changed = "::\n\n" + for change_type in entry["files"]: + files_changed += "".join( + [ + f" {change_type}: {file}\n" + for file in filter(lambda x: not x == "", entry["files"][change_type]) + ] + ) + return files_changed
+ + + +
+[docs] +def generate_file_change_section_list_of_strings( + entry, line, category, title, description +): + lines = [] + for change_type in entry["files"]: + if "{file}" in line: + for file in filter(lambda x: not x == "", entry["files"][change_type]): + lines.append( + line.format( + change_type=category.capitalize(), + title=title, + description=description, + file_change=change_type, + file=file, + ) + ) + else: + lines.append( + line.format( + change_type=category.capitalize(), + title=title, + description=description, + file_change=change_type, + ) + ) + return lines
+ + + +
+[docs] +def generate_section_string( + section_lines, changelog_entries, release_date, version, footer, header +): + lines = [] + entry_keywords = [ + "{" + k + "}" + for k in ["title", "description", "file_change", "file", "change_type"] + ] + if any([keyword in line for keyword in entry_keywords for line in section_lines]): + for category, entries in changelog_entries.items(): + for entry in entries: + if not entry["title"] and not entry["description"]: + continue + if entry["title"]: + title = entry["title"] + title = title.capitalize() + else: + title = Settings.default_title + # print(f"Warning: no title for entry {entry}") + if entry["description"]: + description = entry["description"] + else: + # print("Warning: no description") + description = Settings.default_description + for line in section_lines: + if "{file_change}" in line: + lines.extend( + generate_file_change_section_list_of_strings( + entry, line, category, title, description + ) + ) + else: + lines.append( + line.format( + change_type=category.capitalize(), + title=title, + description=description, + ), + ) + else: + for line in section_lines: + lines.append(line) + for i, line in enumerate(lines): + lines[i] = line.format( + prefix_file=header, + suffix_file=footer, + release_version=version, + release_date=release_date, + ) + return "\n".join(lines)
+ + + +
+[docs] +def format_release_notes(data, version, release_date=None, header=None, footer=None): + """ + Format the parsed YAML data into release notes in .rst format. + + Parameters + ---------- + data : dict + Parsed content of YAML files. + version : str, optional + Version number of the release, by default '1.1'. + release_date : str, optional + Release date, by default None, which uses today's date. + + Returns + ------- + str + Formatted release notes in .rst format. + """ + if release_date is None: + release_date = datetime.now().strftime("%Y-%m-%d") + + header = header or "" + footer = footer or "" + + release_template = Settings.templates.release_template + formatted_string = "" + for section in release_template: + for section_name, lines in section.items(): + formatted_string = ( + formatted_string + + generate_section_string( + lines, data, release_date, version, footer, header + ) + + "\n" + ) + if Settings.default_title in formatted_string: + console.print( + "Warning: Build completed, but at least one title is missing.", + style="yellow", + ) + if Settings.default_description in formatted_string: + console.print( + "Warning: Build completed, but at least one description is missing.", + style="yellow", + ) + return formatted_string.strip()
+ + + +
+[docs] +def build_release_notes( + input_files_or_folders, + console, + rich_open, + version=None, + release_date=None, + header_file=None, + footer_file=None, + working_dir=".", +): + """ + Build release notes from YAML data. + + Parameters + ---------- + data : dict + Parsed content of YAML files. + version : str, optional + Version number of the release, by default '1.1'. + release_date : str, optional + Release date, by default None, which uses today's date. + header_file : str, optional + A header file to prepend to the release notes. + footer_file : str, optional + A footer file to suffix to the release notes. + + Returns + ------- + str + Formatted release notes in .rst format. + """ + yaml_files = brassy.utils.CLI.get_file_list_from_cli_input( + input_files_or_folders, console, working_dir=working_dir + ) + try: + data = brassy.utils.file_handler.read_yaml_files(yaml_files, rich_open) + except (ValueError, TypeError) as e: + console.print(f"[red]{e}") + exit(1) + header, footer = get_header_footer( + rich_open, header_file=header_file, footer_file=footer_file + ) + content = format_release_notes( + data, version=version, release_date=release_date, header=header, footer=footer + ) + return content
+ +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_modules/brassy/actions/init.html b/docs/_modules/brassy/actions/init.html new file mode 100644 index 0000000..bfd1999 --- /dev/null +++ b/docs/_modules/brassy/actions/init.html @@ -0,0 +1,139 @@ + + + + + + + brassy.actions.init — brassy 0.0.3 documentation + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for brassy.actions.init

+import brassy
+from brassy.brassy import Settings
+
+
+
+[docs] +def init(): + """ + Initialize configuration files for the application. + + This function creates site and user configuration files, and optionally + a project configuration file based on user input. + + Returns + ------- + None + + Examples + -------- + >>> init() + Do you want to create a project config file? [y/N]: y + """ + if Settings.enable_experimental_features: + conf_files = [ + brassy.utils.settings_manager.get_site_config_file_path("brassy"), + brassy.utils.settings_manager.get_user_config_file_path("brassy"), + ] + else: + conf_files = [] + for conf_file in conf_files: + brassy.utils.settings_manager.create_config_file(conf_file) + if brassy.utils.messages.boolean_prompt( + "Do you want to create a project config file?" + ): + brassy.utils.settings_manager.create_config_file( + brassy.utils.settings_manager.get_project_config_file_path("brassy") + )
+ +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_modules/brassy/actions/prune_yaml.html b/docs/_modules/brassy/actions/prune_yaml.html new file mode 100644 index 0000000..2cd000c --- /dev/null +++ b/docs/_modules/brassy/actions/prune_yaml.html @@ -0,0 +1,238 @@ + + + + + + + brassy.actions.prune_yaml — brassy 0.0.3 documentation + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for brassy.actions.prune_yaml

+import yaml
+
+import brassy
+import brassy.utils
+
+import brassy.utils.file_handler
+
+
+
+[docs] +def prune_empty(data, prune_lists=True, key=""): + """ + Recursively remove empty values from a nested dictionary or list. + + Parameters + ---------- + data : dict or list + The data structure to prune. + prune_lists : bool, optional + Indicates whether to prune empty lists. Currently unused. + key : str, optional + The key associated with the current data item, used for special cases. + + Returns + ------- + dict or list or None + The pruned data structure, or None if it is empty. + + Notes + ----- + The function considers the following values as empty: `None`, empty strings, + empty dictionaries, and empty lists. If a value is `0` and the key is + `"number"`, it is also considered empty to address the related issues + field which was previously set to 0 instead of null. + + Examples + -------- + >>> data = {'a': None, 'b': '', 'c': {'d': [], 'e': 'value'}} + >>> prune_empty(data) + {'c': {'e': 'value'}} + """ + nulls = (None, "", {}, []) + if isinstance(data, dict): + pruned = {k: prune_empty(v, key=k) for k, v in data.items()} + pruned = {k: v for k, v in pruned.items() if v not in nulls} + return pruned if pruned else None + elif isinstance(data, list): + pruned = [prune_empty(item) for item in data] + pruned = [item for item in pruned if item not in nulls] + return pruned if pruned else None + elif data == 0 and key == "number": + return None + else: + return data
+ + + +
+[docs] +def prune_yaml_file(yaml_file_path, console): + """ + Prune empty values from a YAML file and overwrite it with the pruned content. + + Parameters + ---------- + yaml_file_path : str + The file path to the YAML file to be pruned. + console : Console + An object used for printing messages to the console. + + Returns + ------- + None + + Notes + ----- + This function reads the YAML file, prunes empty values using `prune_empty`, + and writes the pruned content back to the same file. + + Examples + -------- + >>> prune_yaml_file('config.yaml', console) + Pruned config.yaml + """ + with open(yaml_file_path, "r+") as file: + content = yaml.safe_load(file) + file.seek(0) + file.write( + yaml.dump( + prune_empty(content, prune_lists=False), + sort_keys=False, + default_flow_style=False, + ) + ) + file.truncate() + console.print(f"Pruned {yaml_file_path}")
+ + + +
+[docs] +def direct_pruning_of_files(input_files_or_folders, console, working_dir): + """ + Prune empty values from YAML files specified by input paths. + + Parameters + ---------- + input_files_or_folders : list of str + A list of file paths or directories containing YAML files to prune. + console : Console + An object used for printing messages to the console. + working_dir : str + The working directory path. + + Returns + ------- + None + + Notes + ----- + This function collects YAML files from the specified input paths and + prunes each file using `prune_yaml_file`. + + Examples + -------- + >>> direct_pruning_of_files(['configs/'], console, '/home/user') + Pruned configs/config1.yaml + Pruned configs/config2.yaml + """ + import brassy.utils.CLI # here to prevent circular import + + yaml_files = brassy.utils.CLI.get_file_list_from_cli_input( + input_files_or_folders, console, working_dir=working_dir + ) + for yaml_file in yaml_files: + prune_yaml_file(yaml_file, console)
+ +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_modules/brassy/brassy.html b/docs/_modules/brassy/brassy.html new file mode 100644 index 0000000..e26591a --- /dev/null +++ b/docs/_modules/brassy/brassy.html @@ -0,0 +1,131 @@ + + + + + + + brassy.brassy — brassy 0.0.3 documentation + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for brassy.brassy

+"""
+This module provides functionality to generate release notes from YAML files.
+It reads YAML files, parses their content, and formats the parsed data into release notes in .rst format.
+The release notes can be written to an output file.
+"""
+
+import argparse
+import os
+import rich.progress
+import yaml
+from datetime import datetime
+
+import pygit2
+
+import brassy.utils.settings_manager as settings_manager
+
+Settings = settings_manager.get_settings("brassy")
+
+
+
+[docs] +def run_from_CLI(): + import brassy.utils.CLI as CLI + + CLI.run_from_CLI()
+ + + +if __name__ == "__main__": + run_from_CLI() +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_modules/brassy/templates/release_yaml_template.html b/docs/_modules/brassy/templates/release_yaml_template.html new file mode 100644 index 0000000..6dbe410 --- /dev/null +++ b/docs/_modules/brassy/templates/release_yaml_template.html @@ -0,0 +1,237 @@ + + + + + + + brassy.templates.release_yaml_template — brassy 0.0.3 documentation + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for brassy.templates.release_yaml_template

+import pathlib
+from typing import List, Optional, Dict, Union
+from datetime import date as Date
+
+import dateparser
+
+from pydantic import (
+    BaseModel,
+    HttpUrl,
+    ValidationError,
+    model_validator,
+    RootModel,
+    Field,
+    field_validator,
+    validator,
+)
+
+
+
+[docs] +class Files(BaseModel): + deleted: List[str] = [] + moved: List[str] = [] + added: List[str] = [] + modified: List[str] = [] + +
+[docs] + @model_validator(mode="after") + def check_at_least_one_field(self): + if not any( + getattr(self, field) for field in ["deleted", "moved", "added", "modified"] + ): + raise ValueError( + "At least one of deleted, moved, added, or modified must have a value" + ) + return self
+
+ + + +
+[docs] +class RelatedInternalIssue(BaseModel): + string: Optional[str] = Field(pattern=r"[A-Za-z]+#\d+ - .+", default=None)
+ + + +
+[docs] +class RelatedIssue(BaseModel): + number: Optional[int] = None + repo_url: Optional[HttpUrl] = None + +
+[docs] + @field_validator("repo_url", mode="before") + def blank_string(value, field): + if value == "": + return None + return value
+
+ + + +
+[docs] +class DateRange(BaseModel): + start: Optional[Date] + finish: Optional[Date] + +
+[docs] + @validator("start", "finish", pre=True, always=True) + def parse_date(cls, value): + if value is None or isinstance(value, Date): + return value + if isinstance(value, str): + value = value.strip() + if not value or value.lower() in ["never", "null"]: + return None + try: + parsed = dateparser.parse(value) + if parsed is None: + raise ValueError(f"Unable to parse date string: {value}") + return parsed.date() + except Exception as e: + raise ValueError(f"Invalid date format: {value}") from e + raise ValueError(f"Invalid type for date field: {value}")
+
+ + + +
+[docs] +class ChangeItem(BaseModel): + title: Optional[str] = Field(min_length=1, strip_whitespace=True) + description: Optional[str] = Field(min_length=1, strip_whitespace=True) + files: Files + related_issue: Optional[Union[RelatedIssue, RelatedInternalIssue]] = Field( + alias="related-issue", exclude_unset=True, default=None + ) + date: Optional[DateRange] = None + +
+[docs] + @model_validator(mode="before") + def empty_str_to_none(values): + for value in ["title", "description"]: + if values[value] == "": + values[value] = None + # if not values["title"] and not values["description"]: + # if not values == ReleaseNote(): + # raise ValueError("Missing title and description") + return values
+
+ + + +
+[docs] +class ReleaseNote(RootModel[Dict[str, List[ChangeItem]]]): + """ + ReleaseNote is a root model containing a dictionary that maps category names to lists of ChangeItems. + """ + + pass
+ + + +from brassy.utils.settings_manager import get_settings + +Settings = get_settings("brassy") + +# List of categories stored in a variable +categories = Settings.change_categories +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_modules/brassy/templates/settings_template.html b/docs/_modules/brassy/templates/settings_template.html new file mode 100644 index 0000000..bfb2a93 --- /dev/null +++ b/docs/_modules/brassy/templates/settings_template.html @@ -0,0 +1,208 @@ + + + + + + + brassy.templates.settings_template — brassy 0.0.3 documentation + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for brassy.templates.settings_template

+import pathlib
+from typing import List, Optional, Dict
+
+from pydantic import BaseModel, Field
+
+
+
+[docs] +class ReleaseTemplate(BaseModel): + release_template: Optional[List[Dict[str, List[str]]]] = Field( + default=None, alias="release-template" + ) + +
+[docs] + class Config: + populate_by_name = True
+
+ + + +# Example YAML representation (as a string for reference) +""" +release-template: + - header: + - {prefix_file} + - title: + - "Version {release_version} ({release_date})" + - "**************************" + - summary: + - " * *{change_type}*: {title}" + - entry: + - "{change_type}" + - "===========" + - "" + - "{title}" + - "-------------------------" + - "" + - "{description}" + - "" + - "::" + - "" + - " {file_change}: {file}" + - footer: + - {suffix_file} +""" + +# Corrected instantiation of DefaultTemplate +DefaultTemplate = ReleaseTemplate( + **{ + "release-template": [ + {"header": ["{prefix_file}", ""]}, + { + "title": [ + "", + "Version {release_version} ({release_date})", + "**************************", + "", + ] + }, + {"summary": [" * *{change_type}*: {title}"]}, + { + "entry": [ + "", + "{change_type}", + "===========", + "", + "{title}", + "-------------------------", + "", + "{description}", + "", + "::", + "", + " {file_change}: {file}", + ] + }, + {"footer": ["", "{suffix_file}"]}, + ] + } +) + + +
+[docs] +class SettingsTemplate(BaseModel): + use_color: bool = True + default_yaml_path: Optional[pathlib.Path] = None + change_categories: List[str] = [ + "bug fix", + "enhancement", + "deprecation", + "removal", + "performance", + "documentation", + "continuous integration", + ] + default_title: str = "NO TITLE" + default_description: str = "NO DESCRIPTION" + fail_on_empty_dir: bool = True + description_populates_with_pipe: bool = False + + valid_fields: List[str] = ["title", "description", "files", "related-issue"] + valid_changes: List[str] = ["deleted", "moved", "added", "modified"] + enable_experimental_features: bool = False + templates: Optional[ReleaseTemplate] = DefaultTemplate
+ +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_modules/brassy/utils/CLI.html b/docs/_modules/brassy/utils/CLI.html new file mode 100644 index 0000000..e972691 --- /dev/null +++ b/docs/_modules/brassy/utils/CLI.html @@ -0,0 +1,437 @@ + + + + + + + brassy.utils.CLI — brassy 0.0.3 documentation + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for brassy.utils.CLI

+# This module is for the CLI only portions of brassy.
+# Brassy can be run without this file, and importing it should
+
+import argparse
+import os
+
+from rich_argparse import RichHelpFormatter
+
+import brassy.actions.build_release_notes
+import brassy.actions.init
+import brassy.actions.prune_yaml
+import brassy.utils.file_handler
+import brassy.utils.git_handler
+from brassy.utils.settings_manager import get_settings
+import brassy.utils.messages as messages
+
+Settings = get_settings("brassy")
+
+
+
+[docs] +def get_parser(): + """ + Returns an ArgumentParser object with predefined arguments for generating release notes from YAML files. + + Returns: + argparse.ArgumentParser: The ArgumentParser object with predefined arguments. + """ + parser = argparse.ArgumentParser( + description="Generate release notes from YAML files." + + " Entries are sorted by order in yaml files, " + + "and by order of yaml files provided via the command line.", + formatter_class=RichHelpFormatter, + ) + parser.add_argument( + "-t", + "--write-yaml-template", + type=str, + help="Write template YAML to provided file." + + " If folder provided, place template in folder with current " + + "git branch name as file name.", + nargs="?", + default=argparse.SUPPRESS, + ) + parser.add_argument( + "-c", + "--get-changed-files", + type=str, + help="Print git tracked file changes against main." + + " If directory provided, use that directories checked-out branch.", + nargs="?", + default=argparse.SUPPRESS, + ) + parser.add_argument( + "input_files_or_folders", + type=str, + nargs="*", + help="The folder(s) containing YAML files and/or YAML files. " + + "Folders will be searched recursively.", + ) + parser.add_argument( + "-r", + "--release-version", + type=str, + default="[UNKNOWN]", + help="Version number of the release. Default is '[UNKNOWN]'.", + required=False, + dest="version", + ) + parser.add_argument( + "-d", + "--release-date", + type=str, + help="Date of the release. Default is current system time.", + required=False, + ) + parser.add_argument( + "-nc", + "--no-color", + action="store_true", + default=Settings.use_color, + help="Disable text formatting for CLI output.", + ) + parser.add_argument( + "-p", + "--prefix-file", + type=str, + help="A header file to prepend to the release notes.", + ) + parser.add_argument( + "-s", + "--suffix-file", + type=str, + help="A footer file to suffix to the release notes.", + ) + parser.add_argument( + "-o", "--output-file", type=str, help="The output file for release notes." + ) + if Settings.default_yaml_path and Settings.enable_experimental_features: + yaml_path = os.path.join(".", Settings.default_yaml_path) + else: + yaml_path = "." + parser.add_argument( + "-yd", + "--yaml-dir", + type=str, + help="Directory to write yaml files to", + default=yaml_path, + ) + parser.add_argument( + "--output-to-console", + action="store_true", + default=False, + help="Write generated release notes to console.", + ) + parser.add_argument( + "-nr", "--no-rich", action="store_true", help="Disable rich text output" + ) + parser.add_argument("-q", "--quiet", action="store_true", help="Only output errors") + parser.add_argument( + "-pr", + "--prune", + action="store_true", + help="Prune provided yaml file(s) of empty sections", + ) + parser.add_argument( + "--init", + action="store_true", + help="Initialize brassy and generate config files", + default=False, + ) + parser.add_argument( + "--version", + action="store_true", + dest="print_version", + help="Print program version and exit", + default=False, + ) + return parser
+ + + +
+[docs] +def parse_arguments(): + """ + Parse command line arguments for input folder and output file. + + Returns + ------- + argparse.Namespace + Parsed arguments containing input_folder and output_file. + """ + parser = get_parser() + return parser.parse_args(), parser
+ + + + + + + +
+[docs] +def exit_on_invalid_arguments(args, parser, console): + """ + Validate the argparse arguments. + + This function validates the provided argparse arguments to ensure + that the required input files/folders and output file are provided. + If arguments are invalid, it prints an error message and exits the program. + + Parameters + ---------- + args : argparse.Namespace + Parsed arguments. + + parser : argparse.ArgumentParser + The ArgumentParser object used to parse the command-line arguments. + """ + if ( + bool(args.input_files_or_folders) + or "get_changed_files" in args + or args.init + or args.version + ): + return + + if "write_yaml_template" in args: + return + + console.print("[bold red]Invalid arguments.\n") + parser.print_help() + exit(1)
+ + + +
+[docs] +def get_yaml_files_from_input(input_files_or_folders): + """ + Get a list of YAML files from the given input files or folders. + + Parameters + ---------- + input_files_or_folders : list + List of paths to input files or folders. + + Returns + ------- + list + List of paths to YAML files. + + Raises + ------ + ValueError + If a file is not a YAML file or if no YAML files are found in a directory. + """ + yaml_files = [] + for path in input_files_or_folders: + if os.path.isfile(path): + if not path.endswith(".yaml") or path.endswith(".yml"): + raise ValueError(f"File {path} is not a YAML file.") + yaml_files.append(path) + elif os.path.isdir(path): + dir_yaml_files = [] + for root, dirs, files in os.walk(path): + for file in files: + if file.endswith(".yaml") or file.endswith(".yml"): + dir_yaml_files.append(os.path.join(root, file)) + if len(dir_yaml_files) == 0: + raise FileExistsError(f"No YAML files found in directory {path}.") + yaml_files.extend(dir_yaml_files) + else: + raise FileNotFoundError(path) + return yaml_files
+ + + +
+[docs] +def get_file_list_from_cli_input(input_files_or_folders, console, working_dir="."): + try: + yaml_files = get_yaml_files_from_input( + [ + brassy.utils.file_handler.get_yaml_template_path( + path, working_dir=working_dir + ) + for path in input_files_or_folders + ] + ) + except FileExistsError as e: + if Settings.fail_on_empty_dir: + console.print(f"[red]Invalid file or directory: [bold]{e}[/]") + exit(1) + else: + console.print(f"[yellow]Invalid file or directory: [bold]{e}[/]") + console.print( + f"[yellow]Returning 0 because fail_on_empty_dir is [bold]False[/]" + ) + exit(0) + except FileNotFoundError as e: + console.print(f"[red]Invalid file or directory: [bold]{e}[/]") + exit(1) + except ValueError as e: + console.print(f"[red]{e}") + exit(1) + return yaml_files
+ + + +
+[docs] +def run_from_CLI(): + """ + Main function to generate release notes from YAML files and write to an output file. + """ + args, parser = parse_arguments() + + messages.setup_messages(format=not args.no_rich, quiet=args.quiet) + + console = messages.RichConsole + printer = messages.print + rich_open = messages.open + + exit_on_invalid_arguments(args, parser, console) + if args.print_version: + print_version_and_exit() + elif args.init: + brassy.actions.init.init() + exit(0) + elif args.prune: + brassy.actions.prune_yaml.direct_pruning_of_files( + args.input_files_or_folders, console, args.yaml_dir + ) + elif "write_yaml_template" in args: + brassy.utils.file_handler.create_blank_template_yaml_file( + args.write_yaml_template, + console, + working_dir=args.yaml_dir, + ) + elif "get_changed_files" in args: + brassy.utils.git_handler.print_out_git_changed_files( + printer, + repo_path=args.get_changed_files if args.get_changed_files else ".", + ) + elif args.input_files_or_folders: + content = brassy.actions.build_release_notes.build_release_notes( + args.input_files_or_folders, + console, + rich_open, + version=args.version, + release_date=args.release_date, + header_file=args.prefix_file, + footer_file=args.suffix_file, + working_dir=args.yaml_dir, + ) + if args.output_file: + brassy.utils.file_handler.write_output_file(args.output_file, content) + if not args.quiet: + console.print(f"[green]Wrote release notes to {args.output_file}") + else: + if not args.quiet: + console.print( + f"[green]Release notes built successfully. No output file provided." + ) + if args.output_to_console: + console.print(content) + + else: + parser.print_help()
+ +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_modules/brassy/utils/file_handler.html b/docs/_modules/brassy/utils/file_handler.html new file mode 100644 index 0000000..1872543 --- /dev/null +++ b/docs/_modules/brassy/utils/file_handler.html @@ -0,0 +1,291 @@ + + + + + + + brassy.utils.file_handler — brassy 0.0.3 documentation + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for brassy.utils.file_handler

+import os
+from pathlib import Path
+
+import yaml
+from pygit2 import GitError
+
+import brassy.utils.git_handler as git_handler
+from brassy.brassy import Settings
+
+
+
+[docs] +def get_yaml_template_path(file_path_arg, working_dir=os.getcwd()): + """ + Returns the path of the YAML template file based on the given file path argument. + + Args: + file_path_arg (str): The file path argument provided by the user. + + Returns: + str: The path of the YAML template file. + + """ + if file_path_arg is None: + filename = f"{git_handler.get_current_git_branch()}.yaml" + return os.path.join(working_dir, filename) + if "/" in file_path_arg or "\\" in file_path_arg or Path(file_path_arg).is_file(): + return file_path_arg + return os.path.join(working_dir, file_path_arg)
+ + + +
+[docs] +def create_blank_template_yaml_file(file_path_arg, console, working_dir="."): + """ + Creates a blank YAML template file with a predefined structure. + + This function generates a YAML file at the specified path with a default + template. It handles special characters required for YAML compatibility and writes + the file to disk. + + Parameters + ---------- + file_path_arg : str + The file path of the YAML template as passed via the CLI. + console : rich.console.Console + A Rich Console object used for displaying messages and errors to the user. + working_dir : str, optional + The working directory path. Defaults to the current directory ".". + + Raises + ------ + SystemExit + If a Git repo is not found in the current working directory and no file path + is provided, the program exits with an error message. + + Notes + ----- + This function performs a string replacement to insert a "|" due to an issue with + YAML's handling of pipe symbols. For more details, see: + https://github.com/yaml/pyyaml/pull/822 + """ + pipe_replace_string = "REPLACE_ME_WITH_PIPE" + from brassy.templates.release_yaml_template import ReleaseNote + + default_yaml = { + category: [ + { + "title": "", + "description": ( + pipe_replace_string + if Settings.description_populates_with_pipe + else "" + ), + "files": {change: [""] for change in Settings.valid_changes}, + "related-issue": {"number": None, "repo_url": ""}, + # in time, extract from the first and last commit + "date": {"start": None, "finish": None}, + } + ] + for category in Settings.change_categories + } + try: + yaml_template_path = get_yaml_template_path(file_path_arg, working_dir) + except GitError: + console.print( + "[bold red]Could not find a git repo. Please run in a " + + "git repo or pass a file path for the yaml template " + + "(eg '-t /path/to/file.yaml')." + ) + exit(1) + with open(yaml_template_path, "w") as file: + yaml_text = yaml.safe_dump( + default_yaml, sort_keys=False, default_flow_style=False + ) + if Settings.description_populates_with_pipe: + yaml_text = yaml_text.replace(pipe_replace_string, "|\n replace_me") + file.write(yaml_text)
+ + + +
+[docs] +def value_error_on_invalid_yaml(content, file_path): + """ + Check if the YAML content follows the correct schema. + + Parameters + ---------- + content : dict + Parsed content of the YAML file. + file_path : str + Path to the YAML file. + + Raises + ------ + ValueError + If the YAML content does not follow the correct schema. + """ + if content is None: + raise ValueError(f"No valid brassy-related YAML. Please populate {file_path}") + from brassy.templates.release_yaml_template import ReleaseNote + + ReleaseNote(**content)
+ + + +
+[docs] +def read_yaml_files(input_files, rich_open): + """ + Read and parse the given list of YAML files. + + Parameters + ---------- + input_files : list + List of paths to the YAML files. + + Returns + ------- + dict + Parsed content of all YAML files categorized by type of change. + + Examples + -------- + >>> read_yaml_files(["file1.yaml", "file2.yaml"]) + {'bug-fix': [ + {'title': 'fixed explosions', + 'description': 'This fixed the explosion mechanism'}, + {'title': 'fixed cats not being cute', + 'description': 'This made the cats WAY cuter'} + ] + } + """ + data = {} + for file_path in input_files: + with rich_open(file_path, "r", description=f"Reading {file_path}") as file: + content = yaml.safe_load(file) + value_error_on_invalid_yaml(content, file_path) + for category, entries in content.items(): + entries = [ + entry + for entry in entries + if not (entry["title"] == "" or entry["description"] == "") + ] + if category not in data and len(entries) > 0: + data[category] = [] + if len(entries) > 0: + data[category].extend(entries) + return data
+ + + +
+[docs] +def write_output_file(output_file, content): + """ + Write the formatted release notes to the output file. + + Parameters + ---------- + output_file : str + Path to the output .rst file. + content : str + Formatted release notes. + """ + with open(output_file, "w") as file: + file.write(content)
+ +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_modules/brassy/utils/git_handler.html b/docs/_modules/brassy/utils/git_handler.html new file mode 100644 index 0000000..bc68c76 --- /dev/null +++ b/docs/_modules/brassy/utils/git_handler.html @@ -0,0 +1,190 @@ + + + + + + + brassy.utils.git_handler — brassy 0.0.3 documentation + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for brassy.utils.git_handler

+import pygit2
+
+
+
+[docs] +def get_git_status(repo_path="."): + """ + Retrieves the status of files in the given Git repository. + + Parameters: + repo_path (str): The path to the Git repository. Defaults to the current directory. + + Returns: + dict: A dictionary with keys 'added', 'modified', 'deleted', and 'renamed', + each containing a list of file paths that match the respective status. + """ + + # Open the repository + repo = pygit2.Repository(repo_path) + + # Get the current branch reference + try: + current_branch = repo.head + except pygit2.GitError: + raise pygit2.GitError(f"{repo_path} is not a git repo or does not have a head") + + # Get the main branch reference + main_branch = repo.branches["main"] + + # Get the commit objects + current_commit = repo[current_branch.target] + main_commit = repo[main_branch.target] + + # Get the diff between the current commit and the main branch commit + diff = repo.diff(main_commit, current_commit) + + # Prepare dictionaries to store file statuses + status = { + "added": [], + "modified": [], + "deleted": [], + "moved": [], + } + + # Process the diff + for delta in diff.deltas: + if delta.status == pygit2.GIT_DELTA_ADDED: + status["added"].append(delta.new_file.path) + elif delta.status == pygit2.GIT_DELTA_MODIFIED: + status["modified"].append(delta.new_file.path) + elif delta.status == pygit2.GIT_DELTA_DELETED: + status["deleted"].append(delta.old_file.path) + elif delta.status == pygit2.GIT_DELTA_RENAMED: + status["moved"].append((delta.old_file.path, delta.new_file.path)) + + return { + "added": status["added"], + "modified": status["modified"], + "deleted": status["deleted"], + "moved": status["moved"], + }
+ + + + + + + +
+[docs] +def get_current_git_branch(): + """ + Get the current dirs git branch name. + + Returns + ------- + str + The name of the current git branch. + """ + repo = pygit2.Repository(".") + return repo.head.shorthand
+ +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_modules/brassy/utils/messages.html b/docs/_modules/brassy/utils/messages.html new file mode 100644 index 0000000..76cf156 --- /dev/null +++ b/docs/_modules/brassy/utils/messages.html @@ -0,0 +1,219 @@ + + + + + + + brassy.utils.messages — brassy 0.0.3 documentation + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for brassy.utils.messages

+import logging
+import rich
+from rich.logging import RichHandler
+from rich.console import Console as rich_console
+from rich.prompt import Confirm
+from rich.traceback import install as install_rich_tracebacks
+
+logging.captureWarnings(True)
+
+
+
+[docs] +def init_logger(use_rich): + """ + Initialize and configure the logger. + + Parameters + ---------- + use_rich : bool + If True, sets up rich logging else use standard stream logging + + Returns + ------- + logger : logging.Logger + The configured logger instance + """ + logger = logging.getLogger("build_docs") + if use_rich: + install_rich_tracebacks() + logging_handlers = [RichHandler(rich_tracebacks=True)] + else: + logging_handlers = [logging.StreamHandler()] + + logging.basicConfig(level=logging.DEBUG, datefmt="[%X]", handlers=logging_handlers) + logger.debug("Program initialized") + return logger
+ + + +
+[docs] +def get_rich_opener(no_format=False): + """ + Returns the appropriate opener function for rich progress bar. + + Args: + no_format (bool, optional): If True, returns the opener function without any formatting. + If False, returns the opener function with formatting. Defaults to False. + + Returns: + function: The opener function for rich progress bar. + """ + if no_format: + return rich.progress.Progress().open + else: + return rich.progress.open
+ + + +
+[docs] +def setup_console(no_format=False, quiet=False): + """ + Set up and return the console for printing messages. + + Args: + no_format (bool, optional): Whether to disable formatting. Defaults to False. + quiet (bool, optional): Whether to suppress console output. Defaults to False. + + Returns: + Console: The configured rich console object. + """ + if not no_format: + install_rich_tracebacks() + console = rich_console(quiet=quiet, no_color=(no_format or quiet)) + return console
+ + + +
+[docs] +def get_boolean_prompt_function(format=True): + if format: + return Confirm.ask + else: + + def bool_prompt(question): + answer = input(question).lower() + if answer in ["yes", "y", "ye"]: + return True + elif answer in ["no", "n", ""]: + return False + else: + print("Please respond with 'yes' or 'no'.") + return bool_prompt(question) + + return bool_prompt
+ + + +
+[docs] +def setup_messages(format, quiet): + global open + global boolean_prompt + global RichConsole + global print + open = get_rich_opener(no_format=not format) + RichConsole = setup_console(no_format=not format, quiet=quiet) + print = RichConsole.print if format else print + boolean_prompt = get_boolean_prompt_function(format=format)
+ + + +open = get_rich_opener() +RichConsole = setup_console() +print = RichConsole.print +boolean_prompt = get_boolean_prompt_function() +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_modules/brassy/utils/settings_manager.html b/docs/_modules/brassy/utils/settings_manager.html new file mode 100644 index 0000000..ee07195 --- /dev/null +++ b/docs/_modules/brassy/utils/settings_manager.html @@ -0,0 +1,393 @@ + + + + + + + brassy.utils.settings_manager — brassy 0.0.3 documentation + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for brassy.utils.settings_manager

+import os
+
+from pydantic import ValidationError
+import platformdirs
+import pygit2
+import yaml
+
+from brassy.templates.settings_template import SettingsTemplate
+
+
+
+[docs] +def get_git_repo_root(path="."): + """ + Get the root directory of the Git repository containing the given path. + + Parameters + ---------- + path : str, optional + A path within the Git repository. Defaults to the current directory. + + Returns + ------- + str + Absolute path to the root of the Git repository. This is usually the + path containing the .git folder. + """ + return os.path.abspath(os.path.join(pygit2.Repository(path).path, ".."))
+ + + +
+[docs] +def get_project_config_file_path(app_name): + """ + Retrieve the project-specific configuration file path for the application. + + Parameters + ---------- + app_name : str + Name of the application. + + Returns + ------- + str + Path to the project's configuration file. + """ + project_file = f".{app_name}" + if os.path.isfile(project_file): + return project_file + try: + return os.path.join(get_git_repo_root(), project_file) + except pygit2.GitError: + return project_file
+ + + +
+[docs] +def get_user_config_file_path(app_name): + """ + Retrieve the user-specific configuration file path for the application. + + Parameters + ---------- + app_name : str + Name of the application. + + Returns + ------- + str + Path to the user's configuration file. + """ + return os.path.join(platformdirs.user_config_dir(app_name), "user.config")
+ + + +
+[docs] +def get_site_config_file_path(app_name): + """ + Retrieve the site-specific configuration file path for the application. + + Parameters + ---------- + app_name : str + Name of the application. + + Returns + ------- + str + Path to the site's configuration file. + """ + return os.path.join(platformdirs.site_config_dir(app_name), "site.config")
+ + + +
+[docs] +def get_config_files(app_name): + """ + Get a list of configuration file paths in order of increasing precedence. + + Parameters + ---------- + app_name : str + Name of the application. + + Returns + ------- + list of str + List of configuration file paths. + """ + config_files = [] + for f in [ + get_site_config_file_path, + get_user_config_file_path, + get_project_config_file_path, + ]: + path = f(app_name) + config_files.append(path) + return config_files
+ + + +
+[docs] +def create_config_file(config_file): + """ + Create a configuration file with default settings. + + Parameters + ---------- + config_file : str + Path where the configuration file will be created. + """ + default_settings = SettingsTemplate() + config_dir = os.path.dirname(config_file) + if config_dir: + os.makedirs(config_dir, exist_ok=True) + with open(config_file, "wt") as f: + yaml.dump(default_settings.dict(), f)
+ + + +
+[docs] +def read_config_file(config_file, create_file_if_not_exist=False): + """ + Read and parse a YAML configuration file. + + Parameters + ---------- + config_file : str + Path to the configuration file. + create_file_if_not_exist : bool + Creates file if it doesn't exist + + Returns + ------- + dict + Parsed configuration settings. + """ + try: + with open(config_file, "rt") as f: + return yaml.safe_load(f) + except FileNotFoundError: + if not create_file_if_not_exist: + return SettingsTemplate().dict() + else: + create_config_file(config_file) + return read_config_file(config_file)
+ + + +
+[docs] +def merge_and_validate_config_files(config_files): + """ + Merge settings from multiple configuration files and validate them. + + Parameters + ---------- + config_files : list of str + List of configuration file paths. The order of the files matters. + Each file overwrites the values of the previous. + + Returns + ------- + dict + Merged and validated configuration settings. + + Raises + ------ + ValidationError + If any of the settings do not conform to the `Settings` model. + """ + settings = {} + for config_file in config_files: + file_settings = read_config_file(config_file, create_file_if_not_exist=False) + try: + SettingsTemplate(**file_settings) + except ValidationError as e: + print(f"Failed to validate {config_file}") + print(repr(e.errors()[0])) + raise e + settings.update(file_settings) + return settings
+ + + +
+[docs] +def get_settings_from_config_files(app_name): + """ + Retrieve settings from configuration files without environment overrides. + + Parameters + ---------- + app_name : str + Name of the application. + + Returns + ------- + dict + Configuration settings merged from files. + """ + return merge_and_validate_config_files(get_config_files(app_name))
+ + + +
+[docs] +def override_dict_with_environmental_variables(input_dict): + """ + Override dict values with case insensitive environment variables when available. + + + Parameters + ---------- + input_dict : dict + Original settings dictionary. + + Returns + ------- + dict + Updated settings dictionary with environment variable overrides. + """ + env_vars = dict(os.environ) + lower_env_vars = { + key.lower(): {"env_var": key, "value": value} for key, value in env_vars.items() + } + for key in input_dict.keys(): + if key.lower() in lower_env_vars: + override = lower_env_vars[key.lower()] + # print( + # f"Overriding value {key} with environmental " + # f"variable {override['env_var']} " + # f"with value {override['value']}" + # ) + input_dict[key] = override["value"] + return input_dict
+ + + +
+[docs] +def get_settings(app_name): + """ + Return application settings from config files and environment variables. + + Parameters + ---------- + app_name : str + Name of the application. + + Returns + ------- + Settings + An instance of the `Settings` model with all configurations applied. + + Raises + ------ + ValidationError + If the final settings do not conform to the `Settings` model. + """ + file_settings = override_dict_with_environmental_variables( + get_settings_from_config_files(app_name) + ) + Settings = SettingsTemplate(**file_settings) + return Settings
+ +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_modules/index.html b/docs/_modules/index.html new file mode 100644 index 0000000..a765a46 --- /dev/null +++ b/docs/_modules/index.html @@ -0,0 +1,110 @@ + + + + + + + Overview: module code — brassy 0.0.3 documentation + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_sources/api.rst.txt b/docs/_sources/api.rst.txt index 858cce7..e7c7e06 100644 --- a/docs/_sources/api.rst.txt +++ b/docs/_sources/api.rst.txt @@ -1,17 +1,110 @@ API === -CLI Arguments -------------- +.. + CLI Arguments + ------------- -.. argparse:: - :filename: ../src/brassy/brassy.py - :func: get_parser + .. argparse:: + :filename: ../src/brassy/utils/CLI.py + :func: get_parser Code Docstrings --------------- -.. automodule:: brassy +Actions +^^^^^^^ + +Prune YAML +"""""""""" + +.. automodule:: brassy.actions.prune_yaml + :members: + :undoc-members: + :show-inheritance: + +Build Release Notes +""""""""""""""""""" + +.. automodule:: brassy.actions.build_release_notes + :members: + :undoc-members: + :show-inheritance: + +Initialize +"""""""""" + +.. automodule:: brassy.actions.init + :members: + :undoc-members: + :show-inheritance: + +Templates +^^^^^^^^^ + +Release YAML +"""""""""""" + +.. automodule:: brassy.templates.release_yaml_template + :members: + :undoc-members: + :show-inheritance: + +Settings +"""""""" + +.. automodule:: brassy.templates.settings_template + :members: + :undoc-members: + :show-inheritance: + +Utils +^^^^^ + +CLI +""" + +.. automodule:: brassy.utils.CLI + :members: + :undoc-members: + :show-inheritance: + +git_handler +""""""""""" + +.. automodule:: brassy.utils.git_handler + :members: + :undoc-members: + :show-inheritance: + +Messages +"""""""" + +.. automodule:: brassy.utils.messages + :members: + :undoc-members: + :show-inheritance: + +Settings Manager +"""""""""""""""" + +.. automodule:: brassy.utils.settings_manager + :members: + :undoc-members: + :show-inheritance: + +File Handler +"""""""""""" + +.. automodule:: brassy.utils.file_handler + :members: + :undoc-members: + :show-inheritance: + +Main Module +^^^^^^^^^^^ + +.. automodule:: brassy.brassy :members: :undoc-members: :private-members: diff --git a/docs/_sources/index.rst.txt b/docs/_sources/index.rst.txt index a1a8292..fa1f515 100644 --- a/docs/_sources/index.rst.txt +++ b/docs/_sources/index.rst.txt @@ -12,7 +12,7 @@ Welcome to brassy's documentation! .. toctree:: :maxdepth: 2 - getting-started + using-brassy api ./releases/index @@ -22,3 +22,5 @@ Indices and tables * :ref:`genindex` * :ref:`modindex` * :ref:`search` + +`GitHub Repository `_ diff --git a/docs/_sources/releases/0.0.3.rst.txt b/docs/_sources/releases/0.0.3.rst.txt new file mode 100644 index 0000000..2860c6e --- /dev/null +++ b/docs/_sources/releases/0.0.3.rst.txt @@ -0,0 +1,238 @@ +Version 0.0.3 (October 23, 2024) +******************************** + + * *Enhancement*: Implement default path + * *Enhancement*: Add internal option to related issue + * *Enhancement*: Add --init that writes out config files + * *Enhancement*: Put title at top of template + * *Enhancement*: Allow users to disable pipe on description field + * *Enhancement*: Add start and finish dates for changes + * *Bug fix*: Print pretty error on blank yaml. + * *Bug fix*: Builds now have sections with either a missing title or description + * *Bug fix*: Allow entries to have no field and no title + * *Bug fix*: Fix bug with --write-template-error + * *Bug fix*: Make optional dates optional + * *Bug fix*: Fix empty "--write-yaml-template bug + * *Documentation*: Documentation updated for new version + * *Documentation*: Fix docs rendering of examples. + * *Continuous integration*: Added new templating integration test + * *Continuous integration*: Add integration tests + +Enhancement +=========== + +Implement default path +---------------------- + +Add experimental feature where a path can be specified in the settings as the "default" path for brassy actions. + +:: + + modified: brassy.py + +Enhancement +=========== + +Add internal option to related issue +------------------------------------ + +Some organizations use internal repos that cannot be accessed by CI. This change adds an "internal" field for the +related issue so that users can add strings directly rather than use the URL+issue number combo previously provided. The +strings are rendered directly, but must fit a "repo#number - description" pattern. The field is named "internal" to +suggest to users that when possible the URL option *should* be used to access future functionality. + +:: + + modified: src/brassy/templates/release_yaml_template.py + +Enhancement +=========== + +Add --init that writes out config files +--------------------------------------- + +Added new CLI option that generates project ``.brassy`` config files + +:: + + added: src/brassy/actions/init.py + +Enhancement +=========== + +Put title at top of template +---------------------------- + +Title is now at top of template because users were mixing up description and title. This change is backwards compatible, +and no updates to yaml files are needed. + +:: + + modified: src/brassy/brassy.py + +Enhancement +=========== + +Allow users to disable pipe on description field +------------------------------------------------ + +A pipe is added by default in this release. This change allows users to revert +to the old behaviour by changing their local settings. + +Enhancement +=========== + +Add start and finish dates for changes +-------------------------------------- + +Per user request, start and finish date fields have been added to the yaml template. They are NOT rendered yet. + +:: + + modified: src/brassy/brassy.py + +Bug fix +======= + +Print pretty error on blank yaml. +--------------------------------- + +Now prints "No valid YAML content in file. Please populate example.yaml" when run on blank yaml file. + +:: + + modified: src/brassy/brassy.py + +Bug fix +======= + +Builds now have sections with either a missing title or description +------------------------------------------------------------------- + +Previously, if yaml files only had title OR description populated, they would silently not get built into release notes. +Now they are incorporated into release notes with a warning. + +:: + + modified: README.md + modified: src/brassy/__init__.py + modified: src/brassy/actions/build_release_notes.py + modified: src/brassy/templates/release_yaml_template.py + modified: src/brassy/utils/CLI.py + +Bug fix +======= + +Allow entries to have no field and no title +------------------------------------------- + +Not allowing fields to have no title was causing blank sections to +break builds. Check removed for now. + +Bug fix +======= + +Fix bug with --write-template-error +----------------------------------- + +Fixed issue where --write-template-error wouldn't run without argument + +:: + + modified: src/brassy/brassy.py + +Bug fix +======= + +Make optional dates optional +---------------------------- + +Dates for changes are optional. This fix allows users to leave the entry off, rather than requiring a null field for +building. + +:: + + modified: src/brassy/templates/release_yaml_template.py + +Bug fix +======= + +Fix empty "--write-yaml-template bug +------------------------------------ + +This was previously hotfixed, merging into main codebase now. + +:: + + modified: src/brassy/brassy.py + +Documentation +============= + +Documentation updated for new version +------------------------------------- + +Documentation written for new versions! :) + +Documentation +============= + +Fix docs rendering of examples. +------------------------------- + +Fix docs rendering of yaml examples by updating old yaml files. + +:: + + modified: docs/api.html + modified: docs/genindex.html + modified: docs/getting-started.html + modified: docs/index.html + modified: docs/objects.inv + modified: docs/searchindex.js + modified: pyproject.toml + modified: sphinx/source/examples/basic-usage/new-release-note + modified: sphinx/source/examples/basic-usage/new-release-note-date + modified: sphinx/source/examples/basic-usage/new-release-note-header-footer + modified: sphinx/source/examples/basic-usage/new-release-note-v1 + modified: sphinx/source/examples/basic-usage/release-note.yaml + deleted: docs/py-modindex.html + +Continuous integration +====================== + +Added new templating integration test +------------------------------------- + +Added a new test that creates a template and then tried to build it. + +:: + + modified: tests/test_integ.py + +Continuous integration +====================== + +Add integration tests +--------------------- + +Added basic integration tests. More work needed. + +:: + + added: pytest.ini + added: tests/inputs/barebones.yaml + added: tests/inputs/fully-featured.yaml + added: tests/inputs/mostly-featured.yaml + added: tests/inputs/to-prune.yaml + added: tests/outputs/barebones.rst + added: tests/outputs/fully-featured.rst + added: tests/outputs/mostly-featured.rst + added: tests/outputs/pruned.yaml + added: tests/test_integ.py + deleted: test/575-cli-class-factory.yaml + deleted: test/burgers.rst + deleted: test/burgers.yaml + deleted: test/test.py + deleted: test/test.rst + deleted: test/test2.yaml diff --git a/docs/_sources/releases/index.rst.txt b/docs/_sources/releases/index.rst.txt index fd4d740..5d4a039 100644 --- a/docs/_sources/releases/index.rst.txt +++ b/docs/_sources/releases/index.rst.txt @@ -1,6 +1,14 @@ Release Notes ************* +Version 0.0.3 +------------- + +.. toctree:: + :maxdepth: 1 + + 0.0.3 + Version 0.0.2 ------------- diff --git a/sphinx/source/getting-started.rst b/docs/_sources/using-brassy.rst.txt similarity index 79% rename from sphinx/source/getting-started.rst rename to docs/_sources/using-brassy.rst.txt index b91b16e..20c94cc 100644 --- a/sphinx/source/getting-started.rst +++ b/docs/_sources/using-brassy.rst.txt @@ -1,5 +1,5 @@ -Getting Started -=============== +Using Brassy +============ Example usage ------------- @@ -23,7 +23,7 @@ branch name. You can also specify a name manually, and By default, the yaml template will be populated with the following fields: -.. runcmd:: python3 -c "from brassy import brassy; print('\n'.join(brassy.default_categories))" +.. runcmd:: python3 -c "from brassy.templates.release_yaml_template import categories; print('\n'.join(categories))" You can configure this in your ``.brassy`` file. See also Settings. @@ -142,8 +142,10 @@ Which would output: Specifying Date ^^^^^^^^^^^^^^^ -You can specify the date of the release notes -by using the ``-d`` or ``--release-date`` flag. +By default, brassy uses todays date in ``YYYY-MM-DD`` format. + +You can specify the date of the release notes in any format +with the ``-d`` or ``--release-date`` flag. For example, using the previous yaml file: @@ -179,8 +181,44 @@ Would output: .. literalinclude :: ./examples/basic-usage/new-release-note-header-footer +Change YAML directory +--------------------- + +By default brassy works in your current working directory. + +You can specify a directory with ``--yaml-dir`` or ``-yd``. + +For example: + +.. code-block:: bash + + brassy --yaml-dir ./docs/release-notes/v1.0.0 \ + --write-template "updating-gpu-code" + +would write a template file ``updating-gpu-code.yaml`` +to ``./docs/release-notes/v1.0.0``. + +Prune YAML file +--------------- + +Brassy can "prune" yaml files by removing blank sections. Sections are considered blank +if all of their items are blank OR are empty lists. + +For example: + +.. literalinclude :: ./examples/basic-usage/to-prune.yaml + +would become + +.. literalinclude :: ./examples/basic-usage/pruned.yaml + +after pruning. + +To prune a file, pass it to brassy with ``--prune``. +Eg. ``brassy --prune fake_file.yaml`` + Controlling CLI Output -^^^^^^^^^^^^^^^^^^^^^^ +---------------------- You can turn off fancy formatting (colors, bold, etc.) by using the ``--no-color``/``-nc`` flag. @@ -188,7 +226,7 @@ by using the ``--no-color``/``-nc`` flag. You can also turn off all non-error outputs by using the ``--quiet`` or ``-q`` flag. Help! -^^^^^ +----- When in doubt, you can always run the help command to see what options are available: diff --git a/docs/_static/alabaster.css b/docs/_static/alabaster.css index e3174bf..7e75bf8 100644 --- a/docs/_static/alabaster.css +++ b/docs/_static/alabaster.css @@ -1,5 +1,3 @@ -@import url("basic.css"); - /* -- page layout ----------------------------------------------------------- */ body { @@ -160,8 +158,8 @@ div.sphinxsidebar input { font-size: 1em; } -div.sphinxsidebar #searchbox input[type="text"] { - width: 160px; +div.sphinxsidebar #searchbox { + margin: 1em 0; } div.sphinxsidebar .search > div { @@ -263,10 +261,6 @@ div.admonition p.last { margin-bottom: 0; } -div.highlight { - background-color: #fff; -} - dt:target, .highlight { background: #FAF3E8; } @@ -454,7 +448,7 @@ ul, ol { } pre { - background: #EEE; + background: unset; padding: 7px 30px; margin: 15px 0px; line-height: 1.3em; @@ -485,15 +479,15 @@ a.reference { border-bottom: 1px dotted #004B6B; } +a.reference:hover { + border-bottom: 1px solid #6D4100; +} + /* Don't put an underline on images */ a.image-reference, a.image-reference:hover { border-bottom: none; } -a.reference:hover { - border-bottom: 1px solid #6D4100; -} - a.footnote-reference { text-decoration: none; font-size: 0.7em; @@ -509,68 +503,7 @@ a:hover tt, a:hover code { background: #EEE; } - -@media screen and (max-width: 870px) { - - div.sphinxsidebar { - display: none; - } - - div.document { - width: 100%; - - } - - div.documentwrapper { - margin-left: 0; - margin-top: 0; - margin-right: 0; - margin-bottom: 0; - } - - div.bodywrapper { - margin-top: 0; - margin-right: 0; - margin-bottom: 0; - margin-left: 0; - } - - ul { - margin-left: 0; - } - - li > ul { - /* Matches the 30px from the "ul, ol" selector above */ - margin-left: 30px; - } - - .document { - width: auto; - } - - .footer { - width: auto; - } - - .bodywrapper { - margin: 0; - } - - .footer { - width: auto; - } - - .github { - display: none; - } - - - -} - - - -@media screen and (max-width: 875px) { +@media screen and (max-width: 940px) { body { margin: 0; @@ -580,12 +513,16 @@ a:hover tt, a:hover code { div.documentwrapper { float: none; background: #fff; + margin-left: 0; + margin-top: 0; + margin-right: 0; + margin-bottom: 0; } div.sphinxsidebar { display: block; float: none; - width: 102.5%; + width: unset; margin: 50px -30px -20px -30px; padding: 10px 20px; background: #333; @@ -620,8 +557,14 @@ a:hover tt, a:hover code { div.body { min-height: 0; + min-width: auto; /* fixes width on small screens, breaks .hll */ padding: 0; } + + .hll { + /* "fixes" the breakage */ + width: max-content; + } .rtd_doc_footer { display: none; @@ -635,13 +578,18 @@ a:hover tt, a:hover code { width: auto; } - .footer { - width: auto; - } - .github { display: none; } + + ul { + margin-left: 0; + } + + li > ul { + /* Matches the 30px from the "ul, ol" selector above */ + margin-left: 30px; + } } @@ -705,4 +653,11 @@ nav#breadcrumbs li+li:before { div.related { display: none; } +} + +img.github { + position: absolute; + top: 0; + border: 0; + right: 0; } \ No newline at end of file diff --git a/docs/_static/basic.css b/docs/_static/basic.css index 4157edf..e5179b7 100644 --- a/docs/_static/basic.css +++ b/docs/_static/basic.css @@ -4,7 +4,7 @@ * * Sphinx stylesheet -- basic theme. * - * :copyright: Copyright 2007-2023 by the Sphinx team, see AUTHORS. + * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ diff --git a/docs/_static/doctools.js b/docs/_static/doctools.js index d06a71d..4d67807 100644 --- a/docs/_static/doctools.js +++ b/docs/_static/doctools.js @@ -4,7 +4,7 @@ * * Base JavaScript utilities for all Sphinx HTML documentation. * - * :copyright: Copyright 2007-2023 by the Sphinx team, see AUTHORS. + * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ diff --git a/docs/_static/documentation_options.js b/docs/_static/documentation_options.js index 8a3ed37..74029b8 100644 --- a/docs/_static/documentation_options.js +++ b/docs/_static/documentation_options.js @@ -1,5 +1,5 @@ const DOCUMENTATION_OPTIONS = { - VERSION: '0.0.1a', + VERSION: '0.0.3', LANGUAGE: 'en', COLLAPSE_INDEX: false, BUILDER: 'html', diff --git a/docs/_static/github-banner.svg b/docs/_static/github-banner.svg new file mode 100644 index 0000000..c47d9dc --- /dev/null +++ b/docs/_static/github-banner.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/docs/_static/language_data.js b/docs/_static/language_data.js index 250f566..367b8ed 100644 --- a/docs/_static/language_data.js +++ b/docs/_static/language_data.js @@ -5,7 +5,7 @@ * This script contains the language-specific data used by searchtools.js, * namely the list of stopwords, stemmer, scorer and splitter. * - * :copyright: Copyright 2007-2023 by the Sphinx team, see AUTHORS. + * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ @@ -13,7 +13,7 @@ var stopwords = ["a", "and", "are", "as", "at", "be", "but", "by", "for", "if", "in", "into", "is", "it", "near", "no", "not", "of", "on", "or", "such", "that", "the", "their", "then", "there", "these", "they", "this", "to", "was", "will", "with"]; -/* Non-minified version is copied as a separate JS file, is available */ +/* Non-minified version is copied as a separate JS file, if available */ /** * Porter Stemmer diff --git a/docs/_static/searchtools.js b/docs/_static/searchtools.js index 7918c3f..b08d58c 100644 --- a/docs/_static/searchtools.js +++ b/docs/_static/searchtools.js @@ -4,7 +4,7 @@ * * Sphinx JavaScript utilities for the full-text search. * - * :copyright: Copyright 2007-2023 by the Sphinx team, see AUTHORS. + * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ @@ -99,7 +99,7 @@ const _displayItem = (item, searchTerms, highlightTerms) => { .then((data) => { if (data) listItem.appendChild( - Search.makeSearchSummary(data, searchTerms) + Search.makeSearchSummary(data, searchTerms, anchor) ); // highlight search terms in the summary if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js @@ -116,8 +116,8 @@ const _finishSearch = (resultCount) => { ); else Search.status.innerText = _( - `Search finished, found ${resultCount} page(s) matching the search query.` - ); + "Search finished, found ${resultCount} page(s) matching the search query." + ).replace('${resultCount}', resultCount); }; const _displayNextItem = ( results, @@ -137,6 +137,22 @@ const _displayNextItem = ( // search finished, update title and status message else _finishSearch(resultCount); }; +// Helper function used by query() to order search results. +// Each input is an array of [docname, title, anchor, descr, score, filename]. +// Order the results by score (in opposite order of appearance, since the +// `_displayNextItem` function uses pop() to retrieve items) and then alphabetically. +const _orderResultsByScoreThenName = (a, b) => { + const leftScore = a[4]; + const rightScore = b[4]; + if (leftScore === rightScore) { + // same score: sort alphabetically + const leftTitle = a[1].toLowerCase(); + const rightTitle = b[1].toLowerCase(); + if (leftTitle === rightTitle) return 0; + return leftTitle > rightTitle ? -1 : 1; // inverted is intentional + } + return leftScore > rightScore ? 1 : -1; +}; /** * Default splitQuery function. Can be overridden in ``sphinx.search`` with a @@ -160,13 +176,26 @@ const Search = { _queued_query: null, _pulse_status: -1, - htmlToText: (htmlString) => { + htmlToText: (htmlString, anchor) => { const htmlElement = new DOMParser().parseFromString(htmlString, 'text/html'); - htmlElement.querySelectorAll(".headerlink").forEach((el) => { el.remove() }); + for (const removalQuery of [".headerlink", "script", "style"]) { + htmlElement.querySelectorAll(removalQuery).forEach((el) => { el.remove() }); + } + if (anchor) { + const anchorContent = htmlElement.querySelector(`[role="main"] ${anchor}`); + if (anchorContent) return anchorContent.textContent; + + console.warn( + `Anchored content block not found. Sphinx search tries to obtain it via DOM query '[role=main] ${anchor}'. Check your theme or template.` + ); + } + + // if anchor not specified or not found, fall back to main content const docContent = htmlElement.querySelector('[role="main"]'); - if (docContent !== undefined) return docContent.textContent; + if (docContent) return docContent.textContent; + console.warn( - "Content block not found. Sphinx search tries to obtain it via '[role=main]'. Could you check your theme or template." + "Content block not found. Sphinx search tries to obtain it via DOM query '[role=main]'. Check your theme or template." ); return ""; }, @@ -239,16 +268,7 @@ const Search = { else Search.deferQuery(query); }, - /** - * execute search (requires search index to be loaded) - */ - query: (query) => { - const filenames = Search._index.filenames; - const docNames = Search._index.docnames; - const titles = Search._index.titles; - const allTitles = Search._index.alltitles; - const indexEntries = Search._index.indexentries; - + _parseQuery: (query) => { // stem the search terms and add them to the correct list const stemmer = new Stemmer(); const searchTerms = new Set(); @@ -284,21 +304,38 @@ const Search = { // console.info("required: ", [...searchTerms]); // console.info("excluded: ", [...excludedTerms]); - // array of [docname, title, anchor, descr, score, filename] - let results = []; + return [query, searchTerms, excludedTerms, highlightTerms, objectTerms]; + }, + + /** + * execute search (requires search index to be loaded) + */ + _performSearch: (query, searchTerms, excludedTerms, highlightTerms, objectTerms) => { + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const titles = Search._index.titles; + const allTitles = Search._index.alltitles; + const indexEntries = Search._index.indexentries; + + // Collect multiple result groups to be sorted separately and then ordered. + // Each is an array of [docname, title, anchor, descr, score, filename]. + const normalResults = []; + const nonMainIndexResults = []; + _removeChildren(document.getElementById("search-progress")); - const queryLower = query.toLowerCase(); + const queryLower = query.toLowerCase().trim(); for (const [title, foundTitles] of Object.entries(allTitles)) { - if (title.toLowerCase().includes(queryLower) && (queryLower.length >= title.length/2)) { + if (title.toLowerCase().trim().includes(queryLower) && (queryLower.length >= title.length/2)) { for (const [file, id] of foundTitles) { - let score = Math.round(100 * queryLower.length / title.length) - results.push([ + const score = Math.round(Scorer.title * queryLower.length / title.length); + const boost = titles[file] === title ? 1 : 0; // add a boost for document titles + normalResults.push([ docNames[file], titles[file] !== title ? `${titles[file]} > ${title}` : title, id !== null ? "#" + id : "", null, - score, + score + boost, filenames[file], ]); } @@ -308,46 +345,47 @@ const Search = { // search for explicit entries in index directives for (const [entry, foundEntries] of Object.entries(indexEntries)) { if (entry.includes(queryLower) && (queryLower.length >= entry.length/2)) { - for (const [file, id] of foundEntries) { - let score = Math.round(100 * queryLower.length / entry.length) - results.push([ + for (const [file, id, isMain] of foundEntries) { + const score = Math.round(100 * queryLower.length / entry.length); + const result = [ docNames[file], titles[file], id ? "#" + id : "", null, score, filenames[file], - ]); + ]; + if (isMain) { + normalResults.push(result); + } else { + nonMainIndexResults.push(result); + } } } } // lookup as object objectTerms.forEach((term) => - results.push(...Search.performObjectSearch(term, objectTerms)) + normalResults.push(...Search.performObjectSearch(term, objectTerms)) ); // lookup as search terms in fulltext - results.push(...Search.performTermsSearch(searchTerms, excludedTerms)); + normalResults.push(...Search.performTermsSearch(searchTerms, excludedTerms)); // let the scorer override scores with a custom scoring function - if (Scorer.score) results.forEach((item) => (item[4] = Scorer.score(item))); - - // now sort the results by score (in opposite order of appearance, since the - // display function below uses pop() to retrieve items) and then - // alphabetically - results.sort((a, b) => { - const leftScore = a[4]; - const rightScore = b[4]; - if (leftScore === rightScore) { - // same score: sort alphabetically - const leftTitle = a[1].toLowerCase(); - const rightTitle = b[1].toLowerCase(); - if (leftTitle === rightTitle) return 0; - return leftTitle > rightTitle ? -1 : 1; // inverted is intentional - } - return leftScore > rightScore ? 1 : -1; - }); + if (Scorer.score) { + normalResults.forEach((item) => (item[4] = Scorer.score(item))); + nonMainIndexResults.forEach((item) => (item[4] = Scorer.score(item))); + } + + // Sort each group of results by score and then alphabetically by name. + normalResults.sort(_orderResultsByScoreThenName); + nonMainIndexResults.sort(_orderResultsByScoreThenName); + + // Combine the result groups in (reverse) order. + // Non-main index entries are typically arbitrary cross-references, + // so display them after other results. + let results = [...nonMainIndexResults, ...normalResults]; // remove duplicate search results // note the reversing of results, so that in the case of duplicates, the highest-scoring entry is kept @@ -361,7 +399,12 @@ const Search = { return acc; }, []); - results = results.reverse(); + return results.reverse(); + }, + + query: (query) => { + const [searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms] = Search._parseQuery(query); + const results = Search._performSearch(searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms); // for debugging //Search.lastresults = results.slice(); // a copy @@ -466,14 +509,18 @@ const Search = { // add support for partial matches if (word.length > 2) { const escapedWord = _escapeRegExp(word); - Object.keys(terms).forEach((term) => { - if (term.match(escapedWord) && !terms[word]) - arr.push({ files: terms[term], score: Scorer.partialTerm }); - }); - Object.keys(titleTerms).forEach((term) => { - if (term.match(escapedWord) && !titleTerms[word]) - arr.push({ files: titleTerms[word], score: Scorer.partialTitle }); - }); + if (!terms.hasOwnProperty(word)) { + Object.keys(terms).forEach((term) => { + if (term.match(escapedWord)) + arr.push({ files: terms[term], score: Scorer.partialTerm }); + }); + } + if (!titleTerms.hasOwnProperty(word)) { + Object.keys(titleTerms).forEach((term) => { + if (term.match(escapedWord)) + arr.push({ files: titleTerms[term], score: Scorer.partialTitle }); + }); + } } // no match but word was a required one @@ -496,9 +543,8 @@ const Search = { // create the mapping files.forEach((file) => { - if (fileMap.has(file) && fileMap.get(file).indexOf(word) === -1) - fileMap.get(file).push(word); - else fileMap.set(file, [word]); + if (!fileMap.has(file)) fileMap.set(file, [word]); + else if (fileMap.get(file).indexOf(word) === -1) fileMap.get(file).push(word); }); }); @@ -549,8 +595,8 @@ const Search = { * search summary for a given text. keywords is a list * of stemmed words. */ - makeSearchSummary: (htmlText, keywords) => { - const text = Search.htmlToText(htmlText); + makeSearchSummary: (htmlText, keywords, anchor) => { + const text = Search.htmlToText(htmlText, anchor); if (text === "") return null; const textLower = text.toLowerCase(); diff --git a/docs/api.html b/docs/api.html index 1ae63c4..302bfc0 100644 --- a/docs/api.html +++ b/docs/api.html @@ -5,16 +5,17 @@ - API — brassy 0.0.1a documentation + API — brassy 0.0.3 documentation - - - + + + + - + @@ -34,67 +35,1101 @@

API

-
-

CLI Arguments

-

Generate release notes from YAML files. Entries are sorted by order in yaml files, and by order of yaml files provided via the command line.

-

-
Usage: sphinx-build [-h] [-t [WRITE_YAML_TEMPLATE]] [-c [GET_CHANGED_FILES]] [-r VERSION] [-d RELEASE_DATE] [-nc] [-p PREFIX_FILE] [-s SUFFIX_FILE] [-o OUTPUT_FILE] [--output-to-console] [-nr] [-q] [input_files_or_folders ...]
+
+

Code Docstrings

+
+

Actions

+
+

Prune YAML

+
+
+brassy.actions.prune_yaml.direct_pruning_of_files(input_files_or_folders, console, working_dir)[source]
+

Prune empty values from YAML files specified by input paths.

+
+
Parameters:
+
    +
  • input_files_or_folders (list of str) – A list of file paths or directories containing YAML files to prune.

  • +
  • console (Console) – An object used for printing messages to the console.

  • +
  • working_dir (str) – The working directory path.

  • +
+
+
Return type:
+

None

+
+
+

Notes

+

This function collects YAML files from the specified input paths and +prunes each file using prune_yaml_file.

+

Examples

+
>>> direct_pruning_of_files(['configs/'], console, '/home/user')
+Pruned configs/config1.yaml
+Pruned configs/config2.yaml
+
+
+
+ +
+
+brassy.actions.prune_yaml.prune_empty(data, prune_lists=True, key='')[source]
+

Recursively remove empty values from a nested dictionary or list.

+
+
Parameters:
+
    +
  • data (dict or list) – The data structure to prune.

  • +
  • prune_lists (bool, optional) – Indicates whether to prune empty lists. Currently unused.

  • +
  • key (str, optional) – The key associated with the current data item, used for special cases.

  • +
+
+
Returns:
+

The pruned data structure, or None if it is empty.

+
+
Return type:
+

dict or list or None

+
+
+

Notes

+

The function considers the following values as empty: None, empty strings, +empty dictionaries, and empty lists. If a value is 0 and the key is +“number”, it is also considered empty to address the related issues +field which was previously set to 0 instead of null.

+

Examples

+
>>> data = {'a': None, 'b': '', 'c': {'d': [], 'e': 'value'}}
+>>> prune_empty(data)
+{'c': {'e': 'value'}}
 
-
-

Positional Arguments

-
-
input_files_or_folders
-

The folder(s) containing YAML files and/or YAML files. Folders will be searched recursively.

+
+ +
+
+brassy.actions.prune_yaml.prune_yaml_file(yaml_file_path, console)[source]
+

Prune empty values from a YAML file and overwrite it with the pruned content.

+
+
Parameters:
+
    +
  • yaml_file_path (str) – The file path to the YAML file to be pruned.

  • +
  • console (Console) – An object used for printing messages to the console.

  • +
+
+
Return type:
+

None

+

Notes

+

This function reads the YAML file, prunes empty values using prune_empty, +and writes the pruned content back to the same file.

+

Examples

+
>>> prune_yaml_file('config.yaml', console)
+Pruned config.yaml
+
+
+
+
-
-

Named Arguments

-
-
-t, --write-yaml-template
-

Write template YAML to provided file. If folder provided, place template in folder with current git branch name as file name.

+
+

Build Release Notes

+
+
+brassy.actions.build_release_notes.build_release_notes(input_files_or_folders, console, rich_open, version=None, release_date=None, header_file=None, footer_file=None, working_dir='.')[source]
+

Build release notes from YAML data.

+
+
Parameters:
+
    +
  • data (dict) – Parsed content of YAML files.

  • +
  • version (str, optional) – Version number of the release, by default ‘1.1’.

  • +
  • release_date (str, optional) – Release date, by default None, which uses today’s date.

  • +
  • header_file (str, optional) – A header file to prepend to the release notes.

  • +
  • footer_file (str, optional) – A footer file to suffix to the release notes.

  • +
+
+
Returns:
+

Formatted release notes in .rst format.

-
-c, --get-changed-files
-

Print git tracked file changes against main. If directory provided, use that directories checked-out branch.

+
Return type:
+

str

+
+
+
+ +
+
+brassy.actions.build_release_notes.find_duplicate_titles(data)[source]
+

Check if there are any duplicate titles in dictionaries of lists of dictionaries.

+
+
Parameters:
+

data (dict) – A dictionary containing lists of dictionaries with items +indexed by “title”.

-
-r, --release-version
-

Version number of the release. Default is ‘[UNKNOWN]’.

-

Default: “[UNKNOWN]”

+
Returns:
+

True if there are duplicate “title” values, False otherwise.

-
-d, --release-date
-

Date of the release. Default is current system time.

+
Return type:
+

bool

-
-nc, --no-color
-

Disable text formatting for CLI output.

-

Default: False

+
+
+ +
+
+brassy.actions.build_release_notes.format_files_changed_entry(detailed, entry)[source]
+
+ +
+
+brassy.actions.build_release_notes.format_release_notes(data, version, release_date=None, header=None, footer=None)[source]
+

Format the parsed YAML data into release notes in .rst format.

+
+
Parameters:
+
    +
  • data (dict) – Parsed content of YAML files.

  • +
  • version (str, optional) – Version number of the release, by default ‘1.1’.

  • +
  • release_date (str, optional) – Release date, by default None, which uses today’s date.

  • +
-
-p, --prefix-file
-

A header file to prepend to the release notes.

+
Returns:
+

Formatted release notes in .rst format.

-
-s, --suffix-file
-

A footer file to suffix to the release notes.

+
Return type:
+

str

-
-o, --output-file
-

The output file for release notes.

+
+
+ +
+
+brassy.actions.build_release_notes.generate_file_change_section_list_of_strings(entry, line, category, title, description)[source]
+
+ +
+
+brassy.actions.build_release_notes.generate_section_string(section_lines, changelog_entries, release_date, version, footer, header)[source]
+
+ +
+ +

Adds a header and/or footer to the given content.

+
+
Parameters:
+
    +
  • content (str) – The content to which the header and/or footer will be added.

  • +
  • rich_open (function) – A function used to open files.

  • +
  • header_file (str, optional) – The file containing the header content. Defaults to None.

  • +
  • footer_file (str, optional) – The file containing the footer content. Defaults to None.

  • +
-
--output-to-console
-

Write generated release notes to console.

-

Default: False

+
Returns:
+

The content with the header and/or footer added.

-
-nr, --no-rich
-

Disable rich text output

-

Default: False

+
Return type:
+

str

-
-q, --quiet
-

Only output errors

-

Default: False

+
+
+ +
+
+

Initialize

+
+
+brassy.actions.init.init()[source]
+

Initialize configuration files for the application.

+

This function creates site and user configuration files, and optionally +a project configuration file based on user input.

+
+
Return type:
+

None

+

Examples

+
>>> init()
+Do you want to create a project config file? [y/N]: y
+
+
+
+
-
-

Code Docstrings

+
+

Templates

+
+

Release YAML

+
+
+class brassy.templates.release_yaml_template.ChangeItem(*, title: Annotated[str | None, MinLen(min_length=1)], description: Annotated[str | None, MinLen(min_length=1)], files: Files, related_issue: RelatedIssue | RelatedInternalIssue | None = None, date: DateRange | None = None)[source]
+

Bases: BaseModel

+
+
+date: DateRange | None
+
+ +
+
+description: str | None
+
+ +
+
+empty_str_to_none()[source]
+
+ +
+
+files: Files
+
+ +
+
+model_computed_fields: ClassVar[Dict[str, ComputedFieldInfo]] = {}
+

A dictionary of computed field names and their corresponding ComputedFieldInfo objects.

+
+ +
+
+model_config: ClassVar[ConfigDict] = {}
+

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

+
+ +
+
+model_fields: ClassVar[Dict[str, FieldInfo]] = {'date': FieldInfo(annotation=Union[DateRange, NoneType], required=False, default=None), 'description': FieldInfo(annotation=Union[str, NoneType], required=True, json_schema_extra={'strip_whitespace': True}, metadata=[MinLen(min_length=1)]), 'files': FieldInfo(annotation=Files, required=True), 'related_issue': FieldInfo(annotation=Union[RelatedIssue, RelatedInternalIssue, NoneType], required=False, default=None, alias='related-issue', alias_priority=2, json_schema_extra={'exclude_unset': True}), 'title': FieldInfo(annotation=Union[str, NoneType], required=True, json_schema_extra={'strip_whitespace': True}, metadata=[MinLen(min_length=1)])}
+

Metadata about the fields defined on the model, +mapping of field names to [FieldInfo][pydantic.fields.FieldInfo] objects.

+

This replaces Model.__fields__ from Pydantic V1.

+
+ +
+
+related_issue: RelatedIssue | RelatedInternalIssue | None
+
+ +
+
+title: str | None
+
+ +
+ +
+
+class brassy.templates.release_yaml_template.DateRange(*, start: date | None, finish: date | None)[source]
+

Bases: BaseModel

+
+
+finish: date | None
+
+ +
+
+model_computed_fields: ClassVar[Dict[str, ComputedFieldInfo]] = {}
+

A dictionary of computed field names and their corresponding ComputedFieldInfo objects.

+
+ +
+
+model_config: ClassVar[ConfigDict] = {}
+

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

+
+ +
+
+model_fields: ClassVar[Dict[str, FieldInfo]] = {'finish': FieldInfo(annotation=Union[date, NoneType], required=True, validate_default=True), 'start': FieldInfo(annotation=Union[date, NoneType], required=True, validate_default=True)}
+

Metadata about the fields defined on the model, +mapping of field names to [FieldInfo][pydantic.fields.FieldInfo] objects.

+

This replaces Model.__fields__ from Pydantic V1.

+
+ +
+
+classmethod parse_date(value)[source]
+
+ +
+
+start: date | None
+
+ +
+ +
+
+class brassy.templates.release_yaml_template.Files(*, deleted: List[str] = [], moved: List[str] = [], added: List[str] = [], modified: List[str] = [])[source]
+

Bases: BaseModel

+
+
+added: List[str]
+
+ +
+
+check_at_least_one_field()[source]
+
+ +
+
+deleted: List[str]
+
+ +
+
+model_computed_fields: ClassVar[Dict[str, ComputedFieldInfo]] = {}
+

A dictionary of computed field names and their corresponding ComputedFieldInfo objects.

+
+ +
+
+model_config: ClassVar[ConfigDict] = {}
+

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

+
+ +
+
+model_fields: ClassVar[Dict[str, FieldInfo]] = {'added': FieldInfo(annotation=List[str], required=False, default=[]), 'deleted': FieldInfo(annotation=List[str], required=False, default=[]), 'modified': FieldInfo(annotation=List[str], required=False, default=[]), 'moved': FieldInfo(annotation=List[str], required=False, default=[])}
+

Metadata about the fields defined on the model, +mapping of field names to [FieldInfo][pydantic.fields.FieldInfo] objects.

+

This replaces Model.__fields__ from Pydantic V1.

+
+ +
+
+modified: List[str]
+
+ +
+
+moved: List[str]
+
+ +
+ +
+
+class brassy.templates.release_yaml_template.RelatedInternalIssue(*, string: Annotated[str | None, _PydanticGeneralMetadata(pattern='[A-Za-z]+#\\d+ - .+')] = None)[source]
+

Bases: BaseModel

+
+
+model_computed_fields: ClassVar[Dict[str, ComputedFieldInfo]] = {}
+

A dictionary of computed field names and their corresponding ComputedFieldInfo objects.

+
+ +
+
+model_config: ClassVar[ConfigDict] = {}
+

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

+
+ +
+
+model_fields: ClassVar[Dict[str, FieldInfo]] = {'string': FieldInfo(annotation=Union[str, NoneType], required=False, default=None, metadata=[_PydanticGeneralMetadata(pattern='[A-Za-z]+#\\d+ - .+')])}
+

Metadata about the fields defined on the model, +mapping of field names to [FieldInfo][pydantic.fields.FieldInfo] objects.

+

This replaces Model.__fields__ from Pydantic V1.

+
+ +
+
+string: str | None
+
+ +
+ +
+
+class brassy.templates.release_yaml_template.RelatedIssue(*, number: int | None = None, repo_url: Annotated[Url, UrlConstraints(max_length=2083, allowed_schemes=['http', 'https'], host_required=None, default_host=None, default_port=None, default_path=None)] | None = None)[source]
+

Bases: BaseModel

+
+
+blank_string(field)[source]
+
+ +
+
+model_computed_fields: ClassVar[Dict[str, ComputedFieldInfo]] = {}
+

A dictionary of computed field names and their corresponding ComputedFieldInfo objects.

+
+ +
+
+model_config: ClassVar[ConfigDict] = {}
+

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

+
+ +
+
+model_fields: ClassVar[Dict[str, FieldInfo]] = {'number': FieldInfo(annotation=Union[int, NoneType], required=False, default=None), 'repo_url': FieldInfo(annotation=Union[Annotated[Url, UrlConstraints(max_length=2083, allowed_schemes=['http', 'https'], host_required=None, default_host=None, default_port=None, default_path=None)], NoneType], required=False, default=None)}
+

Metadata about the fields defined on the model, +mapping of field names to [FieldInfo][pydantic.fields.FieldInfo] objects.

+

This replaces Model.__fields__ from Pydantic V1.

+
+ +
+
+number: int | None
+
+ +
+
+repo_url: Annotated[Url, UrlConstraints(max_length=2083, allowed_schemes=['http', 'https'], host_required=None, default_host=None, default_port=None, default_path=None)] | None
+
+ +
+ +
+
+class brassy.templates.release_yaml_template.ReleaseNote(root: RootModelRootType = PydanticUndefined)[source]
+

Bases: RootModel[Dict[str, List[ChangeItem]]]

+

ReleaseNote is a root model containing a dictionary that maps category names to lists of ChangeItems.

+
+
+model_computed_fields: ClassVar[Dict[str, ComputedFieldInfo]] = {}
+

A dictionary of computed field names and their corresponding ComputedFieldInfo objects.

+
+ +
+
+model_config: ClassVar[ConfigDict] = {}
+

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

+
+ +
+
+model_fields: ClassVar[Dict[str, FieldInfo]] = {'root': FieldInfo(annotation=Dict[str, List[ChangeItem]], required=True)}
+

Metadata about the fields defined on the model, +mapping of field names to [FieldInfo][pydantic.fields.FieldInfo] objects.

+

This replaces Model.__fields__ from Pydantic V1.

+
+ +
+ +
+
+

Settings

+
+
+class brassy.templates.settings_template.ReleaseTemplate(*, release_template: List[Dict[str, List[str]]] | None = None)[source]
+

Bases: BaseModel

+
+
+class Config[source]
+

Bases: object

+
+
+populate_by_name = True
+
+ +
+ +
+
+model_computed_fields: ClassVar[Dict[str, ComputedFieldInfo]] = {}
+

A dictionary of computed field names and their corresponding ComputedFieldInfo objects.

+
+ +
+
+model_config: ClassVar[ConfigDict] = {'populate_by_name': True}
+

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

+
+ +
+
+model_fields: ClassVar[Dict[str, FieldInfo]] = {'release_template': FieldInfo(annotation=Union[List[Dict[str, List[str]]], NoneType], required=False, default=None, alias='release-template', alias_priority=2)}
+

Metadata about the fields defined on the model, +mapping of field names to [FieldInfo][pydantic.fields.FieldInfo] objects.

+

This replaces Model.__fields__ from Pydantic V1.

+
+ +
+
+release_template: List[Dict[str, List[str]]] | None
+
+ +
+ +
+
+class brassy.templates.settings_template.SettingsTemplate(*, use_color: bool = True, default_yaml_path: Path | None = None, change_categories: List[str] = ['bug fix', 'enhancement', 'deprecation', 'removal', 'performance', 'documentation', 'continuous integration'], default_title: str = 'NO TITLE', default_description: str = 'NO DESCRIPTION', fail_on_empty_dir: bool = True, description_populates_with_pipe: bool = False, valid_fields: List[str] = ['title', 'description', 'files', 'related-issue'], valid_changes: List[str] = ['deleted', 'moved', 'added', 'modified'], enable_experimental_features: bool = False, templates: ReleaseTemplate | None = ReleaseTemplate(release_template=[{'header': ['{prefix_file}', '']}, {'title': ['', 'Version {release_version} ({release_date})', '**************************', '']}, {'summary': [' * *{change_type}*: {title}']}, {'entry': ['', '{change_type}', '===========', '', '{title}', '-------------------------', '', '{description}', '', '::', '', '     {file_change}: {file}']}, {'footer': ['', '{suffix_file}']}]))[source]
+

Bases: BaseModel

+
+
+change_categories: List[str]
+
+ +
+
+default_description: str
+
+ +
+
+default_title: str
+
+ +
+
+default_yaml_path: Path | None
+
+ +
+
+description_populates_with_pipe: bool
+
+ +
+
+enable_experimental_features: bool
+
+ +
+
+fail_on_empty_dir: bool
+
+ +
+
+model_computed_fields: ClassVar[Dict[str, ComputedFieldInfo]] = {}
+

A dictionary of computed field names and their corresponding ComputedFieldInfo objects.

+
+ +
+
+model_config: ClassVar[ConfigDict] = {}
+

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

+
+ +
+
+model_fields: ClassVar[Dict[str, FieldInfo]] = {'change_categories': FieldInfo(annotation=List[str], required=False, default=['bug fix', 'enhancement', 'deprecation', 'removal', 'performance', 'documentation', 'continuous integration']), 'default_description': FieldInfo(annotation=str, required=False, default='NO DESCRIPTION'), 'default_title': FieldInfo(annotation=str, required=False, default='NO TITLE'), 'default_yaml_path': FieldInfo(annotation=Union[Path, NoneType], required=False, default=None), 'description_populates_with_pipe': FieldInfo(annotation=bool, required=False, default=False), 'enable_experimental_features': FieldInfo(annotation=bool, required=False, default=False), 'fail_on_empty_dir': FieldInfo(annotation=bool, required=False, default=True), 'templates': FieldInfo(annotation=Union[ReleaseTemplate, NoneType], required=False, default=ReleaseTemplate(release_template=[{'header': ['{prefix_file}', '']}, {'title': ['', 'Version {release_version} ({release_date})', '**************************', '']}, {'summary': [' * *{change_type}*: {title}']}, {'entry': ['', '{change_type}', '===========', '', '{title}', '-------------------------', '', '{description}', '', '::', '', '     {file_change}: {file}']}, {'footer': ['', '{suffix_file}']}])), 'use_color': FieldInfo(annotation=bool, required=False, default=True), 'valid_changes': FieldInfo(annotation=List[str], required=False, default=['deleted', 'moved', 'added', 'modified']), 'valid_fields': FieldInfo(annotation=List[str], required=False, default=['title', 'description', 'files', 'related-issue'])}
+

Metadata about the fields defined on the model, +mapping of field names to [FieldInfo][pydantic.fields.FieldInfo] objects.

+

This replaces Model.__fields__ from Pydantic V1.

+
+ +
+
+templates: ReleaseTemplate | None
+
+ +
+
+use_color: bool
+
+ +
+
+valid_changes: List[str]
+
+ +
+
+valid_fields: List[str]
+
+ +
+ +
+
+
+

Utils

+
+

CLI

+
+
+brassy.utils.CLI.exit_on_invalid_arguments(args, parser, console)[source]
+

Validate the argparse arguments.

+

This function validates the provided argparse arguments to ensure +that the required input files/folders and output file are provided. +If arguments are invalid, it prints an error message and exits the program.

+
+
Parameters:
+
    +
  • args (argparse.Namespace) – Parsed arguments.

  • +
  • parser (argparse.ArgumentParser) – The ArgumentParser object used to parse the command-line arguments.

  • +
+
+
+
+ +
+
+brassy.utils.CLI.get_file_list_from_cli_input(input_files_or_folders, console, working_dir='.')[source]
+
+ +
+
+brassy.utils.CLI.get_parser()[source]
+

Returns an ArgumentParser object with predefined arguments for generating release notes from YAML files.

+
+
Returns:
+

The ArgumentParser object with predefined arguments.

+
+
Return type:
+

argparse.ArgumentParser

+
+
+
+ +
+
+brassy.utils.CLI.get_yaml_files_from_input(input_files_or_folders)[source]
+

Get a list of YAML files from the given input files or folders.

+
+
Parameters:
+

input_files_or_folders (list) – List of paths to input files or folders.

+
+
Returns:
+

List of paths to YAML files.

+
+
Return type:
+

list

+
+
Raises:
+

ValueError – If a file is not a YAML file or if no YAML files are found in a directory.

+
+
+
+ +
+
+brassy.utils.CLI.parse_arguments()[source]
+

Parse command line arguments for input folder and output file.

+
+
Returns:
+

Parsed arguments containing input_folder and output_file.

+
+
Return type:
+

argparse.Namespace

+
+
+
+ +
+
+brassy.utils.CLI.print_version_and_exit()[source]
+
+ +
+
+brassy.utils.CLI.run_from_CLI()[source]
+

Main function to generate release notes from YAML files and write to an output file.

+
+ +
+
+

git_handler

+
+
+brassy.utils.git_handler.get_current_git_branch()[source]
+

Get the current dirs git branch name.

+
+
Returns:
+

The name of the current git branch.

+
+
Return type:
+

str

+
+
+
+ +
+
+brassy.utils.git_handler.get_git_status(repo_path='.')[source]
+

Retrieves the status of files in the given Git repository.

+

Parameters: +repo_path (str): The path to the Git repository. Defaults to the current directory.

+

Returns: +dict: A dictionary with keys ‘added’, ‘modified’, ‘deleted’, and ‘renamed’,

+
+

each containing a list of file paths that match the respective status.

+
+
+ +
+
+brassy.utils.git_handler.print_out_git_changed_files(print_function, repo_path='.')[source]
+
+ +
+
+

Messages

+
+
+brassy.utils.messages.get_boolean_prompt_function(format=True)[source]
+
+ +
+
+brassy.utils.messages.get_rich_opener(no_format=False)[source]
+

Returns the appropriate opener function for rich progress bar.

+
+
Parameters:
+

no_format (bool, optional) – If True, returns the opener function without any formatting. +If False, returns the opener function with formatting. Defaults to False.

+
+
Returns:
+

The opener function for rich progress bar.

+
+
Return type:
+

function

+
+
+
+ +
+
+brassy.utils.messages.init_logger(use_rich)[source]
+

Initialize and configure the logger.

+
+
Parameters:
+

use_rich (bool) – If True, sets up rich logging else use standard stream logging

+
+
Returns:
+

logger – The configured logger instance

+
+
Return type:
+

logging.Logger

+
+
+
+ +
+
+brassy.utils.messages.setup_console(no_format=False, quiet=False)[source]
+

Set up and return the console for printing messages.

+
+
Parameters:
+
    +
  • no_format (bool, optional) – Whether to disable formatting. Defaults to False.

  • +
  • quiet (bool, optional) – Whether to suppress console output. Defaults to False.

  • +
+
+
Returns:
+

The configured rich console object.

+
+
Return type:
+

Console

+
+
+
+ +
+
+brassy.utils.messages.setup_messages(format, quiet)[source]
+
+ +
+
+

Settings Manager

+
+
+brassy.utils.settings_manager.create_config_file(config_file)[source]
+

Create a configuration file with default settings.

+
+
Parameters:
+

config_file (str) – Path where the configuration file will be created.

+
+
+
+ +
+
+brassy.utils.settings_manager.get_config_files(app_name)[source]
+

Get a list of configuration file paths in order of increasing precedence.

+
+
Parameters:
+

app_name (str) – Name of the application.

+
+
Returns:
+

List of configuration file paths.

+
+
Return type:
+

list of str

+
+
+
+ +
+
+brassy.utils.settings_manager.get_git_repo_root(path='.')[source]
+

Get the root directory of the Git repository containing the given path.

+
+
Parameters:
+

path (str, optional) – A path within the Git repository. Defaults to the current directory.

+
+
Returns:
+

Absolute path to the root of the Git repository. This is usually the +path containing the .git folder.

+
+
Return type:
+

str

+
+
+
+ +
+
+brassy.utils.settings_manager.get_project_config_file_path(app_name)[source]
+

Retrieve the project-specific configuration file path for the application.

+
+
Parameters:
+

app_name (str) – Name of the application.

+
+
Returns:
+

Path to the project’s configuration file.

+
+
Return type:
+

str

+
+
+
+ +
+
+brassy.utils.settings_manager.get_settings(app_name)[source]
+

Return application settings from config files and environment variables.

+
+
Parameters:
+

app_name (str) – Name of the application.

+
+
Returns:
+

An instance of the Settings model with all configurations applied.

+
+
Return type:
+

Settings

+
+
Raises:
+

ValidationError – If the final settings do not conform to the Settings model.

+
+
+
+ +
+
+brassy.utils.settings_manager.get_settings_from_config_files(app_name)[source]
+

Retrieve settings from configuration files without environment overrides.

+
+
Parameters:
+

app_name (str) – Name of the application.

+
+
Returns:
+

Configuration settings merged from files.

+
+
Return type:
+

dict

+
+
+
+ +
+
+brassy.utils.settings_manager.get_site_config_file_path(app_name)[source]
+

Retrieve the site-specific configuration file path for the application.

+
+
Parameters:
+

app_name (str) – Name of the application.

+
+
Returns:
+

Path to the site’s configuration file.

+
+
Return type:
+

str

+
+
+
+ +
+
+brassy.utils.settings_manager.get_user_config_file_path(app_name)[source]
+

Retrieve the user-specific configuration file path for the application.

+
+
Parameters:
+

app_name (str) – Name of the application.

+
+
Returns:
+

Path to the user’s configuration file.

+
+
Return type:
+

str

+
+
+
+ +
+
+brassy.utils.settings_manager.merge_and_validate_config_files(config_files)[source]
+

Merge settings from multiple configuration files and validate them.

+
+
Parameters:
+

config_files (list of str) – List of configuration file paths. The order of the files matters. +Each file overwrites the values of the previous.

+
+
Returns:
+

Merged and validated configuration settings.

+
+
Return type:
+

dict

+
+
Raises:
+

ValidationError – If any of the settings do not conform to the Settings model.

+
+
+
+ +
+
+brassy.utils.settings_manager.override_dict_with_environmental_variables(input_dict)[source]
+

Override dict values with case insensitive environment variables when available.

+
+
Parameters:
+

input_dict (dict) – Original settings dictionary.

+
+
Returns:
+

Updated settings dictionary with environment variable overrides.

+
+
Return type:
+

dict

+
+
+
+ +
+
+brassy.utils.settings_manager.read_config_file(config_file, create_file_if_not_exist=False)[source]
+

Read and parse a YAML configuration file.

+
+
Parameters:
+
    +
  • config_file (str) – Path to the configuration file.

  • +
  • create_file_if_not_exist (bool) – Creates file if it doesn’t exist

  • +
+
+
Returns:
+

Parsed configuration settings.

+
+
Return type:
+

dict

+
+
+
+ +
+
+

File Handler

+
+
+brassy.utils.file_handler.create_blank_template_yaml_file(file_path_arg, console, working_dir='.')[source]
+

Creates a blank YAML template file with a predefined structure.

+

This function generates a YAML file at the specified path with a default +template. It handles special characters required for YAML compatibility and writes +the file to disk.

+
+
Parameters:
+
    +
  • file_path_arg (str) – The file path of the YAML template as passed via the CLI.

  • +
  • console (rich.console.Console) – A Rich Console object used for displaying messages and errors to the user.

  • +
  • working_dir (str, optional) – The working directory path. Defaults to the current directory “.”.

  • +
+
+
Raises:
+

SystemExit – If a Git repo is not found in the current working directory and no file path + is provided, the program exits with an error message.

+
+
+

Notes

+

This function performs a string replacement to insert a “|” due to an issue with +YAML’s handling of pipe symbols. For more details, see: +https://github.com/yaml/pyyaml/pull/822

+
+ +
+
+brassy.utils.file_handler.get_yaml_template_path(file_path_arg, working_dir='/Users/gwynu/Documents/yaml-to-release-note/sphinx')[source]
+

Returns the path of the YAML template file based on the given file path argument.

+
+
Parameters:
+

file_path_arg (str) – The file path argument provided by the user.

+
+
Returns:
+

The path of the YAML template file.

+
+
Return type:
+

str

+
+
+
+ +
+
+brassy.utils.file_handler.read_yaml_files(input_files, rich_open)[source]
+

Read and parse the given list of YAML files.

+
+
Parameters:
+

input_files (list) – List of paths to the YAML files.

+
+
Returns:
+

Parsed content of all YAML files categorized by type of change.

+
+
Return type:
+

dict

+
+
+

Examples

+
>>> read_yaml_files(["file1.yaml", "file2.yaml"])
+{'bug-fix': [
+    {'title': 'fixed explosions',
+      'description': 'This fixed the explosion mechanism'},
+    {'title': 'fixed cats not being cute',
+      'description': 'This made the cats WAY cuter'}
+    ]
+}
+
+
+
+ +
+
+brassy.utils.file_handler.value_error_on_invalid_yaml(content, file_path)[source]
+

Check if the YAML content follows the correct schema.

+
+
Parameters:
+
    +
  • content (dict) – Parsed content of the YAML file.

  • +
  • file_path (str) – Path to the YAML file.

  • +
+
+
Raises:
+

ValueError – If the YAML content does not follow the correct schema.

+
+
+
+ +
+
+brassy.utils.file_handler.write_output_file(output_file, content)[source]
+

Write the formatted release notes to the output file.

+
+
Parameters:
+
    +
  • output_file (str) – Path to the output .rst file.

  • +
  • content (str) – Formatted release notes.

  • +
+
+
+
+ +
+
+
+

Main Module

+

This module provides functionality to generate release notes from YAML files. +It reads YAML files, parses their content, and formats the parsed data into release notes in .rst format. +The release notes can be written to an output file.

+
+
+brassy.brassy.run_from_CLI()[source]
+
+ +
@@ -103,7 +1138,7 @@

Code Docstrings +

-
@@ -67,7 +73,7 @@

Indices and tables +

-
-

Adding changed files

-

To add what files have been changed as part of your edit you must edit the -files section. For example:

-
enhancement:
-    - description: |
-        --output-to-console now writes generated release notes to the console.
-        This is disabled by default.
-    files:
-        modified:
-        - 'src/brassy/brassy.py'
+  description: |
+  files:
+    deleted:
+    - 'die-on-thoughts.py'
+    moved:
+    - ''
+    added:
+    - ''
+    modified:
+    - 'main.py'
+  related-issue:
+    number: 1938
+    repo_url: 'http://github.com/fake/repo'
+  date:
+    start: "10-10-1999"
+    finish: "02-21-2026"
 
@@ -91,12 +114,16 @@

Generating the changed files via branch. It runs on the current directory by default, but it accepts a path as an argument.

For example, the output looks like this:

-
brassy --get-changed-files
+
brassy --get-changed-files
 
-    added: test
-    modified: test2
-    deleted: test3
-    moved: test4
+    added:
+    - test.py
+    modified:
+    - test2.js
+    deleted:
+    - test3.cpp
+    moved:
+    - test4.fortran
 

It prints with indents for easy copy-and-pasting into your yaml files.

@@ -107,7 +134,8 @@

Generating the changed files via

Generate release notes

Once you have filled out your yaml template, you can generate release notes with the following command:

-
brassy -o new-release-note.rst release-note.yaml
+
brassy --output-file new-release-note.rst release-note.yaml
+brassy -o new-release-note.rst release-note.yaml
 

For example, if release-note.yaml contains the following:

@@ -125,78 +153,81 @@

Generate release notes

The output will be:

-
Version [UNKNOWN] (2024-09-17)
-******************************
+
Version [UNKNOWN] (2024-10-22)
+**************************
 
  * *Bug fix*: Fixed a bug
 
 Bug fix
-=======
+===========
 
 Fixed a bug
------------
+-------------------------
 
 This bug was really annoying
 
 ::
 
-    added: new-test.py
-    deleted: old/test.py
+     added: new-test.py
+     deleted: old/test.py
 

Specifying Version

-

You can specify the version of the release notes by using the -r or --release-version flag. -For example, using the previous yaml file:

+

You can specify the version of the release notes by using the +--release-version or -r flag.

+

For example, using the previous yaml file:

brassy -o new-release-note.rst release-note.yaml -r 1.0.0
 

Which would output:

-
Version 1.0.0 (2024-09-17)
+
Version 1.0.0 (2024-10-22)
 **************************
 
  * *Bug fix*: Fixed a bug
 
 Bug fix
-=======
+===========
 
 Fixed a bug
------------
+-------------------------
 
 This bug was really annoying
 
 ::
 
-    added: new-test.py
-    deleted: old/test.py
+     added: new-test.py
+     deleted: old/test.py
 

Specifying Date

-

You can specify the date of the release notes by using the -d or --release-date flag.

+

By default, brassy uses todays date in YYYY-MM-DD format.

+

You can specify the date of the release notes in any format +with the -d or --release-date flag.

For example, using the previous yaml file:

brassy -o new-release-note.rst release-note.yaml -d 3000-30-30
 

Which would output:

-
Version [UNKNOWN] (2024-09-17)
-******************************
+
Version [UNKNOWN] (3000-30-30)
+**************************
 
  * *Bug fix*: Fixed a bug
 
 Bug fix
-=======
+===========
 
 Fixed a bug
------------
+-------------------------
 
 This bug was really annoying
 
 ::
 
-    added: new-test.py
-    deleted: old/test.py
+     added: new-test.py
+     deleted: old/test.py
 
@@ -238,23 +269,25 @@

Adding Headers and/or Footers +

Change YAML directory

+

By default brassy works in your current working directory.

+

You can specify a directory with --yaml-dir or -yd.

+

For example:

+
brassy --yaml-dir ./docs/release-notes/v1.0.0 \
+       --write-template "updating-gpu-code"
+
+
+

would write a template file updating-gpu-code.yaml +to ./docs/release-notes/v1.0.0.

+

+
+

Prune YAML file

+

Brassy can “prune” yaml files by removing blank sections. Sections are considered blank +if all of their items are blank OR are empty lists.

+

For example:

+
bug fix:
+- title: 'Real issue'
+  description: 'Questionably real description.'
+  files:
+    deleted:
+    - ''
+    moved:
+    - ''
+    added:
+    - ''
+    modified:
+    - 'fake_file.py'
+  related-issue:
+    number: 100000
+    repo_url: ''
+  date:
+    start: null
+    finish: null
+
+
+

would become

+
bug fix:
+- title: Real issue
+  description: Questionably real description.
+  files:
+    modified:
+    - fake_file.py
+  related-issue:
+    number: 100000
+
+
+

after pruning.

+

To prune a file, pass it to brassy with --prune. +Eg. brassy --prune fake_file.yaml

+
-

Controlling CLI Output

-

You can turn off fancy formatting (colors, bold, etc.) by using the --no-color/-nc flag.

-

You can also turn off ALL non-error outputs by using the --quiet/-q flag.

+

Controlling CLI Output

+

You can turn off fancy formatting (colors, bold, etc.) +by using the --no-color/-nc flag.

+

You can also turn off all non-error outputs by using the --quiet or -q flag.

-

Help!

+

Help!

When in doubt, you can always run the help command to see what options are available:

brassy --help
 
@@ -286,8 +373,8 @@

Help!<

Which outputs:

Usage: brassy [-h] [-t [WRITE_YAML_TEMPLATE]] [-c [GET_CHANGED_FILES]]
               [-r VERSION] [-d RELEASE_DATE] [-nc] [-p PREFIX_FILE]
-              [-s SUFFIX_FILE] [-o OUTPUT_FILE] [--output-to-console] [-nr]
-              [-q]
+              [-s SUFFIX_FILE] [-o OUTPUT_FILE] [-yd YAML_DIR]
+              [--output-to-console] [-nr] [-q] [-pr] [--init] [--version]
               [input_files_or_folders ...]
 
 Generate release notes from YAML files. Entries are sorted by order in yaml
@@ -319,13 +406,17 @@ 

Help!< A footer file to suffix to the release notes. -o, --output-file OUTPUT_FILE The output file for release notes. + -yd, --yaml-dir YAML_DIR + Directory to write yaml files to --output-to-console Write generated release notes to console. -nr, --no-rich Disable rich text output -q, --quiet Only output errors + -pr, --prune Prune provided yaml file(s) of empty sections + --init Initialize brassy and generate config files + --version Print program version and exit

-
@@ -333,7 +424,7 @@

Help!< -