This commit is contained in:
Ritchie Mwewa
2026-01-03 03:44:28 +02:00
parent b0e98fe386
commit b0aef9e204
11 changed files with 1267 additions and 682 deletions

33
Makefile Normal file
View File

@@ -0,0 +1,33 @@
.PHONY: help install dev run test lint format clean sync lock update
help:
@echo "Available commands:"
@echo " make install - Install dependencies"
@echo " make dev - Install dev dependencies"
@echo " make run - Run the application"
@echo " make format - Format code"
@echo " make sync - Sync dependencies with lock file"
@echo " make lock - Update lock file"
@echo " make update - Update dependencies"
install:
@uv pip install .
dev:
@uv pip install .[dev]
run: install
@uv run octosuite
format:
@uv run black .
sync:
@uv sync
lock:
@uv lock
update:
@uv lock --upgrade
@uv sync

View File

@@ -0,0 +1,223 @@
import csv
import json
import os
import subprocess
import typing as t
from datetime import datetime
from pathlib import Path
import pyfiglet
from prompt_toolkit.shortcuts import message_dialog
from rich.console import Console
from rich.text import Text
from rich.tree import Tree
from update_checker import UpdateChecker
__pkg__ = "octosuite"
__version__ = "4.0.0"
__author__ = "Ritchie Mwewa"
__all__ = [
"console",
"__pkg__",
"__author__",
"__version__",
"preview_response",
"export_response",
"check_updates",
"clear_screen",
"text_banner",
"set_menu_title",
]
console = Console(log_time=False)
def preview_response(data: t.Union[dict, list], source: str, _type: str):
if isinstance(data, dict):
tree = Tree(
label=f"\n[bold]{data.get('name') or data.get('login') or data.get('id') or 'Data'}[/bold]",
guide_style="#444444",
highlight=True,
)
fill_tree(tree=tree, data=data)
console.print(tree)
print()
elif isinstance(data, list):
preview_data = data[:5]
tree = Tree(
label=f"\nPreview: First {len(preview_data)} of {len(data)} {_type} from {source}",
guide_style="#444444",
highlight=True,
)
for item in preview_data:
if isinstance(item, dict):
branch_label = (
item.get("full_name")
or item.get("name")
or item.get("login")
or item.get("id")
or "Item"
)
branch = tree.add(label=f"[bold]{branch_label}[/bold]", highlight=True)
fill_tree(tree=branch, data=item)
console.print(tree)
print()
else:
console.print(data)
def export_response(data, data_type, source, file_formats, output_dir="../exports"):
"""Export data to selected formats using built-in libraries."""
# Create output directory if it doesn't exist
output_dir = Path(output_dir)
output_dir.mkdir(exist_ok=True)
# Generate filename with timestamp
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
safe_source = source.replace("/", "_")
filename = f"{safe_source}_{data_type}_{timestamp}"
# Normalize data to list of dicts
if isinstance(data, dict):
data_list = [data]
else:
data_list = data
# Export to all selected formats
exported_files = []
for file_format in file_formats:
filepath = output_dir / f"{filename}.{file_format}"
if file_format == "json":
with open(filepath, "w", encoding="utf-8") as f:
json.dump(data_list, f, indent=2, ensure_ascii=False)
elif file_format == "csv":
if data_list:
# Get all unique keys from all dicts
keys = set()
for item in data_list:
if isinstance(item, dict):
keys.update(item.keys())
keys = sorted(keys)
with open(filepath, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=keys)
writer.writeheader()
for item in data_list:
if isinstance(item, dict):
writer.writerow(item)
elif file_format == "html":
with open(filepath, "w", encoding="utf-8") as file:
file.write("<!DOCTYPE html>\n<html>\n<head>\n")
file.write('<meta charset="UTF-8">\n')
file.write("<style>table { border-collapse: collapse; width: 100%; }\n")
file.write(
"th, td { border: 1px solid black; padding: 8px; text-align: left; }\n"
)
file.write("th { background-color: #f2f2f2; }</style>\n")
file.write("</head>\n<body>\n")
file.write('<table class="table table-striped" border="1">\n')
if data_list:
# Get all unique keys
keys = set()
for item in data_list:
if isinstance(item, dict):
keys.update(item.keys())
keys = sorted(keys)
# Write header
file.write("<thead>\n<tr>\n")
for key in keys:
file.write(f"<th>{key}</th>\n")
file.write("</tr>\n</thead>\n")
# Write rows
file.write("<tbody>\n")
for item in data_list:
if isinstance(item, dict):
file.write("<tr>\n")
for key in keys:
value = item.get(key, "")
file.write(f"<td>{value}</td>\n")
file.write("</tr>\n")
file.write("</tbody>\n")
file.write("</table>\n</body>\n</html>")
exported_files.append(str(filepath))
# Show success message
console.print("\n[green]✓ Export successful![/green]")
for filepath in exported_files:
console.print(f"{filepath}")
return exported_files
def fill_tree(tree: Tree, data: t.Union[dict, list]) -> Tree:
if isinstance(data, dict):
for key, value in data.items():
if isinstance(value, dict) or isinstance(value, list):
branch = tree.add(f"{key}")
fill_tree(tree=branch, data=value)
else:
tree.add(f"[dim]{key.replace('_', ' ').title()}[/dim]: {value}")
elif isinstance(data, list):
for item in data:
if isinstance(item, dict) or isinstance(item, list):
fill_tree(tree=tree, data=item)
else:
tree.add(str(item))
else:
tree.add(str(data))
return tree
def check_updates():
checker = UpdateChecker()
result = checker.check(__pkg__, __version__)
if result:
message_dialog(title="Update Available", text=result).run()
else:
message_dialog(
title="Up to Date",
text=f"You're running the current version, {__version__}",
).run()
def clear_screen():
"""Clear the terminal screen."""
subprocess.run(["cls" if os.name == "nt" else "clear"])
def text_banner(text: str):
clear_screen()
ascii_text = pyfiglet.figlet_format(text=text, font="chunky")
banner_text = Text(ascii_text)
length = len(ascii_text)
for i in range(length):
ratio = i / max(length - 1, 1)
r = int(255 - 127 * ratio)
g = int(200 - 200 * ratio)
b = 255
banner_text.stylize(f"rgb({r},{g},{b})", i, i + 1)
console.print(banner_text)
def set_menu_title(menu_type: t.Literal["home", "user", "org", "repo", "search"]):
title: str = __pkg__.title()
title += f" | {menu_type.title()}"
console.set_window_title(title=title)

