mirror of
https://github.com/bellingcat/octosuite.git
synced 2026-06-07 19:08:36 +03:00
4.0/beta
This commit is contained in:
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
# Python-generated files
|
||||
__pycache__/
|
||||
*.py[oc]
|
||||
build/
|
||||
dist/
|
||||
wheels/
|
||||
*.egg-info
|
||||
|
||||
# Virtual environments
|
||||
.venv
|
||||
1
.python-version
Normal file
1
.python-version
Normal file
@@ -0,0 +1 @@
|
||||
3.14
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 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.
|
||||
0
octosuite/__init__.py
Normal file
0
octosuite/__init__.py
Normal file
0
octosuite/core/__init__.py
Normal file
0
octosuite/core/__init__.py
Normal file
37
octosuite/core/api.py
Normal file
37
octosuite/core/api.py
Normal file
@@ -0,0 +1,37 @@
|
||||
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
|
||||
187
octosuite/core/models.py
Normal file
187
octosuite/core/models.py
Normal file
@@ -0,0 +1,187 @@
|
||||
from .api import Api, BASE_URL
|
||||
|
||||
|
||||
api = Api()
|
||||
|
||||
|
||||
class User:
|
||||
def __init__(self, name: str):
|
||||
|
||||
self.name = name
|
||||
self.endpoint = f"{BASE_URL}/users/{name}"
|
||||
|
||||
def profile(self) -> dict:
|
||||
profile = api.get(url=self.endpoint)
|
||||
return profile
|
||||
|
||||
def repos(self, page: int, per_page: int) -> list:
|
||||
params = {"page": page, "per_page": per_page}
|
||||
repos = api.get(url=f"{self.endpoint}/repos", params=params)
|
||||
return repos
|
||||
|
||||
def subscriptions(self, page: int, per_page: int) -> list:
|
||||
params = {"page": page, "per_page": per_page}
|
||||
subscriptions = api.get(url=f"{self.endpoint}/subscriptions", params=params)
|
||||
return subscriptions
|
||||
|
||||
def starred(self, page: int, per_page: int) -> list:
|
||||
params = {"page": page, "per_page": per_page}
|
||||
starred = api.get(url=f"{self.endpoint}/starred", params=params)
|
||||
return starred
|
||||
|
||||
def followers(self, page: int, per_page: int) -> list:
|
||||
params = {"page": page, "per_page": per_page}
|
||||
users = api.get(url=f"{self.endpoint}/followers", params=params)
|
||||
return users
|
||||
|
||||
def following(self, page: int, per_page: int) -> list:
|
||||
params = {"page": page, "per_page": per_page}
|
||||
users = api.get(url=f"{self.endpoint}/following", params=params)
|
||||
return users
|
||||
|
||||
def follows(self, user: str) -> bool: ...
|
||||
|
||||
def orgs(self, page: int, per_page: int) -> list:
|
||||
params = {"page": page, "per_page": per_page}
|
||||
orgs = api.get(url=f"{self.endpoint}/orgs", params=params)
|
||||
return orgs
|
||||
|
||||
def gists(self, page: int, per_page: int) -> list:
|
||||
params = {"page": page, "per_page": per_page}
|
||||
gists = api.get(url=f"{self.endpoint}/gists", params=params)
|
||||
return gists
|
||||
|
||||
def events(self, page: int, per_page: int) -> list:
|
||||
params = {"page": page, "per_page": per_page}
|
||||
events = api.get(url=f"{self.endpoint}/events", params=params)
|
||||
return events
|
||||
|
||||
def received_events(self, page: int, per_page: int) -> list:
|
||||
params = {"page": page, "per_page": per_page}
|
||||
received_events = api.get(url=f"{self.endpoint}/received_events", params=params)
|
||||
return received_events
|
||||
|
||||
|
||||
class Org:
|
||||
def __init__(self, name: str):
|
||||
|
||||
self.name = name
|
||||
self.endpoint = f"{BASE_URL}/orgs/{name}"
|
||||
|
||||
def profile(self) -> dict:
|
||||
profile = api.get(url=self.endpoint)
|
||||
return profile
|
||||
|
||||
def repos(self, page: int, per_page: int) -> list:
|
||||
params = {"page": page, "per_page": per_page}
|
||||
repos = api.get(url=f"{self.endpoint}/repos", params=params)
|
||||
return repos
|
||||
|
||||
def events(self, page: int, per_page: int) -> list:
|
||||
params = {"page": page, "per_page": per_page}
|
||||
events = api.get(url=f"{self.endpoint}/events", params=params)
|
||||
return events
|
||||
|
||||
def hooks(self, page: int, per_page: int) -> list:
|
||||
params = {"page": page, "per_page": per_page}
|
||||
hooks = api.get(url=f"{self.endpoint}/hooks", params=params)
|
||||
return hooks
|
||||
|
||||
def issues(self, page: int, per_page: int) -> list:
|
||||
params = {"page": page, "per_page": per_page}
|
||||
issues = api.get(url=f"{self.endpoint}/issues", params=params)
|
||||
return issues
|
||||
|
||||
def members(self, page: int, per_page: int) -> list:
|
||||
params = {"page": page, "per_page": per_page}
|
||||
members = api.get(url=f"{self.endpoint}/members", params=params)
|
||||
return members
|
||||
|
||||
|
||||
class Repo:
|
||||
def __init__(self, name: str, owner: str):
|
||||
self.name = name
|
||||
self.owner = owner
|
||||
self.endpoint = f"{BASE_URL}/repos/{owner}/{name}"
|
||||
|
||||
def profile(self) -> dict:
|
||||
profile = api.get(url=self.endpoint)
|
||||
return profile
|
||||
|
||||
def forks(self, page: int, per_page: int) -> list:
|
||||
params = {"page": page, "per_page": per_page}
|
||||
forks = api.get(url=f"{self.endpoint}/forks", params=params)
|
||||
return forks
|
||||
|
||||
def issue_events(self, page: int, per_page: int) -> list:
|
||||
params = {"page": page, "per_page": per_page}
|
||||
issue_events = api.get(url=f"{self.endpoint}/issue_events", params=params)
|
||||
return issue_events
|
||||
|
||||
def events(self, page: int, per_page: int) -> list:
|
||||
params = {"page": page, "per_page": per_page}
|
||||
events = api.get(url=f"{self.endpoint}/events", params=params)
|
||||
return events
|
||||
|
||||
def assignees(self, page: int, per_page: int) -> list:
|
||||
params = {"page": page, "per_page": per_page}
|
||||
assignees = api.get(url=f"{self.endpoint}/assignees", params=params)
|
||||
return assignees
|
||||
|
||||
def branches(self, page: int, per_page: int) -> list:
|
||||
params = {"page": page, "per_page": per_page}
|
||||
branches = api.get(url=f"{self.endpoint}/branches", params=params)
|
||||
return branches
|
||||
|
||||
def tags(self, page: int, per_page: int) -> list:
|
||||
params = {"page": page, "per_page": per_page}
|
||||
tags = api.get(url=f"{self.endpoint}/tags", params=params)
|
||||
return tags
|
||||
|
||||
def languages(self) -> dict:
|
||||
languages = api.get(url=f"{self.endpoint}/languages")
|
||||
return languages
|
||||
|
||||
def stargazers(self, page: int, per_page: int) -> list:
|
||||
params = {"page": page, "per_page": per_page}
|
||||
stargazers = api.get(url=f"{self.endpoint}/stargazers", params=params)
|
||||
return stargazers
|
||||
|
||||
def subscribers(self, page: int, per_page: int) -> list:
|
||||
params = {"page": page, "per_page": per_page}
|
||||
subscribers = api.get(url=f"{self.endpoint}/subscribers", params=params)
|
||||
return subscribers
|
||||
|
||||
def commits(self, page: int, per_page: int) -> list:
|
||||
params = {"page": page, "per_page": per_page}
|
||||
commits = api.get(url=f"{self.endpoint}/commits", params=params)
|
||||
return commits
|
||||
|
||||
def comments(self, page: int, per_page: int) -> list:
|
||||
params = {"page": page, "per_page": per_page}
|
||||
comments = api.get(url=f"{self.endpoint}/comments", params=params)
|
||||
return comments
|
||||
|
||||
def contents(self, path: str) -> list:
|
||||
contents = api.get(url=f"{self.endpoint}/contents/{path}")
|
||||
return contents
|
||||
|
||||
def issues(self, page: int, per_page: int) -> list:
|
||||
params = {"page": page, "per_page": per_page}
|
||||
issues = api.get(url=f"{self.endpoint}/issues", params=params)
|
||||
return issues
|
||||
|
||||
def releases(self, page: int, per_page: int) -> list:
|
||||
params = {"page": page, "per_page": per_page}
|
||||
releases = api.get(url=f"{self.endpoint}/releases", params=params)
|
||||
return releases
|
||||
|
||||
def deployments(self, page: int, per_page: int) -> list:
|
||||
params = {"page": page, "per_page": per_page}
|
||||
deployments = api.get(url=f"{self.endpoint}/deployments", params=params)
|
||||
return deployments
|
||||
|
||||
def labels(self, page: int, per_page: int) -> list:
|
||||
params = {"page": page, "per_page": per_page}
|
||||
labels = api.get(url=f"{self.endpoint}/labels", params=params)
|
||||
return labels
|
||||
13
octosuite/main.py
Normal file
13
octosuite/main.py
Normal file
@@ -0,0 +1,13 @@
|
||||
import sys
|
||||
|
||||
from .tui.menus import Menus
|
||||
from .tui.prompts import Prompts
|
||||
|
||||
|
||||
def start():
|
||||
try:
|
||||
prompts = Prompts()
|
||||
menu = Menus(prompts=prompts)
|
||||
menu.main()
|
||||
except KeyboardInterrupt:
|
||||
sys.exit()
|
||||
0
octosuite/tui/__init__.py
Normal file
0
octosuite/tui/__init__.py
Normal file
642
octosuite/tui/menus.py
Normal file
642
octosuite/tui/menus.py
Normal file
@@ -0,0 +1,642 @@
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import typing as t
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import pyfiglet
|
||||
import questionary as q
|
||||
from questionary import Style
|
||||
from rich.console import Console
|
||||
|
||||
from .prompts import Prompts
|
||||
from ..core.models import User, Repo, Org
|
||||
|
||||
CUSTOM_STYLE = Style(
|
||||
[
|
||||
("highlighted", "fg:black bold bg:white"),
|
||||
("instruction", "fg:gray"),
|
||||
]
|
||||
)
|
||||
|
||||
INSTRUCTIONS = "[Move] ⮃ [ENTER] ⮠"
|
||||
|
||||
POINTER = "🖛 "
|
||||
|
||||
console = Console()
|
||||
|
||||
|
||||
def clear_screen():
|
||||
"""Clear the terminal screen."""
|
||||
subprocess.run(["cls" if os.name == "nt" else "clear"])
|
||||
|
||||
|
||||
def banner(text: str):
|
||||
console.print(pyfiglet.figlet_format(text=text, font="chunky"))
|
||||
|
||||
|
||||
class Menus:
|
||||
def __init__(self, prompts: Prompts):
|
||||
# Define which methods require pagination
|
||||
self.paginated_methods = {
|
||||
"repos",
|
||||
"subscriptions",
|
||||
"starred",
|
||||
"followers",
|
||||
"following",
|
||||
"orgs",
|
||||
"gists",
|
||||
"events",
|
||||
"received_events",
|
||||
"forks",
|
||||
"issue_events",
|
||||
"assignees",
|
||||
"branches",
|
||||
"tags",
|
||||
"stargazers",
|
||||
"subscribers",
|
||||
"commits",
|
||||
"comments",
|
||||
"issues",
|
||||
"releases",
|
||||
"deployments",
|
||||
"labels",
|
||||
"hooks",
|
||||
"members",
|
||||
}
|
||||
self.prompts = prompts
|
||||
|
||||
# Methods that don't need pagination
|
||||
self.non_paginated_methods = {"profile", "languages"}
|
||||
|
||||
self.mode_handlers = {
|
||||
"user": self.user,
|
||||
"repo": self.repo,
|
||||
"org": self.org,
|
||||
"search": lambda: (
|
||||
print("Search functionality not yet implemented"),
|
||||
self.main(),
|
||||
),
|
||||
}
|
||||
|
||||
def main(self):
|
||||
"""Main menu to select mode."""
|
||||
clear_screen()
|
||||
try:
|
||||
banner(text="octosuite")
|
||||
|
||||
mode = q.select(
|
||||
"Select a mode to begin with",
|
||||
choices=[
|
||||
q.Choice(
|
||||
title="User",
|
||||
value="user",
|
||||
description="data about a user",
|
||||
shortcut_key="u",
|
||||
),
|
||||
q.Choice(
|
||||
title="Repo",
|
||||
value="repo",
|
||||
description="data about a repository",
|
||||
shortcut_key="r",
|
||||
),
|
||||
q.Choice(
|
||||
title="Org",
|
||||
value="org",
|
||||
description="data about an organisation",
|
||||
shortcut_key="o",
|
||||
),
|
||||
q.Choice(
|
||||
title="Search",
|
||||
value="search",
|
||||
description="Search GitHub",
|
||||
shortcut_key="s",
|
||||
),
|
||||
q.Choice(
|
||||
title="Check for updates",
|
||||
value="check_updates",
|
||||
description="Check for updates",
|
||||
shortcut_key="c",
|
||||
),
|
||||
q.Choice(
|
||||
title="Quit",
|
||||
value="quit",
|
||||
description="Close this session",
|
||||
shortcut_key="q",
|
||||
),
|
||||
],
|
||||
style=CUSTOM_STYLE,
|
||||
instruction=INSTRUCTIONS,
|
||||
pointer=POINTER,
|
||||
use_shortcuts=True,
|
||||
).ask()
|
||||
|
||||
if mode == "quit":
|
||||
if self.prompts.quit():
|
||||
sys.exit()
|
||||
else:
|
||||
self.main()
|
||||
else:
|
||||
handler = self.mode_handlers.get(mode)
|
||||
if handler:
|
||||
handler()
|
||||
else:
|
||||
self.main()
|
||||
except KeyboardInterrupt:
|
||||
sys.exit()
|
||||
|
||||
def _execute_action(self, instance, method_name, target: str):
|
||||
"""Execute a method on an instance, prompting for pagination if needed."""
|
||||
method = getattr(instance, method_name)
|
||||
|
||||
# pagination params if needed
|
||||
params = (
|
||||
self.prompts.pagination_params()
|
||||
if method_name in self.paginated_methods
|
||||
else {}
|
||||
)
|
||||
|
||||
# Execute with status spinner
|
||||
with console.status(f"Fetching {method_name} for {target}..."):
|
||||
return method(**params)
|
||||
|
||||
def _handle_navigation(self, option, callback, *callback_args):
|
||||
"""Handle navigation options (back, quit, change settings)."""
|
||||
navigation_handlers = {
|
||||
"back": lambda: self.main(),
|
||||
"quit": lambda: (
|
||||
sys.exit() if self.prompts.quit() else callback(*callback_args)
|
||||
),
|
||||
}
|
||||
|
||||
handler = navigation_handlers.get(option)
|
||||
if handler:
|
||||
handler()
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _preview_data(data: t.Union[dict, list]):
|
||||
"""Preview data as formatted JSON."""
|
||||
import json
|
||||
from rich.panel import Panel
|
||||
from rich.syntax import Syntax
|
||||
|
||||
if isinstance(data, dict):
|
||||
# Show the entire dict as JSON
|
||||
json_str = json.dumps(data, indent=2)
|
||||
syntax = Syntax(json_str, "json", line_numbers=False)
|
||||
console.print(
|
||||
Panel(
|
||||
renderable=syntax,
|
||||
border_style="#444444",
|
||||
)
|
||||
)
|
||||
|
||||
elif isinstance(data, list):
|
||||
if not data:
|
||||
console.print("[yellow]No data to preview[/yellow]")
|
||||
return
|
||||
|
||||
console.print(f"Total items: {len(data)}")
|
||||
console.print(f"Showing: first 5 items")
|
||||
|
||||
# Show first 5 items as JSON
|
||||
preview_data = data[:5]
|
||||
json_str = json.dumps(preview_data, indent=2)
|
||||
syntax = Syntax(json_str, "json", line_numbers=False)
|
||||
console.print(
|
||||
Panel(
|
||||
renderable=syntax,
|
||||
border_style="#444444",
|
||||
)
|
||||
)
|
||||
|
||||
else:
|
||||
console.print("[yellow]No data to preview[/yellow]")
|
||||
return
|
||||
|
||||
console.print()
|
||||
|
||||
def export(self, data: t.Union[dict, list], data_type: str, target: str) -> None:
|
||||
"""Export data to file in user-selected format(s)."""
|
||||
if not data:
|
||||
console.print(f"No data found for {target}")
|
||||
return
|
||||
|
||||
# Show what was found
|
||||
if isinstance(data, list):
|
||||
console.print(f"\n✓ Found {len(data)} valid {data_type} for {target}")
|
||||
else:
|
||||
console.print(f"\n✓ Found valid {data_type} data for {target}")
|
||||
|
||||
# Ask for export formats (multi-select)
|
||||
try:
|
||||
self._preview_data(data=data)
|
||||
|
||||
if not q.confirm(
|
||||
"Would you like to export this data?",
|
||||
default=True,
|
||||
).ask():
|
||||
console.print("[yellow]Skipping export.[/yellow]")
|
||||
return
|
||||
|
||||
file_formats = q.checkbox(
|
||||
"Select export format(s)",
|
||||
choices=[
|
||||
q.Choice(
|
||||
title="JSON",
|
||||
value="json",
|
||||
description="Export as JSON file",
|
||||
),
|
||||
q.Choice(
|
||||
title="CSV",
|
||||
value="csv",
|
||||
description="Export as CSV file",
|
||||
),
|
||||
q.Choice(
|
||||
title="HTML",
|
||||
value="html",
|
||||
description="Export as HTML table",
|
||||
),
|
||||
],
|
||||
style=CUSTOM_STYLE,
|
||||
instruction="[SPACE] ☑ [ENTER] ⮠ [CTRL+C] Skip",
|
||||
pointer=POINTER,
|
||||
validate=lambda x: len(x) > 0 or "Please select at least one format",
|
||||
).ask()
|
||||
|
||||
if not file_formats:
|
||||
console.print("[yellow]No formats selected. Skipping export.[/yellow]")
|
||||
return
|
||||
|
||||
import pandas as pd
|
||||
|
||||
# Create output directory if it doesn't exist
|
||||
output_dir = Path("../exports")
|
||||
output_dir.mkdir(exist_ok=True)
|
||||
|
||||
# Generate filename with timestamp
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
safe_target = target.replace("/", "_")
|
||||
filename = f"{safe_target}_{data_type}_{timestamp}"
|
||||
|
||||
# Convert data to DataFrame
|
||||
if isinstance(data, dict):
|
||||
# For dict data, convert to a single-row DataFrame
|
||||
df = pd.DataFrame([data])
|
||||
else:
|
||||
# For list data
|
||||
df = pd.DataFrame(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":
|
||||
df.to_json(filepath, orient="records", indent=2)
|
||||
elif file_format == "csv":
|
||||
df.to_csv(filepath, index=False)
|
||||
elif file_format == "html":
|
||||
df.to_html(filepath, border=1, classes="table table-striped")
|
||||
|
||||
exported_files.append(str(filepath))
|
||||
|
||||
# Show success message
|
||||
console.print("\n[green]✓ Export successful![/green]")
|
||||
for filepath in exported_files:
|
||||
console.print(f" • {filepath}")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\nExport cancelled")
|
||||
|
||||
def user(self, username: t.Optional[str] = None):
|
||||
"""User menu for querying user data."""
|
||||
clear_screen()
|
||||
try:
|
||||
if username is None:
|
||||
banner(text="user")
|
||||
username = self.prompts.prompt(
|
||||
message="GitHub Username", instruction="e.g., octocat"
|
||||
)
|
||||
|
||||
clear_screen()
|
||||
banner(text=username)
|
||||
|
||||
option = q.select(
|
||||
"Select an action",
|
||||
choices=[
|
||||
q.Choice(
|
||||
title="Profile", value="profile", description="Profile data"
|
||||
),
|
||||
q.Choice(
|
||||
title="Repositories",
|
||||
value="repos",
|
||||
description="Public repositories",
|
||||
),
|
||||
q.Choice(
|
||||
title="Subscriptions",
|
||||
value="subscriptions",
|
||||
description="Subscribed repositories",
|
||||
),
|
||||
q.Choice(
|
||||
title="Starred",
|
||||
value="starred",
|
||||
description="Starred repositories",
|
||||
),
|
||||
q.Choice(
|
||||
title="Followers",
|
||||
value="followers",
|
||||
description="Accounts that follow this user",
|
||||
),
|
||||
q.Choice(
|
||||
title="Following",
|
||||
value="following",
|
||||
description="Accounts that this user follows",
|
||||
),
|
||||
q.Choice(
|
||||
title="Organisations",
|
||||
value="orgs",
|
||||
description="This user's organisations",
|
||||
),
|
||||
q.Choice(
|
||||
title="Gists",
|
||||
value="gists",
|
||||
description="Gists (code snippets/files)",
|
||||
),
|
||||
q.Choice(title="events", value="events", description="Events"),
|
||||
q.Choice(
|
||||
title="Received events",
|
||||
value="received_events",
|
||||
description="This user's received events",
|
||||
),
|
||||
q.Choice(
|
||||
title="Change username",
|
||||
value="change_username",
|
||||
description="Query a different user",
|
||||
shortcut_key="u",
|
||||
),
|
||||
q.Choice(
|
||||
title="Back",
|
||||
value="back",
|
||||
description="Go back to previous menu",
|
||||
shortcut_key="b",
|
||||
),
|
||||
q.Choice(
|
||||
title="Quit",
|
||||
value="quit",
|
||||
description="Close this session",
|
||||
shortcut_key="q",
|
||||
),
|
||||
],
|
||||
style=CUSTOM_STYLE,
|
||||
instruction=INSTRUCTIONS,
|
||||
pointer=POINTER,
|
||||
use_shortcuts=True,
|
||||
).ask()
|
||||
|
||||
# Handle navigation
|
||||
if self._handle_navigation(option, self.user, username):
|
||||
return
|
||||
|
||||
# Handle change username
|
||||
if option == "change_username":
|
||||
self.user()
|
||||
return
|
||||
|
||||
# Execute action if it's a valid method
|
||||
valid_methods = self.paginated_methods | self.non_paginated_methods
|
||||
if option in valid_methods:
|
||||
user = User(name=username)
|
||||
data = self._execute_action(user, option, username)
|
||||
self.export(data=data, data_type=option, target=username)
|
||||
|
||||
self.user(username=username)
|
||||
except KeyboardInterrupt:
|
||||
print("\n\nReturning to main menu...")
|
||||
self.main()
|
||||
|
||||
def repo(self, name: t.Optional[str] = None, owner: t.Optional[str] = None):
|
||||
"""Repository menu for querying repo data."""
|
||||
clear_screen()
|
||||
try:
|
||||
if name is None or owner is None:
|
||||
banner(text="Repository")
|
||||
if name is None:
|
||||
name = self.prompts.prompt(
|
||||
message="GitHub Repo Name",
|
||||
instruction="e.g., spoon-knife",
|
||||
)
|
||||
if owner is None:
|
||||
owner = self.prompts.prompt(
|
||||
message="GitHub Repo Owner",
|
||||
instruction="e.g., octocat",
|
||||
)
|
||||
|
||||
clear_screen()
|
||||
banner(text=name)
|
||||
|
||||
option = q.select(
|
||||
"What would you like to do?",
|
||||
choices=[
|
||||
q.Choice(
|
||||
title="Change repo name",
|
||||
value="change_repo_name",
|
||||
description="Update the name of the repo",
|
||||
),
|
||||
q.Choice(
|
||||
title="Change owner name",
|
||||
value="change_owner",
|
||||
description="Update the name of repo owner",
|
||||
),
|
||||
q.Choice(
|
||||
title="Change both repo and owner names",
|
||||
value="change_both",
|
||||
description="Update both the repo and owner names",
|
||||
),
|
||||
q.Choice(
|
||||
title="Profile",
|
||||
value="profile",
|
||||
description="Repository data",
|
||||
),
|
||||
q.Choice(
|
||||
title="Forks", value="forks", description="Repository forks"
|
||||
),
|
||||
q.Choice(
|
||||
title="Issue events",
|
||||
value="issue_events",
|
||||
description="Issue events",
|
||||
),
|
||||
q.Choice(
|
||||
title="Events",
|
||||
value="events",
|
||||
description="Repository events",
|
||||
),
|
||||
q.Choice(
|
||||
title="Assignees",
|
||||
value="assignees",
|
||||
description="Assignees",
|
||||
),
|
||||
q.Choice(
|
||||
title="Branches", value="branches", description="Branches"
|
||||
),
|
||||
q.Choice(title="tags", value="tags", description="Tags"),
|
||||
q.Choice(
|
||||
title="Languages",
|
||||
value="languages",
|
||||
description="Programming languages",
|
||||
),
|
||||
q.Choice(
|
||||
title="Stargazers",
|
||||
value="stargazers",
|
||||
description="Users who starred this repo",
|
||||
),
|
||||
q.Choice(
|
||||
title="Subscribers",
|
||||
value="subscribers",
|
||||
description="Users subscribed to this repo",
|
||||
),
|
||||
q.Choice(title="Commits", value="commits", description="Commits"),
|
||||
q.Choice(
|
||||
title="Comments", value="comments", description="Comments"
|
||||
),
|
||||
q.Choice(title="Issues", value="issues", description="Issues"),
|
||||
q.Choice(
|
||||
title="Releases", value="releases", description="Releases"
|
||||
),
|
||||
q.Choice(
|
||||
title="Deployments",
|
||||
value="deployments",
|
||||
description="Deployments",
|
||||
),
|
||||
q.Choice(title="labels", value="labels", description="Labels"),
|
||||
q.Choice(
|
||||
title="Back",
|
||||
value="back",
|
||||
description="Go back to previous menu",
|
||||
shortcut_key="b",
|
||||
),
|
||||
q.Choice(
|
||||
title="Quit",
|
||||
value="quit",
|
||||
description="Close this session",
|
||||
shortcut_key="q",
|
||||
),
|
||||
],
|
||||
style=CUSTOM_STYLE,
|
||||
instruction=INSTRUCTIONS,
|
||||
pointer=POINTER,
|
||||
use_shortcuts=True,
|
||||
).ask()
|
||||
|
||||
# Handle navigation
|
||||
if self._handle_navigation(option, self.repo, name, owner):
|
||||
return
|
||||
|
||||
# Handle change options
|
||||
change_handlers = {
|
||||
"change_repo_name": lambda: self.repo(owner=owner),
|
||||
"change_owner": lambda: self.repo(name=name),
|
||||
"change_both": lambda: self.repo(),
|
||||
}
|
||||
|
||||
if option in change_handlers:
|
||||
change_handlers[option]()
|
||||
return
|
||||
|
||||
# Execute action if it's a valid method
|
||||
valid_methods = self.paginated_methods | self.non_paginated_methods
|
||||
if option in valid_methods:
|
||||
repo = Repo(name=name, owner=owner)
|
||||
target = f"{owner}/{name}"
|
||||
data = self._execute_action(repo, option, target)
|
||||
self.export(data=data, data_type=option, target=target)
|
||||
|
||||
self.repo(name=name, owner=owner)
|
||||
except KeyboardInterrupt:
|
||||
print("\n\nReturning to main menu...")
|
||||
self.main()
|
||||
|
||||
def org(self, name: t.Optional[str] = None):
|
||||
"""Organization menu for querying org data."""
|
||||
clear_screen()
|
||||
try:
|
||||
if name is None:
|
||||
banner(text="org")
|
||||
name = self.prompts.prompt(
|
||||
message="GitHub Organization Name",
|
||||
instruction="e.g, github",
|
||||
)
|
||||
|
||||
clear_screen()
|
||||
banner(text=name)
|
||||
|
||||
option = q.select(
|
||||
"What would you like to do?",
|
||||
choices=[
|
||||
q.Choice(
|
||||
title="Change org name",
|
||||
value="change_org",
|
||||
description="Query a different organization",
|
||||
),
|
||||
q.Choice(
|
||||
title="Profile", value="profile", description="Profile data"
|
||||
),
|
||||
q.Choice(
|
||||
title="Repositories",
|
||||
value="repos",
|
||||
description="Public repositories",
|
||||
),
|
||||
q.Choice(
|
||||
title="Events",
|
||||
value="events",
|
||||
description="Organization events",
|
||||
),
|
||||
q.Choice(title="Hooks", value="hooks", description="Webhooks"),
|
||||
q.Choice(title="issues", value="issues", description="Issues"),
|
||||
q.Choice(
|
||||
title="Members",
|
||||
value="members",
|
||||
description="Organization members",
|
||||
),
|
||||
q.Choice(
|
||||
title="Back",
|
||||
value="back",
|
||||
description="Go back to previous menu",
|
||||
shortcut_key="b",
|
||||
),
|
||||
q.Choice(
|
||||
title="Quit",
|
||||
value="quit",
|
||||
description="Close this session",
|
||||
shortcut_key="q",
|
||||
),
|
||||
],
|
||||
style=CUSTOM_STYLE,
|
||||
instruction=INSTRUCTIONS,
|
||||
pointer=POINTER,
|
||||
use_shortcuts=True,
|
||||
).ask()
|
||||
|
||||
# Handle navigation
|
||||
if self._handle_navigation(option, self.org, name):
|
||||
return
|
||||
|
||||
# Handle change org
|
||||
if option == "change_org":
|
||||
self.org()
|
||||
return
|
||||
|
||||
# Execute action if it's a valid method
|
||||
valid_methods = self.paginated_methods | self.non_paginated_methods
|
||||
if option in valid_methods:
|
||||
org = Org(name=name)
|
||||
data = self._execute_action(org, option, name)
|
||||
self.export(data=data, data_type=option, target=name)
|
||||
|
||||
self.org(name=name)
|
||||
except KeyboardInterrupt:
|
||||
print("\n\nReturning to main menu...")
|
||||
self.main()
|
||||
54
octosuite/tui/prompts.py
Normal file
54
octosuite/tui/prompts.py
Normal file
@@ -0,0 +1,54 @@
|
||||
import typing as t
|
||||
|
||||
import questionary as q
|
||||
from prompt_toolkit.styles import Style
|
||||
|
||||
|
||||
class Prompts:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def prompt(message: str, instruction: t.Optional[str] = None) -> str:
|
||||
return q.text(
|
||||
message=message,
|
||||
instruction=instruction,
|
||||
).ask()
|
||||
|
||||
@staticmethod
|
||||
def pagination_params() -> dict:
|
||||
"""Prompt user for pagination parameters."""
|
||||
try:
|
||||
page = q.text(message="Page", instruction="defaults to 1", qmark="#").ask()
|
||||
per_page = q.text(
|
||||
message="Per Page",
|
||||
instruction="default and max is 100",
|
||||
qmark="#",
|
||||
).ask()
|
||||
|
||||
return {
|
||||
"page": int(page) if page else 1,
|
||||
"per_page": min(int(per_page) if per_page else 100, 100),
|
||||
}
|
||||
|
||||
except (ValueError, TypeError):
|
||||
print("Invalid input, using defaults (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
|
||||
32
pyproject.toml
Normal file
32
pyproject.toml
Normal file
@@ -0,0 +1,32 @@
|
||||
[project]
|
||||
name = "octosuite"
|
||||
version = "4.0.0"
|
||||
description = "TUI-based toolkit for GitHub-data analysis."
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
authors = [
|
||||
{ name = "Ritchie Mwewa", email = "hi@rly0nheart.com" }
|
||||
]
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"rich>=14.2.0",
|
||||
"requests>=2.32.5",
|
||||
"questionary>=2.1.1",
|
||||
"pyfiglet>=1.0.4",
|
||||
"pandas>=2.3.3"
|
||||
]
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Intended Audience :: End Users/Desktop",
|
||||
"Operating System :: OS Independent",
|
||||
"Natural Language :: English"
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
homepage = "https://bellingcat.com"
|
||||
issues = "https://github.com/bellingcat/octosuite/issues"
|
||||
repository = "https://github.com/bellingcat/octosuite"
|
||||
|
||||
[project.scripts]
|
||||
octosuite = "octosuite.main:start"
|
||||
271
uv.lock
generated
Normal file
271
uv.lock
generated
Normal file
@@ -0,0 +1,271 @@
|
||||
version = 1
|
||||
revision = 2
|
||||
requires-python = ">=3.14"
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.11.12"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.4"
|
||||
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" }
|
||||
wheels = [
|
||||
{ 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/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/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" },
|
||||
{ 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 = "idna"
|
||||
version = "3.11"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
version = "4.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mdurl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mdurl"
|
||||
version = "0.1.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "numpy"
|
||||
version = "2.4.0"
|
||||
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" }
|
||||
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/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]]
|
||||
name = "octosuite"
|
||||
version = "4.0.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "pandas" },
|
||||
{ name = "pyfiglet" },
|
||||
{ name = "questionary" },
|
||||
{ name = "requests" },
|
||||
{ name = "rich" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "pandas", specifier = ">=2.3.3,<3.0.0" },
|
||||
{ name = "pyfiglet", specifier = ">=1.0.4,<2.0.0" },
|
||||
{ name = "questionary", specifier = ">=2.1.1,<3.0.0" },
|
||||
{ name = "requests", specifier = ">=2.32.5,<3.0.0" },
|
||||
{ name = "rich", specifier = ">=14.2.0,<15.0.0" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pandas"
|
||||
version = "2.3.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ 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 = [
|
||||
{ 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/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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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/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]]
|
||||
name = "prompt-toolkit"
|
||||
version = "3.0.52"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "wcwidth" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyfiglet"
|
||||
version = "1.0.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c8/e3/0a86276ad2c383ce08d76110a8eec2fe22e7051c4b8ba3fa163a0b08c428/pyfiglet-1.0.4.tar.gz", hash = "sha256:db9c9940ed1bf3048deff534ed52ff2dafbbc2cd7610b17bb5eca1df6d4278ef", size = 1560615, upload-time = "2025-08-15T18:32:47.302Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/5c/fe9f95abd5eaedfa69f31e450f7e2768bef121dbdf25bcddee2cd3087a16/pyfiglet-1.0.4-py3-none-any.whl", hash = "sha256:65b57b7a8e1dff8a67dc8e940a117238661d5e14c3e49121032bd404d9b2b39f", size = 1806118, upload-time = "2025-08-15T18:32:45.556Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.19.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dateutil"
|
||||
version = "2.9.0.post0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ 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 = [
|
||||
{ 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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "questionary"
|
||||
version = "2.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "prompt-toolkit" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f6/45/eafb0bba0f9988f6a2520f9ca2df2c82ddfa8d67c95d6625452e97b204a5/questionary-2.1.1.tar.gz", hash = "sha256:3d7e980292bb0107abaa79c68dd3eee3c561b83a0f89ae482860b181c8bd412d", size = 25845, upload-time = "2025-08-28T19:00:20.851Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl", hash = "sha256:a51af13f345f1cdea62347589fbb6df3b290306ab8930713bfae4d475a7d4a59", size = 36753, upload-time = "2025-08-28T19:00:19.56Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.32.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "charset-normalizer" },
|
||||
{ name = "idna" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rich"
|
||||
version = "14.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markdown-it-py" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.17.0"
|
||||
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" }
|
||||
wheels = [
|
||||
{ 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" },
|
||||
]
|
||||
|
||||
[[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 = [
|
||||
{ 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" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.6.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1e/24/a2a2ed9addd907787d7aa0355ba36a6cadf1768b934c652ea78acbd59dcd/urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797", size = 432930, upload-time = "2025-12-11T15:56:40.252Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd", size = 131182, upload-time = "2025-12-11T15:56:38.584Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wcwidth"
|
||||
version = "0.2.14"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293, upload-time = "2025-09-22T16:29:53.023Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" },
|
||||
]
|
||||
Reference in New Issue
Block a user