From fb3f1a39af6eb17b33fd09946fe67a1527aa3a53 Mon Sep 17 00:00:00 2001
From: Ritchie Mwewa <74001397+rly0nheart@users.noreply.github.com>
Date: Fri, 23 Jan 2026 23:16:48 +0200
Subject: [PATCH] 5.0 beta
---
LICENSE | 2 +-
README.md | 194 ++++++------
pyproject.toml | 12 +-
src/octosuite/__init__.py | 7 +-
src/octosuite/_app.py | 20 ++
src/octosuite/_cli.py | 474 ++++++++++++++++++++++++++++++
src/octosuite/{lib.py => _lib.py} | 0
src/octosuite/app.py | 16 -
src/octosuite/core/github.py | 2 +-
src/octosuite/tui/__init__.py | 12 +
src/octosuite/tui/menus.py | 4 +-
uv.lock | 38 +--
12 files changed, 641 insertions(+), 140 deletions(-)
create mode 100644 src/octosuite/_app.py
create mode 100644 src/octosuite/_cli.py
rename src/octosuite/{lib.py => _lib.py} (100%)
delete mode 100644 src/octosuite/app.py
diff --git a/LICENSE b/LICENSE
index 0a1aea6..7a719d1 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2025 Bellingcat
+Copyright (c) 2026 Bellingcat
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 83fca42..dead859 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@

-TUI-based toolkit for GitHub data analysis.
+Terminal-based toolkit for GitHub data analysis.


@@ -9,113 +9,115 @@ TUI-based toolkit for GitHub data analysis.