View File

@@ -1,37 +0,0 @@
import re
import typing as t
import requests
BASE_URL = "https://api.github.com"
class Api:
def __init__(self): ...
def get(self, url: str, params: t.Optional[dict] = None) -> t.Union[dict, list]:
response = requests.get(url, params=params)
response.raise_for_status()
return self._sanitise_response(response=response.json())
def _sanitise_response(self, response: t.Union[dict, list]) -> t.Union[dict, list]:
pattern = re.compile(r"https://api\.github\.com")
if isinstance(response, list):
return [self._sanitise_response(response=item) for item in response]
if isinstance(response, dict):
keys_to_remove = [
key
for key, value in response.items()
if (isinstance(value, str) and pattern.search(value)) or value is None
]
for key in keys_to_remove:
response.pop(key)
# Recursively clean nested dicts/lists
for key, value in response.items():
if isinstance(value, (dict, list)):
response[key] = self._sanitise_response(response=value)
return response

77
octosuite/core/github.py Normal file
View File

@@ -0,0 +1,77 @@
import re
import sys
import typing as t
import requests
from requests import Response
from .. import __version__
BASE_URL = "https://api.github.com"
class GitHub:
def __init__(
self,
user_agent: str = (
f"octosuite/{__version__} "
f"(Python {sys.version.split()[0]}; "
f"https://github.com/bellingcat/octosuite) requests/{requests.__version__}"
),
):
self.user_agent = user_agent
def get(
self, url: str, params: t.Optional[dict] = None, return_response: bool = False
) -> t.Union[dict, list, Response]:
response = requests.get(
url=url, params=params, headers={"User-Agent": self.user_agent}
)
if return_response:
return response
if response.status_code == 200:
return self._sanitise_response(response=response.json())
return []
def is_valid_entity(
self, entity_type: t.Literal["user", "org", "repo"], **kwargs
) -> bool:
"""Validate if a GitHub entity exists."""
try:
if entity_type == "user":
url = f"https://api.github.com/users/{kwargs.get('username')}"
elif entity_type == "org":
url = f"https://api.github.com/orgs/{kwargs.get('username')}"
elif entity_type == "repo":
url = f"https://api.github.com/repos/{kwargs.get('repo_owner')}/{kwargs.get('repo_name')}"
else:
return True
response = self.get(url=url, return_response=True)
return response.status_code == 200
except requests.RequestException:
return False
def _sanitise_response(self, response: t.Union[dict, list]) -> t.Union[dict, list]:
pattern = re.compile(r"https://api\.github\.com")
if isinstance(response, list):
return [self._sanitise_response(response=item) for item in response]
if isinstance(response, dict):
keys_to_remove = [
key
for key, value in response.items()
if (isinstance(value, str) and pattern.search(value)) or value is None
]
for key in keys_to_remove:
response.pop(key)
# Recursively clean nested dicts/lists
for key, value in response.items():
if isinstance(value, (dict, list)):
response[key] = self._sanitise_response(response=value)
return response

View File

@@ -1,187 +1,238 @@
from .api import Api, BASE_URL from requests import exceptions
from .github import GitHub, BASE_URL
github = GitHub()
__all__ = ["User", "Org", "Repo", "Search"]
api = Api() class GitHubEntity:
"""Base class for GitHub entities with common functionality."""
def __init__(self, source: str):
self.endpoint = None
self.source = source
def exists(self) -> bool:
"""Check if the entity exists on GitHub."""
try:
response = github.get(url=self.endpoint, return_response=True)
return response.status_code == 200
except exceptions.RequestException as err:
return False
class User: class User(GitHubEntity):
def __init__(self, name: str): def __init__(self, name: str):
super().__init__(source=name)
self.name = name self.name = name
self.endpoint = f"{BASE_URL}/users/{name}" self.endpoint = f"{BASE_URL}/users/{name}"
def profile(self) -> dict: def profile(self) -> dict:
profile = api.get(url=self.endpoint) profile = github.get(url=self.endpoint)
return profile return profile
def repos(self, page: int, per_page: int) -> list: def repos(self, page: int, per_page: int) -> list:
params = {"page": page, "per_page": per_page} params = {"page": page, "per_page": per_page}
repos = api.get(url=f"{self.endpoint}/repos", params=params) repos = github.get(url=f"{self.endpoint}/repos", params=params)
return repos return repos
def subscriptions(self, page: int, per_page: int) -> list: def subscriptions(self, page: int, per_page: int) -> list:
params = {"page": page, "per_page": per_page} params = {"page": page, "per_page": per_page}
subscriptions = api.get(url=f"{self.endpoint}/subscriptions", params=params) subscriptions = github.get(url=f"{self.endpoint}/subscriptions", params=params)
return subscriptions return subscriptions
def starred(self, page: int, per_page: int) -> list: def starred(self, page: int, per_page: int) -> list:
params = {"page": page, "per_page": per_page} params = {"page": page, "per_page": per_page}
starred = api.get(url=f"{self.endpoint}/starred", params=params) starred = github.get(url=f"{self.endpoint}/starred", params=params)
return starred return starred
def followers(self, page: int, per_page: int) -> list: def followers(self, page: int, per_page: int) -> list:
params = {"page": page, "per_page": per_page} params = {"page": page, "per_page": per_page}
users = api.get(url=f"{self.endpoint}/followers", params=params) users = github.get(url=f"{self.endpoint}/followers", params=params)
return users return users
def following(self, page: int, per_page: int) -> list: def following(self, page: int, per_page: int) -> list:
params = {"page": page, "per_page": per_page} params = {"page": page, "per_page": per_page}
users = api.get(url=f"{self.endpoint}/following", params=params) users = github.get(url=f"{self.endpoint}/following", params=params)
return users return users
def follows(self, user: str) -> bool: ... def follows(self, user: str) -> bool: ...
def orgs(self, page: int, per_page: int) -> list: def orgs(self, page: int, per_page: int) -> list:
params = {"page": page, "per_page": per_page} params = {"page": page, "per_page": per_page}
orgs = api.get(url=f"{self.endpoint}/orgs", params=params) orgs = github.get(url=f"{self.endpoint}/orgs", params=params)
return orgs return orgs
def gists(self, page: int, per_page: int) -> list: def gists(self, page: int, per_page: int) -> list:
params = {"page": page, "per_page": per_page} params = {"page": page, "per_page": per_page}
gists = api.get(url=f"{self.endpoint}/gists", params=params) gists = github.get(url=f"{self.endpoint}/gists", params=params)
return gists return gists
def events(self, page: int, per_page: int) -> list: def events(self, page: int, per_page: int) -> list:
params = {"page": page, "per_page": per_page} params = {"page": page, "per_page": per_page}
events = api.get(url=f"{self.endpoint}/events", params=params) events = github.get(url=f"{self.endpoint}/events", params=params)
return events return events
def received_events(self, page: int, per_page: int) -> list: def received_events(self, page: int, per_page: int) -> list:
params = {"page": page, "per_page": per_page} params = {"page": page, "per_page": per_page}
received_events = api.get(url=f"{self.endpoint}/received_events", params=params) received_events = github.get(
url=f"{self.endpoint}/received_events", params=params
)
return received_events return received_events
class Org: class Org(GitHubEntity):
def __init__(self, name: str): def __init__(self, name: str):
super().__init__(source=name)
self.name = name self.name = name
self.endpoint = f"{BASE_URL}/orgs/{name}" self.endpoint = f"{BASE_URL}/orgs/{name}"
def profile(self) -> dict: def profile(self) -> dict:
profile = api.get(url=self.endpoint) profile = github.get(url=self.endpoint)
return profile return profile
def repos(self, page: int, per_page: int) -> list: def repos(self, page: int, per_page: int) -> list:
params = {"page": page, "per_page": per_page} params = {"page": page, "per_page": per_page}
repos = api.get(url=f"{self.endpoint}/repos", params=params) repos = github.get(url=f"{self.endpoint}/repos", params=params)
return repos return repos
def events(self, page: int, per_page: int) -> list: def events(self, page: int, per_page: int) -> list:
params = {"page": page, "per_page": per_page} params = {"page": page, "per_page": per_page}
events = api.get(url=f"{self.endpoint}/events", params=params) events = github.get(url=f"{self.endpoint}/events", params=params)
return events return events
def hooks(self, page: int, per_page: int) -> list: def hooks(self, page: int, per_page: int) -> list:
params = {"page": page, "per_page": per_page} params = {"page": page, "per_page": per_page}
hooks = api.get(url=f"{self.endpoint}/hooks", params=params) hooks = github.get(url=f"{self.endpoint}/hooks", params=params)
return hooks return hooks
def issues(self, page: int, per_page: int) -> list: def issues(self, page: int, per_page: int) -> list:
params = {"page": page, "per_page": per_page} params = {"page": page, "per_page": per_page}
issues = api.get(url=f"{self.endpoint}/issues", params=params) issues = github.get(url=f"{self.endpoint}/issues", params=params)
return issues return issues
def members(self, page: int, per_page: int) -> list: def members(self, page: int, per_page: int) -> list:
params = {"page": page, "per_page": per_page} params = {"page": page, "per_page": per_page}
members = api.get(url=f"{self.endpoint}/members", params=params) members = github.get(url=f"{self.endpoint}/members", params=params)
return members return members
class Repo: class Repo(GitHubEntity):
def __init__(self, name: str, owner: str): def __init__(self, name: str, owner: str):
super().__init__(source=f"{owner}/{name}")
self.name = name self.name = name
self.owner = owner self.owner = owner
self.endpoint = f"{BASE_URL}/repos/{owner}/{name}" self.endpoint = f"{BASE_URL}/repos/{owner}/{name}"
def profile(self) -> dict: def profile(self) -> dict:
profile = api.get(url=self.endpoint) profile = github.get(url=self.endpoint)
return profile return profile
def forks(self, page: int, per_page: int) -> list: def forks(self, page: int, per_page: int) -> list:
params = {"page": page, "per_page": per_page} params = {"page": page, "per_page": per_page}
forks = api.get(url=f"{self.endpoint}/forks", params=params) forks = github.get(url=f"{self.endpoint}/forks", params=params)
return forks return forks
def issue_events(self, page: int, per_page: int) -> list: def issue_events(self, page: int, per_page: int) -> list:
params = {"page": page, "per_page": per_page} params = {"page": page, "per_page": per_page}
issue_events = api.get(url=f"{self.endpoint}/issue_events", params=params) issue_events = github.get(url=f"{self.endpoint}/issue_events", params=params)
return issue_events return issue_events
def events(self, page: int, per_page: int) -> list: def events(self, page: int, per_page: int) -> list:
params = {"page": page, "per_page": per_page} params = {"page": page, "per_page": per_page}
events = api.get(url=f"{self.endpoint}/events", params=params) events = github.get(url=f"{self.endpoint}/events", params=params)
return events return events
def assignees(self, page: int, per_page: int) -> list: def assignees(self, page: int, per_page: int) -> list:
params = {"page": page, "per_page": per_page} params = {"page": page, "per_page": per_page}
assignees = api.get(url=f"{self.endpoint}/assignees", params=params) assignees = github.get(url=f"{self.endpoint}/assignees", params=params)
return assignees return assignees
def branches(self, page: int, per_page: int) -> list: def branches(self, page: int, per_page: int) -> list:
params = {"page": page, "per_page": per_page} params = {"page": page, "per_page": per_page}
branches = api.get(url=f"{self.endpoint}/branches", params=params) branches = github.get(url=f"{self.endpoint}/branches", params=params)
return branches return branches
def tags(self, page: int, per_page: int) -> list: def tags(self, page: int, per_page: int) -> list:
params = {"page": page, "per_page": per_page} params = {"page": page, "per_page": per_page}
tags = api.get(url=f"{self.endpoint}/tags", params=params) tags = github.get(url=f"{self.endpoint}/tags", params=params)
return tags return tags
def languages(self) -> dict: def languages(self) -> dict:
languages = api.get(url=f"{self.endpoint}/languages") languages = github.get(url=f"{self.endpoint}/languages")
return languages return languages
def stargazers(self, page: int, per_page: int) -> list: def stargazers(self, page: int, per_page: int) -> list:
params = {"page": page, "per_page": per_page} params = {"page": page, "per_page": per_page}
stargazers = api.get(url=f"{self.endpoint}/stargazers", params=params) stargazers = github.get(url=f"{self.endpoint}/stargazers", params=params)
return stargazers return stargazers
def subscribers(self, page: int, per_page: int) -> list: def subscribers(self, page: int, per_page: int) -> list:
params = {"page": page, "per_page": per_page} params = {"page": page, "per_page": per_page}
subscribers = api.get(url=f"{self.endpoint}/subscribers", params=params) subscribers = github.get(url=f"{self.endpoint}/subscribers", params=params)
return subscribers return subscribers
def commits(self, page: int, per_page: int) -> list: def commits(self, page: int, per_page: int) -> list:
params = {"page": page, "per_page": per_page} params = {"page": page, "per_page": per_page}
commits = api.get(url=f"{self.endpoint}/commits", params=params) commits = github.get(url=f"{self.endpoint}/commits", params=params)
return commits return commits
def comments(self, page: int, per_page: int) -> list: def comments(self, page: int, per_page: int) -> list:
params = {"page": page, "per_page": per_page} params = {"page": page, "per_page": per_page}
comments = api.get(url=f"{self.endpoint}/comments", params=params) comments = github.get(url=f"{self.endpoint}/comments", params=params)
return comments return comments
def contents(self, path: str) -> list: def contents(self, path: str) -> list:
contents = api.get(url=f"{self.endpoint}/contents/{path}") contents = github.get(url=f"{self.endpoint}/contents/{path}")
return contents return contents
def issues(self, page: int, per_page: int) -> list: def issues(self, page: int, per_page: int) -> list:
params = {"page": page, "per_page": per_page} params = {"page": page, "per_page": per_page}
issues = api.get(url=f"{self.endpoint}/issues", params=params) issues = github.get(url=f"{self.endpoint}/issues", params=params)
return issues return issues
def releases(self, page: int, per_page: int) -> list: def releases(self, page: int, per_page: int) -> list:
params = {"page": page, "per_page": per_page} params = {"page": page, "per_page": per_page}
releases = api.get(url=f"{self.endpoint}/releases", params=params) releases = github.get(url=f"{self.endpoint}/releases", params=params)
return releases return releases
def deployments(self, page: int, per_page: int) -> list: def deployments(self, page: int, per_page: int) -> list:
params = {"page": page, "per_page": per_page} params = {"page": page, "per_page": per_page}
deployments = api.get(url=f"{self.endpoint}/deployments", params=params) deployments = github.get(url=f"{self.endpoint}/deployments", params=params)
return deployments return deployments
def labels(self, page: int, per_page: int) -> list: def labels(self, page: int, per_page: int) -> list:
params = {"page": page, "per_page": per_page} params = {"page": page, "per_page": per_page}
labels = api.get(url=f"{self.endpoint}/labels", params=params) labels = github.get(url=f"{self.endpoint}/labels", params=params)
return labels return labels
class Search:
def __init__(self, query: str, page: int, per_page: int):
self.query = query
self.page = page
self.per_page = per_page
self.endpoint = f"{BASE_URL}/search"
self.params = {"q": query, "page": page, "per_page": per_page}
def repos(self) -> list:
repos = github.get(url=f"{self.endpoint}/repositories", params=self.params)
return repos
def users(self) -> list:
users = github.get(url=f"{self.endpoint}/users", params=self.params)
return users
def commits(self) -> list:
commits = github.get(url=f"{self.endpoint}/commits", params=self.params)
return commits
def issues(self) -> list:
issues = github.get(url=f"{self.endpoint}/issues", params=self.params)
return issues
def topics(self) -> list:
topics = github.get(url=f"{self.endpoint}/topics", params=self.params)
return topics