-## Overview
-
-OctoSuite provides a terminal interface for exploring and exporting GitHub data. Access information about users,
-repositories, organizations, and search across GitHub's platform.
-
-## Features
-
-
-See details
-
-- **User** - Get user data
- - Profile
- - Repositories
- - Subscriptions
- - Starred
- - Followers
- - Following
- - Organizations
- - Gists
- - Events
- - Received Events
-- **Repository** - Get repository data
- - Profile
- - Forks
- - Issues
- - Issue Events
- - Events
- - Assignees
- - Branches
- - Tags
- - Languages
- - Stargazers
- - Subscribers
- - Commits
- - Comments
- - Releases
- - Deployments
- - Labels
-- **Organisation** - Get organisation data
- - Profile
- - Repositories
- - Events
- - Hooks
- - Issues
- - Members
-- **Search** - Search GitHub
- - Repositories
- - Users
- - Commits
- - Issues
- - Topics
-- **Export** - Export data
- - JSON
- - CSV
- - HTML
-
-
-
## Installation
-### PyPI
-
```bash
pip install octosuite
```
-### Build from source
-
-```bash
-# Clone repository
-git clone https://github.com/bellingcat/octosuite.git
-
-# Move to octosuite directory
-cd octosuite
-
-# Build and install (uses uv)
-make install
-
-# If you dont have uv installed, you can install directly with pip:
-pip install .
-
-# Run
-octosuite
-```
-
-> [!Note]
-> You can then run octosuite with command `octosuite`
-
## Usage
-Navigate using UPDOWN and ENTER to select options. In the export menu, you should
-use SPACE to check the format you want.
+### TUI (Interactive)
-The interface guides you through
-selecting a
-data source
-and
-choosing what information to retrieve. Preview the results and optionally export them in your preferred format.
+Launch the interactive terminal interface:
-
+```bash
+octosuite -t/--tui
+```
-## License
+Navigate using arrow keys and Enter to select options.
-### MIT License
+### CLI
-See the LICENSE file for details. License information is also available through the application's main menu.
+Query GitHub data directly from the command line:
-## Contributing
+```bash
+# User data
+octosuite user torvalds
+octosuite user torvalds --repos --page 1 --per-page 50
+octosuite user torvalds --followers --json
-Contributions are welcome. Please submit pull requests or open issues for bugs and feature requests. Good luck!
\ No newline at end of file
+# Repository data
+octosuite repo torvalds/linux
+octosuite repo torvalds/linux --commits
+octosuite repo torvalds/linux --stargazers --export ./data
+
+# Organisation data
+octosuite org github
+octosuite org github --members --json
+
+# Search
+octosuite search "machine learning" --repos
+octosuite search "python cli" --users --json
+```
+
+**Common options:**
+
+- `--page` - Page number (default: 1)
+- `--per-page` - Results per page, max 100 (default: 100)
+- `--json` - Output as JSON
+- `--export DIR` - Export to directory
+
+Run `octosuite --help` for available data type flags.
+
+### Library
+
+Use octosuite in your Python projects:
+
+```python
+from octosuite import User, Repo, Org, Search
+
+# Get user data
+user = User("torvalds")
+exists, profile = user.exists()
+if exists:
+ repos = user.repos(page=1, per_page=100)
+ followers = user.followers(page=1, per_page=50)
+
+# Get repository data
+repo = Repo(name="linux", owner="torvalds")
+exists, profile = repo.exists()
+if exists:
+ commits = repo.commits(page=1, per_page=100)
+ languages = repo.languages()
+
+# Get organisation data
+org = Org("github")
+exists, profile = org.exists()
+if exists:
+ members = org.members(page=1, per_page=100)
+
+# Search GitHub
+search = Search(query="machine learning", page=1, per_page=50)
+results = search.repos()
+```
+
+## Features
+
+
+Data Types
+
+**User:** profile, repos, subscriptions, starred, followers, following, orgs, gists, events, received_events
+
+**Repository:** profile, forks, issue_events, events, assignees, branches, tags, languages, stargazers, subscribers,
+commits, comments, issues, releases, deployments, labels
+
+**Organisation:** profile, repos, events, hooks, issues, members
+
+**Search:** repos, users, commits, issues, topics
+
+
+
+
+Export Formats
+
+- JSON
+- CSV
+- HTML
+
+
+
+## Licence
+
+MIT Licence. See the [LICENCE](https://raw.githubusercontent.com/bellingcat/octosuite/refs/heads/master/LICENSE) file
+for details.
diff --git a/pyproject.toml b/pyproject.toml
index 9b994d9..51333eb 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,7 +1,7 @@
[project]
name = "octosuite"
-version = "4.0.0"
-description = "TUI-based toolkit for GitHub data analysis."
+version = "5.0.0"
+description = "Terminal-based toolkit for GitHub data analysis."
readme = "README.md"
license = "MIT"
authors = [
@@ -30,8 +30,12 @@ repository = "https://github.com/bellingcat/octosuite"
[project.optional-dependencies]
dev = [
- "black>=25.12.0",
+ "black>=26.1.0",
]
[project.scripts]
-octosuite = "octosuite.app:start"
\ No newline at end of file
+ocs = "octosuite._app:start"
+octosuite = "octosuite.app:start"
+
+[tool.uv]
+package = true
\ No newline at end of file
diff --git a/src/octosuite/__init__.py b/src/octosuite/__init__.py
index da6d8df..8a6eae9 100644
--- a/src/octosuite/__init__.py
+++ b/src/octosuite/__init__.py
@@ -1,2 +1,7 @@
__pkg__ = "octosuite"
-__version__ = "4.0.0"
+__version__ = "5.0.0"
+
+from .core.cache import cache
+from .core.models import User, Org, Repo, Search
+
+__all__ = ["User", "Org", "Repo", "Search", "cache", "__pkg__", "__version__"]
diff --git a/src/octosuite/_app.py b/src/octosuite/_app.py
new file mode 100644
index 0000000..ce9a22f
--- /dev/null
+++ b/src/octosuite/_app.py
@@ -0,0 +1,20 @@
+import sys
+
+from ._lib import console, __pkg__, __version__
+from .core.cache import cache
+
+
+def start():
+ """Entry point for octosuite."""
+
+ try:
+ console.set_window_title(title=f"{__pkg__.title()} CLI v{__version__}")
+
+ from ._cli import run
+
+ run()
+
+ except KeyboardInterrupt:
+ sys.exit()
+ finally:
+ cache.clear()
diff --git a/src/octosuite/_cli.py b/src/octosuite/_cli.py
new file mode 100644
index 0000000..940ddb3
--- /dev/null
+++ b/src/octosuite/_cli.py
@@ -0,0 +1,474 @@
+"""
+Command-line interface for octosuite.
+
+Provides non-interactive access to GitHub data.
+"""
+
+import argparse
+import json
+import sys
+import typing as t
+
+from rich.status import Status
+
+from . import __pkg__, __version__
+from ._lib import export_response, preview_response, console
+from .core.models import User, Org, Repo, Search
+
+
+def create_parser() -> argparse.ArgumentParser:
+ """
+ Create the argument parser.
+
+ :return: Configured ArgumentParser.
+ """
+
+ parser = argparse.ArgumentParser(
+ prog=__pkg__,
+ description="Terminal-based toolkit for GitHub data analysis.",
+ )
+
+ parser.add_argument(
+ "-v", "--version", action="version", version=f"%(prog)s {__version__}"
+ )
+ parser.add_argument(
+ "-t", "--tui", action="store_true", help="launch interactive TUI"
+ )
+
+ subparsers = parser.add_subparsers(dest="command")
+
+ # User command
+ user_parser = subparsers.add_parser("user", help="get user data")
+ user_parser.add_argument("username", help="GitHub username")
+ user_group = user_parser.add_mutually_exclusive_group()
+ user_group.add_argument(
+ "--profile",
+ action="store_const",
+ const="profile",
+ dest="data_type",
+ help="profile data (default)",
+ )
+ user_group.add_argument(
+ "--repos",
+ action="store_const",
+ const="repos",
+ dest="data_type",
+ help="repositories",
+ )
+ user_group.add_argument(
+ "--subscriptions",
+ action="store_const",
+ const="subscriptions",
+ dest="data_type",
+ help="subscriptions",
+ )
+ user_group.add_argument(
+ "--starred",
+ action="store_const",
+ const="starred",
+ dest="data_type",
+ help="starred repos",
+ )
+ user_group.add_argument(
+ "--followers",
+ action="store_const",
+ const="followers",
+ dest="data_type",
+ help="followers",
+ )
+ user_group.add_argument(
+ "--following",
+ action="store_const",
+ const="following",
+ dest="data_type",
+ help="following",
+ )
+ user_group.add_argument(
+ "--orgs",
+ action="store_const",
+ const="orgs",
+ dest="data_type",
+ help="organisations",
+ )
+ user_group.add_argument(
+ "--gists", action="store_const", const="gists", dest="data_type", help="gists"
+ )
+ user_group.add_argument(
+ "--events",
+ action="store_const",
+ const="events",
+ dest="data_type",
+ help="events",
+ )
+ user_group.add_argument(
+ "--received-events",
+ action="store_const",
+ const="received_events",
+ dest="data_type",
+ help="received events",
+ )
+ user_parser.set_defaults(data_type="profile")
+ user_parser.add_argument("--page", type=int, default=1)
+ user_parser.add_argument("--per-page", type=int, default=100)
+ user_parser.add_argument("--json", action="store_true", help="output as JSON")
+ user_parser.add_argument("--export", metavar="DIR", help="export to directory")
+
+ # Repo command
+ repo_parser = subparsers.add_parser("repo", help="get repository data")
+ repo_parser.add_argument("repository", help="repository (owner/name)")
+ repo_group = repo_parser.add_mutually_exclusive_group()
+ repo_group.add_argument(
+ "--profile",
+ action="store_const",
+ const="profile",
+ dest="data_type",
+ help="repo data (default)",
+ )
+ repo_group.add_argument(
+ "--forks", action="store_const", const="forks", dest="data_type", help="forks"
+ )
+ repo_group.add_argument(
+ "--issue-events",
+ action="store_const",
+ const="issue_events",
+ dest="data_type",
+ help="issue events",
+ )
+ repo_group.add_argument(
+ "--events",
+ action="store_const",
+ const="events",
+ dest="data_type",
+ help="events",
+ )
+ repo_group.add_argument(
+ "--assignees",
+ action="store_const",
+ const="assignees",
+ dest="data_type",
+ help="assignees",
+ )
+ repo_group.add_argument(
+ "--branches",
+ action="store_const",
+ const="branches",
+ dest="data_type",
+ help="branches",
+ )
+ repo_group.add_argument(
+ "--tags", action="store_const", const="tags", dest="data_type", help="tags"
+ )
+ repo_group.add_argument(
+ "--languages",
+ action="store_const",
+ const="languages",
+ dest="data_type",
+ help="languages",
+ )
+ repo_group.add_argument(
+ "--stargazers",
+ action="store_const",
+ const="stargazers",
+ dest="data_type",
+ help="stargazers",
+ )
+ repo_group.add_argument(
+ "--subscribers",
+ action="store_const",
+ const="subscribers",
+ dest="data_type",
+ help="subscribers",
+ )
+ repo_group.add_argument(
+ "--commits",
+ action="store_const",
+ const="commits",
+ dest="data_type",
+ help="commits",
+ )
+ repo_group.add_argument(
+ "--comments",
+ action="store_const",
+ const="comments",
+ dest="data_type",
+ help="comments",
+ )
+ repo_group.add_argument(
+ "--issues",
+ action="store_const",
+ const="issues",
+ dest="data_type",
+ help="issues",
+ )
+ repo_group.add_argument(
+ "--releases",
+ action="store_const",
+ const="releases",
+ dest="data_type",
+ help="releases",
+ )
+ repo_group.add_argument(
+ "--deployments",
+ action="store_const",
+ const="deployments",
+ dest="data_type",
+ help="deployments",
+ )
+ repo_group.add_argument(
+ "--labels",
+ action="store_const",
+ const="labels",
+ dest="data_type",
+ help="labels",
+ )
+ repo_parser.set_defaults(data_type="profile")
+ repo_parser.add_argument("--page", type=int, default=1)
+ repo_parser.add_argument("--per-page", type=int, default=100)
+ repo_parser.add_argument("--json", action="store_true", help="output as JSON")
+ repo_parser.add_argument("--export", metavar="DIR", help="export to directory")
+
+ # Org command
+ org_parser = subparsers.add_parser("org", help="get organisation data")
+ org_parser.add_argument("name", help="organisation name")
+ org_group = org_parser.add_mutually_exclusive_group()
+ org_group.add_argument(
+ "--profile",
+ action="store_const",
+ const="profile",
+ dest="data_type",
+ help="profile data (default)",
+ )
+ org_group.add_argument(
+ "--repos",
+ action="store_const",
+ const="repos",
+ dest="data_type",
+ help="repositories",
+ )
+ org_group.add_argument(
+ "--events",
+ action="store_const",
+ const="events",
+ dest="data_type",
+ help="events",
+ )
+ org_group.add_argument(
+ "--hooks",
+ action="store_const",
+ const="hooks",
+ dest="data_type",
+ help="webhooks",
+ )
+ org_group.add_argument(
+ "--issues",
+ action="store_const",
+ const="issues",
+ dest="data_type",
+ help="issues",
+ )
+ org_group.add_argument(
+ "--members",
+ action="store_const",
+ const="members",
+ dest="data_type",
+ help="members",
+ )
+ org_parser.set_defaults(data_type="profile")
+ org_parser.add_argument("--page", type=int, default=1)
+ org_parser.add_argument("--per-page", type=int, default=100)
+ org_parser.add_argument("--json", action="store_true", help="output as JSON")
+ org_parser.add_argument("--export", metavar="DIR", help="export to directory")
+
+ # Search command
+ search_parser = subparsers.add_parser("search", help="search GitHub")
+ search_parser.add_argument("query", help="search query")
+ search_group = search_parser.add_mutually_exclusive_group()
+ search_group.add_argument(
+ "--repos",
+ action="store_const",
+ const="repos",
+ dest="search_type",
+ help="search repositories (default)",
+ )
+ search_group.add_argument(
+ "--users",
+ action="store_const",
+ const="users",
+ dest="search_type",
+ help="search users",
+ )
+ search_group.add_argument(
+ "--commits",
+ action="store_const",
+ const="commits",
+ dest="search_type",
+ help="search commits",
+ )
+ search_group.add_argument(
+ "--issues",
+ action="store_const",
+ const="issues",
+ dest="search_type",
+ help="search issues",
+ )
+ search_group.add_argument(
+ "--topics",
+ action="store_const",
+ const="topics",
+ dest="search_type",
+ help="search topics",
+ )
+ search_parser.set_defaults(search_type="repos")
+ search_parser.add_argument("--page", type=int, default=1)
+ search_parser.add_argument("--per-page", type=int, default=100)
+ search_parser.add_argument("--json", action="store_true", help="output as JSON")
+ search_parser.add_argument("--export", metavar="DIR", help="export to directory")
+
+ return parser
+
+
+def output(
+ data: t.Union[dict, list],
+ as_json: bool,
+ source: str,
+ data_type: str,
+ export_dir: t.Optional[str] = None,
+):
+ """
+ Output data in the appropriate format.
+
+ :param data: Data to output.
+ :param as_json: Output as JSON.
+ :param source: Source identifier.
+ :param data_type: Type of data.
+ :param export_dir: Export directory.
+ """
+
+ if not data:
+ console.print(f"[yellow]No {data_type} data found for '{source}'[/yellow]")
+ return
+
+ if export_dir:
+ export_response(
+ data=data,
+ data_type=data_type,
+ source=source,
+ file_formats=["json"],
+ output_dir=export_dir,
+ )
+ elif as_json:
+ print(json.dumps(data, indent=2, ensure_ascii=False))
+ else:
+ preview_response(data=data, source=source, _type=data_type)
+
+
+def run():
+ """Run the CLI."""
+
+ parser = create_parser()
+ args = parser.parse_args()
+
+ if args.tui:
+ from .tui import run as run_tui
+
+ run_tui()
+ return
+
+ if not args.command:
+ parser.print_help()
+ return
+
+ try:
+ if args.command == "user":
+ user = User(name=args.username)
+
+ with Status(
+ f"[dim]Validating user ({args.username})[/dim]…", console=console
+ ):
+ exists, _ = user.exists()
+
+ if not exists:
+ console.print(f"[red]User '{args.username}' not found[/red]")
+ sys.exit(1)
+
+ method = getattr(user, args.data_type)
+ with Status(
+ f"[dim]Getting {args.data_type} from {args.username}[/dim]…",
+ console=console,
+ ):
+ data = (
+ method()
+ if args.data_type == "profile"
+ else method(page=args.page, per_page=min(args.per_page, 100))
+ )
+ output(data, args.json, args.username, args.data_type, args.export)
+
+ elif args.command == "repo":
+ if "/" not in args.repository:
+ console.print("[red]Repository must be in 'owner/name' format[/red]")
+ sys.exit(1)
+
+ owner, name = args.repository.split("/", 1)
+ repo = Repo(name=name, owner=owner)
+
+ with Status(
+ f"[dim]Validating repo ({args.repository})[/dim]…", console=console
+ ):
+ exists, _ = repo.exists()
+
+ if not exists:
+ console.print(f"[red]Repository '{args.repository}' not found[/red]")
+ sys.exit(1)
+
+ method = getattr(repo, args.data_type)
+ with Status(
+ f"[dim]Getting {args.data_type} from {args.repository}[/dim]…",
+ console=console,
+ ):
+ data = (
+ method()
+ if args.data_type in ("profile", "languages")
+ else method(page=args.page, per_page=min(args.per_page, 100))
+ )
+ output(data, args.json, args.repository, args.data_type, args.export)
+
+ elif args.command == "org":
+ org = Org(name=args.name)
+
+ with Status(f"[dim]Validating org ({args.name})[/dim]…", console=console):
+ exists, _ = org.exists()
+
+ if not exists:
+ console.print(f"[red]Organisation '{args.name}' not found[/red]")
+ sys.exit(1)
+
+ method = getattr(org, args.data_type)
+ with Status(
+ f"[dim]Getting {args.data_type} from {args.name}[/dim]…",
+ console=console,
+ ):
+ data = (
+ method()
+ if args.data_type == "profile"
+ else method(page=args.page, per_page=min(args.per_page, 100))
+ )
+ output(data, args.json, args.name, args.data_type, args.export)
+
+ elif args.command == "search":
+ search = Search(
+ query=args.query,
+ page=args.page,
+ per_page=min(args.per_page, 100),
+ )
+ method = getattr(search, args.search_type)
+ with Status(
+ f"[dim]Searching {args.search_type} for '{args.query}'[/dim]…",
+ console=console,
+ ):
+ result = method()
+ data = result.get("items", result) if isinstance(result, dict) else result
+ output(data, args.json, args.query, args.search_type, args.export)
+
+ except KeyboardInterrupt:
+ console.print("\n[dim]Cancelled[/dim]")
+ sys.exit(130)
diff --git a/src/octosuite/lib.py b/src/octosuite/_lib.py
similarity index 100%
rename from src/octosuite/lib.py
rename to src/octosuite/_lib.py
diff --git a/src/octosuite/app.py b/src/octosuite/app.py
deleted file mode 100644
index ee834c3..0000000
--- a/src/octosuite/app.py
+++ /dev/null
@@ -1,16 +0,0 @@
-import sys
-
-from .core.cache import cache
-from .lib import console, __pkg__, __version__
-from .tui.menus import Menus
-
-
-def start():
- try:
- console.set_window_title(title=f"{__pkg__.title()} v{__version__}")
- menu = Menus()
- menu.main()
- except KeyboardInterrupt:
- sys.exit()
- finally:
- cache.clear()
diff --git a/src/octosuite/core/github.py b/src/octosuite/core/github.py
index d3e993c..a2a6ce8 100644
--- a/src/octosuite/core/github.py
+++ b/src/octosuite/core/github.py
@@ -6,7 +6,7 @@ import requests
from requests import Response
from .cache import cache
-from ..lib import __version__
+from .._lib import __version__
BASE_URL = "https://api.github.com"
diff --git a/src/octosuite/tui/__init__.py b/src/octosuite/tui/__init__.py
index e69de29..360d6da 100644
--- a/src/octosuite/tui/__init__.py
+++ b/src/octosuite/tui/__init__.py
@@ -0,0 +1,12 @@
+"""Terminal user interface for octosuite."""
+
+from .menus import Menus
+
+__all__ = ["run", "Menus"]
+
+
+def run():
+ """Run the interactive TUI."""
+
+ menu = Menus()
+ menu.main()
diff --git a/src/octosuite/tui/menus.py b/src/octosuite/tui/menus.py
index 385347c..6e7308e 100644
--- a/src/octosuite/tui/menus.py
+++ b/src/octosuite/tui/menus.py
@@ -7,10 +7,10 @@ from rich.status import Status
from .dialogs import Dialogs
from .prompts import Prompts
+from .._lib import check_updates, preview_response, export_response, set_menu_title
+from .._lib import console, clear_screen, ascii_banner
from ..core.cache import cache
from ..core.models import User, Repo, Org, Search
-from ..lib import check_updates, preview_response, export_response, set_menu_title
-from ..lib import console, clear_screen, ascii_banner
CUSTOM_STYLE = Style(
[
diff --git a/uv.lock b/uv.lock
index aa90611..8895fb7 100644
--- a/uv.lock
+++ b/uv.lock
@@ -4,7 +4,7 @@ requires-python = ">=3.13"
[[package]]
name = "black"
-version = "25.12.0"
+version = "26.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
@@ -14,19 +14,19 @@ dependencies = [
{ name = "platformdirs" },
{ name = "pytokens" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/c4/d9/07b458a3f1c525ac392b5edc6b191ff140b596f9d77092429417a54e249d/black-25.12.0.tar.gz", hash = "sha256:8d3dd9cea14bff7ddc0eb243c811cdb1a011ebb4800a5f0335a01a68654796a7", size = 659264, upload-time = "2025-12-08T01:40:52.501Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/13/88/560b11e521c522440af991d46848a2bde64b5f7202ec14e1f46f9509d328/black-26.1.0.tar.gz", hash = "sha256:d294ac3340eef9c9eb5d29288e96dc719ff269a88e27b396340459dd85da4c58", size = 658785, upload-time = "2026-01-18T04:50:11.993Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/c8/52/c551e36bc95495d2aa1a37d50566267aa47608c81a53f91daa809e03293f/black-25.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a05ddeb656534c3e27a05a29196c962877c83fa5503db89e68857d1161ad08a5", size = 1923809, upload-time = "2025-12-08T01:46:55.126Z" },
- { url = "https://files.pythonhosted.org/packages/a0/f7/aac9b014140ee56d247e707af8db0aae2e9efc28d4a8aba92d0abd7ae9d1/black-25.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9ec77439ef3e34896995503865a85732c94396edcc739f302c5673a2315e1e7f", size = 1742384, upload-time = "2025-12-08T01:49:37.022Z" },
- { url = "https://files.pythonhosted.org/packages/74/98/38aaa018b2ab06a863974c12b14a6266badc192b20603a81b738c47e902e/black-25.12.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e509c858adf63aa61d908061b52e580c40eae0dfa72415fa47ac01b12e29baf", size = 1798761, upload-time = "2025-12-08T01:46:05.386Z" },
- { url = "https://files.pythonhosted.org/packages/16/3a/a8ac542125f61574a3f015b521ca83b47321ed19bb63fe6d7560f348bfe1/black-25.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:252678f07f5bac4ff0d0e9b261fbb029fa530cfa206d0a636a34ab445ef8ca9d", size = 1429180, upload-time = "2025-12-08T01:45:34.903Z" },
- { url = "https://files.pythonhosted.org/packages/e6/2d/bdc466a3db9145e946762d52cd55b1385509d9f9004fec1c97bdc8debbfb/black-25.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:bc5b1c09fe3c931ddd20ee548511c64ebf964ada7e6f0763d443947fd1c603ce", size = 1239350, upload-time = "2025-12-08T01:46:09.458Z" },
- { url = "https://files.pythonhosted.org/packages/35/46/1d8f2542210c502e2ae1060b2e09e47af6a5e5963cb78e22ec1a11170b28/black-25.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0a0953b134f9335c2434864a643c842c44fba562155c738a2a37a4d61f00cad5", size = 1917015, upload-time = "2025-12-08T01:53:27.987Z" },
- { url = "https://files.pythonhosted.org/packages/41/37/68accadf977672beb8e2c64e080f568c74159c1aaa6414b4cd2aef2d7906/black-25.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2355bbb6c3b76062870942d8cc450d4f8ac71f9c93c40122762c8784df49543f", size = 1741830, upload-time = "2025-12-08T01:54:36.861Z" },
- { url = "https://files.pythonhosted.org/packages/ac/76/03608a9d8f0faad47a3af3a3c8c53af3367f6c0dd2d23a84710456c7ac56/black-25.12.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9678bd991cc793e81d19aeeae57966ee02909877cb65838ccffef24c3ebac08f", size = 1791450, upload-time = "2025-12-08T01:44:52.581Z" },
- { url = "https://files.pythonhosted.org/packages/06/99/b2a4bd7dfaea7964974f947e1c76d6886d65fe5d24f687df2d85406b2609/black-25.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:97596189949a8aad13ad12fcbb4ae89330039b96ad6742e6f6b45e75ad5cfd83", size = 1452042, upload-time = "2025-12-08T01:46:13.188Z" },
- { url = "https://files.pythonhosted.org/packages/b2/7c/d9825de75ae5dd7795d007681b752275ea85a1c5d83269b4b9c754c2aaab/black-25.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:778285d9ea197f34704e3791ea9404cd6d07595745907dd2ce3da7a13627b29b", size = 1267446, upload-time = "2025-12-08T01:46:14.497Z" },
- { url = "https://files.pythonhosted.org/packages/68/11/21331aed19145a952ad28fca2756a1433ee9308079bd03bd898e903a2e53/black-25.12.0-py3-none-any.whl", hash = "sha256:48ceb36c16dbc84062740049eef990bb2ce07598272e673c17d1a7720c71c828", size = 206191, upload-time = "2025-12-08T01:40:50.963Z" },
+ { url = "https://files.pythonhosted.org/packages/79/04/fa2f4784f7237279332aa735cdfd5ae2e7730db0072fb2041dadda9ae551/black-26.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ba1d768fbfb6930fc93b0ecc32a43d8861ded16f47a40f14afa9bb04ab93d304", size = 1877781, upload-time = "2026-01-18T04:59:39.054Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/ad/5a131b01acc0e5336740a039628c0ab69d60cf09a2c87a4ec49f5826acda/black-26.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b807c240b64609cb0e80d2200a35b23c7df82259f80bef1b2c96eb422b4aac9", size = 1699670, upload-time = "2026-01-18T04:59:41.005Z" },
+ { url = "https://files.pythonhosted.org/packages/da/7c/b05f22964316a52ab6b4265bcd52c0ad2c30d7ca6bd3d0637e438fc32d6e/black-26.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1de0f7d01cc894066a1153b738145b194414cc6eeaad8ef4397ac9abacf40f6b", size = 1775212, upload-time = "2026-01-18T04:59:42.545Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/a3/e8d1526bea0446e040193185353920a9506eab60a7d8beb062029129c7d2/black-26.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:91a68ae46bf07868963671e4d05611b179c2313301bd756a89ad4e3b3db2325b", size = 1409953, upload-time = "2026-01-18T04:59:44.357Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/5a/d62ebf4d8f5e3a1daa54adaab94c107b57be1b1a2f115a0249b41931e188/black-26.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:be5e2fe860b9bd9edbf676d5b60a9282994c03fbbd40fe8f5e75d194f96064ca", size = 1217707, upload-time = "2026-01-18T04:59:45.719Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/83/be35a175aacfce4b05584ac415fd317dd6c24e93a0af2dcedce0f686f5d8/black-26.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9dc8c71656a79ca49b8d3e2ce8103210c9481c57798b48deeb3a8bb02db5f115", size = 1871864, upload-time = "2026-01-18T04:59:47.586Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/f5/d33696c099450b1274d925a42b7a030cd3ea1f56d72e5ca8bbed5f52759c/black-26.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b22b3810451abe359a964cc88121d57f7bce482b53a066de0f1584988ca36e79", size = 1701009, upload-time = "2026-01-18T04:59:49.443Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/87/670dd888c537acb53a863bc15abbd85b22b429237d9de1b77c0ed6b79c42/black-26.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53c62883b3f999f14e5d30b5a79bd437236658ad45b2f853906c7cbe79de00af", size = 1767806, upload-time = "2026-01-18T04:59:50.769Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/9c/cd3deb79bfec5bcf30f9d2100ffeec63eecce826eb63e3961708b9431ff1/black-26.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:f016baaadc423dc960cdddf9acae679e71ee02c4c341f78f3179d7e4819c095f", size = 1433217, upload-time = "2026-01-18T04:59:52.218Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/29/f3be41a1cf502a283506f40f5d27203249d181f7a1a2abce1c6ce188035a/black-26.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:66912475200b67ef5a0ab665011964bf924745103f51977a78b4fb92a9fc1bf0", size = 1245773, upload-time = "2026-01-18T04:59:54.457Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/3d/51bdb3ecbfadfaf825ec0c75e1de6077422b4afa2091c6c9ba34fbfc0c2d/black-26.1.0-py3-none-any.whl", hash = "sha256:1054e8e47ebd686e078c0bb0eaf31e6ce69c966058d122f2c0c950311f9f3ede", size = 204010, upload-time = "2026-01-18T04:50:09.978Z" },
]
[[package]]
@@ -141,8 +141,8 @@ wheels = [
[[package]]
name = "octosuite"
-version = "4.0.0"
-source = { virtual = "." }
+version = "5.0.0"
+source = { editable = "." }
dependencies = [
{ name = "black" },
{ name = "pyfiglet" },
@@ -159,7 +159,7 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "black", specifier = ">=25.12.0" },
- { name = "black", marker = "extra == 'dev'", specifier = ">=25.12.0" },
+ { name = "black", marker = "extra == 'dev'", specifier = ">=26.1.0" },
{ name = "pyfiglet", specifier = ">=1.0.4" },
{ name = "questionary", specifier = ">=2.1.1" },
{ name = "rich", specifier = ">=14.2.0" },
@@ -178,11 +178,11 @@ wheels = [
[[package]]
name = "pathspec"
-version = "0.12.1"
+version = "1.0.3"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/4c/b2/bb8e495d5262bfec41ab5cb18f522f1012933347fb5d9e62452d446baca2/pathspec-1.0.3.tar.gz", hash = "sha256:bac5cf97ae2c2876e2d25ebb15078eb04d76e4b98921ee31c6f85ade8b59444d", size = 130841, upload-time = "2026-01-09T15:46:46.009Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" },
+ { url = "https://files.pythonhosted.org/packages/32/2b/121e912bd60eebd623f873fd090de0e84f322972ab25a7f9044c056804ed/pathspec-1.0.3-py3-none-any.whl", hash = "sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c", size = 55021, upload-time = "2026-01-09T15:46:44.652Z" },
]
[[package]]