View File

@@ -1,13 +1,13 @@
import sys import sys
from . import console, __pkg__, __version__
from .tui.menus import Menus from .tui.menus import Menus
from .tui.prompts import Prompts
def start(): def start():
try: try:
prompts = Prompts() console.set_window_title(title=f"{__pkg__.title()} v{__version__}")
menu = Menus(prompts=prompts) menu = Menus()
menu.main() menu.main()
except KeyboardInterrupt: except KeyboardInterrupt:
sys.exit() sys.exit()

44
octosuite/tui/dialogs.py Normal file
View File

@@ -0,0 +1,44 @@
from datetime import datetime
from prompt_toolkit.shortcuts import button_dialog, message_dialog
LICENSE_NOTICE = f"""Copyright (c) {datetime.now().year} 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
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE."""
class Dialogs:
def __init__(self):
...
@staticmethod
def quit() -> bool:
try:
result = button_dialog(
title="Quit",
text="This will close the session, continue?",
buttons=[("Yes", True), ("No", False)],
).run()
return result if result is not None else False
except KeyboardInterrupt:
return True
@staticmethod
def license():
message_dialog(title="MIT License", text=LICENSE_NOTICE).run()

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
import typing as t import typing as t
import questionary as q import questionary as q
from prompt_toolkit.styles import Style from questionary import Style
class Prompts: class Prompts:
@@ -9,21 +9,29 @@ class Prompts:
pass pass
@staticmethod @staticmethod
def prompt(message: str, instruction: t.Optional[str] = None) -> str: def prompt(
message: str,
instruction: t.Optional[str] = None,
style: t.Optional[Style] = None,
qmark: t.Optional[str] = "?",
) -> str:
return q.text( return q.text(
message=message, message=message,
instruction=instruction, instruction=instruction,
style=style,
qmark=qmark,
validate=lambda text: len(text.strip()) > 0 or "Input cannot be empty",
).ask() ).ask()
@staticmethod @staticmethod
def pagination_params() -> dict: def pagination_params() -> dict:
"""Prompt user for pagination parameters.""" """Prompt user for pagination parameters."""
try: try:
page = q.text(message="Page", instruction="defaults to 1", qmark="#").ask() page = q.text(message="Page", default="1", qmark="n").ask()
per_page = q.text( per_page = q.text(
message="Per Page", message="Per Page",
instruction="default and max is 100", default="100",
qmark="#", qmark="n",
).ask() ).ask()
return { return {
@@ -34,21 +42,3 @@ class Prompts:
except (ValueError, TypeError): except (ValueError, TypeError):
print("Invalid input, using defaults (page=1, per_page=100)") print("Invalid input, using defaults (page=1, per_page=100)")
return {"page": 1, "per_page": 100} return {"page": 1, "per_page": 100}
@staticmethod
def quit() -> bool:
try:
if q.confirm(
"This will close the session, continue?",
default=True,
style=Style(
[
("qmark", "fg:red bold"),
("question", "fg:red bold"),
]
),
).ask():
return True
return False
except KeyboardInterrupt:
return True

View File

@@ -10,10 +10,9 @@ authors = [
requires-python = ">=3.13" requires-python = ">=3.13"
dependencies = [ dependencies = [
"rich>=14.2.0", "rich>=14.2.0",
"requests>=2.32.5",
"questionary>=2.1.1", "questionary>=2.1.1",
"pyfiglet>=1.0.4", "pyfiglet>=1.0.4",
"pandas>=2.3.3" "update-checker>=0.18.0"
] ]
classifiers = [ classifiers = [
"Development Status :: 4 - Beta", "Development Status :: 4 - Beta",
@@ -28,5 +27,10 @@ homepage = "https://bellingcat.com"
issues = "https://github.com/bellingcat/octosuite/issues" issues = "https://github.com/bellingcat/octosuite/issues"
repository = "https://github.com/bellingcat/octosuite" repository = "https://github.com/bellingcat/octosuite"
[project.optional-dependencies]
dev = [
"black>=25.12.0",
]
[project.scripts] [project.scripts]
octosuite = "octosuite.main:start" octosuite = "octosuite.main:start"

197
uv.lock generated
View File

@@ -1,6 +1,33 @@
version = 1 version = 1
revision = 2 revision = 2
requires-python = ">=3.14" requires-python = ">=3.13"
[[package]]
name = "black"
version = "25.12.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "mypy-extensions" },
{ name = "packaging" },
{ name = "pathspec" },
{ 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" }
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" },
]
[[package]] [[package]]
name = "certifi" name = "certifi"
@@ -17,6 +44,22 @@ version = "3.4.4"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" },
{ url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
{ url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" },
{ url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" },
{ url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" },
{ url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" },
{ url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" },
{ url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" },
{ url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" },
{ url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" },
{ url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" },
{ url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" },
{ url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" },
{ url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" },
{ url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" },
{ url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" },
{ url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" },
{ url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" },
{ url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" },
@@ -36,6 +79,27 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
] ]
[[package]]
name = "click"
version = "8.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]] [[package]]
name = "idna" name = "idna"
version = "3.11" version = "3.11"
@@ -67,80 +131,65 @@ wheels = [
] ]
[[package]] [[package]]
name = "numpy" name = "mypy-extensions"
version = "2.4.0" version = "1.1.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a4/7a/6a3d14e205d292b738db449d0de649b373a59edb0d0b4493821d0a3e8718/numpy-2.4.0.tar.gz", hash = "sha256:6e504f7b16118198f138ef31ba24d985b124c2c469fe8467007cf30fd992f934", size = 20685720, upload-time = "2025-12-20T16:18:19.023Z" } sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/ab/ed/52eac27de39d5e5a6c9aadabe672bc06f55e24a3d9010cd1183948055d76/numpy-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c95eb6db2884917d86cde0b4d4cf31adf485c8ec36bf8696dd66fa70de96f36b", size = 16647476, upload-time = "2025-12-20T16:17:17.671Z" }, { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
{ url = "https://files.pythonhosted.org/packages/77/c0/990ce1b7fcd4e09aeaa574e2a0a839589e4b08b2ca68070f1acb1fea6736/numpy-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:65167da969cd1ec3a1df31cb221ca3a19a8aaa25370ecb17d428415e93c1935e", size = 12374563, upload-time = "2025-12-20T16:17:20.216Z" },
{ url = "https://files.pythonhosted.org/packages/37/7c/8c5e389c6ae8f5fd2277a988600d79e9625db3fff011a2d87ac80b881a4c/numpy-2.4.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3de19cfecd1465d0dcf8a5b5ea8b3155b42ed0b639dba4b71e323d74f2a3be5e", size = 5203107, upload-time = "2025-12-20T16:17:22.47Z" },
{ url = "https://files.pythonhosted.org/packages/e6/94/ca5b3bd6a8a70a5eec9a0b8dd7f980c1eff4b8a54970a9a7fef248ef564f/numpy-2.4.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6c05483c3136ac4c91b4e81903cb53a8707d316f488124d0398499a4f8e8ef51", size = 6538067, upload-time = "2025-12-20T16:17:24.001Z" },
{ url = "https://files.pythonhosted.org/packages/79/43/993eb7bb5be6761dde2b3a3a594d689cec83398e3f58f4758010f3b85727/numpy-2.4.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36667db4d6c1cea79c8930ab72fadfb4060feb4bfe724141cd4bd064d2e5f8ce", size = 14411926, upload-time = "2025-12-20T16:17:25.822Z" },
{ url = "https://files.pythonhosted.org/packages/03/75/d4c43b61de473912496317a854dac54f1efec3eeb158438da6884b70bb90/numpy-2.4.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9a818668b674047fd88c4cddada7ab8f1c298812783e8328e956b78dc4807f9f", size = 16354295, upload-time = "2025-12-20T16:17:28.308Z" },
{ url = "https://files.pythonhosted.org/packages/b8/0a/b54615b47ee8736a6461a4bb6749128dd3435c5a759d5663f11f0e9af4ac/numpy-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1ee32359fb7543b7b7bd0b2f46294db27e29e7bbdf70541e81b190836cd83ded", size = 16190242, upload-time = "2025-12-20T16:17:30.993Z" },
{ url = "https://files.pythonhosted.org/packages/98/ce/ea207769aacad6246525ec6c6bbd66a2bf56c72443dc10e2f90feed29290/numpy-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e493962256a38f58283de033d8af176c5c91c084ea30f15834f7545451c42059", size = 18280875, upload-time = "2025-12-20T16:17:33.327Z" },
{ url = "https://files.pythonhosted.org/packages/17/ef/ec409437aa962ea372ed601c519a2b141701683ff028f894b7466f0ab42b/numpy-2.4.0-cp314-cp314-win32.whl", hash = "sha256:6bbaebf0d11567fa8926215ae731e1d58e6ec28a8a25235b8a47405d301332db", size = 6002530, upload-time = "2025-12-20T16:17:35.729Z" },
{ url = "https://files.pythonhosted.org/packages/5f/4a/5cb94c787a3ed1ac65e1271b968686521169a7b3ec0b6544bb3ca32960b0/numpy-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:3d857f55e7fdf7c38ab96c4558c95b97d1c685be6b05c249f5fdafcbd6f9899e", size = 12435890, upload-time = "2025-12-20T16:17:37.599Z" },
{ url = "https://files.pythonhosted.org/packages/48/a0/04b89db963af9de1104975e2544f30de89adbf75b9e75f7dd2599be12c79/numpy-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:bb50ce5fb202a26fd5404620e7ef820ad1ab3558b444cb0b55beb7ef66cd2d63", size = 10591892, upload-time = "2025-12-20T16:17:39.649Z" },
{ url = "https://files.pythonhosted.org/packages/53/e5/d74b5ccf6712c06c7a545025a6a71bfa03bdc7e0568b405b0d655232fd92/numpy-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:355354388cba60f2132df297e2d53053d4063f79077b67b481d21276d61fc4df", size = 12494312, upload-time = "2025-12-20T16:17:41.714Z" },
{ url = "https://files.pythonhosted.org/packages/c2/08/3ca9cc2ddf54dfee7ae9a6479c071092a228c68aef08252aa08dac2af002/numpy-2.4.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:1d8f9fde5f6dc1b6fc34df8162f3b3079365468703fee7f31d4e0cc8c63baed9", size = 5322862, upload-time = "2025-12-20T16:17:44.145Z" },
{ url = "https://files.pythonhosted.org/packages/87/74/0bb63a68394c0c1e52670cfff2e309afa41edbe11b3327d9af29e4383f34/numpy-2.4.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e0434aa22c821f44eeb4c650b81c7fbdd8c0122c6c4b5a576a76d5a35625ecd9", size = 6644986, upload-time = "2025-12-20T16:17:46.203Z" },
{ url = "https://files.pythonhosted.org/packages/06/8f/9264d9bdbcf8236af2823623fe2f3981d740fc3461e2787e231d97c38c28/numpy-2.4.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40483b2f2d3ba7aad426443767ff5632ec3156ef09742b96913787d13c336471", size = 14457958, upload-time = "2025-12-20T16:17:48.017Z" },
{ url = "https://files.pythonhosted.org/packages/8c/d9/f9a69ae564bbc7236a35aa883319364ef5fd41f72aa320cc1cbe66148fe2/numpy-2.4.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6a7664ddd9746e20b7325351fe1a8408d0a2bf9c63b5e898290ddc8f09544", size = 16398394, upload-time = "2025-12-20T16:17:50.409Z" },
{ url = "https://files.pythonhosted.org/packages/34/c7/39241501408dde7f885d241a98caba5421061a2c6d2b2197ac5e3aa842d8/numpy-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ecb0019d44f4cdb50b676c5d0cb4b1eae8e15d1ed3d3e6639f986fc92b2ec52c", size = 16241044, upload-time = "2025-12-20T16:17:52.661Z" },
{ url = "https://files.pythonhosted.org/packages/7c/95/cae7effd90e065a95e59fe710eeee05d7328ed169776dfdd9f789e032125/numpy-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d0ffd9e2e4441c96a9c91ec1783285d80bf835b677853fc2770a89d50c1e48ac", size = 18321772, upload-time = "2025-12-20T16:17:54.947Z" },
{ url = "https://files.pythonhosted.org/packages/96/df/3c6c279accd2bfb968a76298e5b276310bd55d243df4fa8ac5816d79347d/numpy-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:77f0d13fa87036d7553bf81f0e1fe3ce68d14c9976c9851744e4d3e91127e95f", size = 6148320, upload-time = "2025-12-20T16:17:57.249Z" },
{ url = "https://files.pythonhosted.org/packages/92/8d/f23033cce252e7a75cae853d17f582e86534c46404dea1c8ee094a9d6d84/numpy-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b1f5b45829ac1848893f0ddf5cb326110604d6df96cdc255b0bf9edd154104d4", size = 12623460, upload-time = "2025-12-20T16:17:58.963Z" },
{ url = "https://files.pythonhosted.org/packages/a4/4f/1f8475907d1a7c4ef9020edf7f39ea2422ec896849245f00688e4b268a71/numpy-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:23a3e9d1a6f360267e8fbb38ba5db355a6a7e9be71d7fce7ab3125e88bb646c8", size = 10661799, upload-time = "2025-12-20T16:18:01.078Z" },
] ]
[[package]] [[package]]
name = "octosuite" name = "octosuite"
version = "4.0.0" version = "4.0.0"
source = { editable = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "pandas" },
{ name = "pyfiglet" }, { name = "pyfiglet" },
{ name = "questionary" }, { name = "questionary" },
{ name = "requests" },
{ name = "rich" }, { name = "rich" },
{ name = "update-checker" },
]
[package.optional-dependencies]
dev = [
{ name = "black" },
] ]
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "pandas", specifier = ">=2.3.3,<3.0.0" }, { name = "black", marker = "extra == 'dev'", specifier = ">=25.12.0" },
{ name = "pyfiglet", specifier = ">=1.0.4,<2.0.0" }, { name = "pyfiglet", specifier = ">=1.0.4" },
{ name = "questionary", specifier = ">=2.1.1,<3.0.0" }, { name = "questionary", specifier = ">=2.1.1" },
{ name = "requests", specifier = ">=2.32.5,<3.0.0" }, { name = "rich", specifier = ">=14.2.0" },
{ name = "rich", specifier = ">=14.2.0,<15.0.0" }, { name = "update-checker", specifier = ">=0.18.0" },
]
provides-extras = ["dev"]
[[package]]
name = "packaging"
version = "25.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
] ]
[[package]] [[package]]
name = "pandas" name = "pathspec"
version = "2.3.3" version = "0.12.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ 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" }
{ name = "numpy" },
{ name = "python-dateutil" },
{ name = "pytz" },
{ name = "tzdata" },
]
sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" }, { 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/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" }, ]
{ url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" },
{ url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" }, [[package]]
{ url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" }, name = "platformdirs"
{ url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" }, version = "4.5.1"
{ url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" }, source = { registry = "https://pypi.org/simple" }
{ url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" }, sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" }
{ url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" }, wheels = [
{ url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" }, { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" },
{ url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" },
{ url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" },
{ url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" },
] ]
[[package]] [[package]]
@@ -174,24 +223,12 @@ wheels = [
] ]
[[package]] [[package]]
name = "python-dateutil" name = "pytokens"
version = "2.9.0.post0" version = "0.3.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ sdist = { url = "https://files.pythonhosted.org/packages/4e/8d/a762be14dae1c3bf280202ba3172020b2b0b4c537f94427435f19c413b72/pytokens-0.3.0.tar.gz", hash = "sha256:2f932b14ed08de5fcf0b391ace2642f858f1394c0857202959000b68ed7a458a", size = 17644, upload-time = "2025-11-05T13:36:35.34Z" }
{ name = "six" },
]
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, { url = "https://files.pythonhosted.org/packages/84/25/d9db8be44e205a124f6c98bc0324b2bb149b7431c53877fc6d1038dddaf5/pytokens-0.3.0-py3-none-any.whl", hash = "sha256:95b2b5eaf832e469d141a378872480ede3f251a5a5041b8ec6e581d3ac71bbf3", size = 12195, upload-time = "2025-11-05T13:36:33.183Z" },
]
[[package]]
name = "pytz"
version = "2025.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
] ]
[[package]] [[package]]
@@ -235,21 +272,15 @@ wheels = [
] ]
[[package]] [[package]]
name = "six" name = "update-checker"
version = "1.17.0" version = "0.18.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } dependencies = [
wheels = [ { name = "requests" },
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/5c/0b/1bec4a6cc60d33ce93d11a7bcf1aeffc7ad0aa114986073411be31395c6f/update_checker-0.18.0.tar.gz", hash = "sha256:6a2d45bb4ac585884a6b03f9eade9161cedd9e8111545141e9aa9058932acb13", size = 6699, upload-time = "2020-08-04T07:08:50.429Z" }
[[package]]
name = "tzdata"
version = "2025.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, { url = "https://files.pythonhosted.org/packages/0c/ba/8dd7fa5f0b1c6a8ac62f8f57f7e794160c1f86f31c6d0fb00f582372a3e4/update_checker-0.18.0-py3-none-any.whl", hash = "sha256:cbba64760a36fe2640d80d85306e8fe82b6816659190993b7bdabadee4d4bbfd", size = 7008, upload-time = "2020-08-04T07:08:49.51Z" },
] ]
[[package]] [[package]]