Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/workflows/publish_dockerhub.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: CI Pipeline

on:
release:
types:
- created

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- name: Login Dockerhub
env:
DOCKER_USERNAME: ${{secrets.DOCKER_USERNAME}}
DOCKER_PASSWORD: ${{secrets.DOCKER_PASSWORD}}
run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD

- name: Build the Docker image
run: docker build -t mrsunglasses/bitssh .
- name: Push to Dockerhub
run: docker push mrsunglasses/bitssh:latest
8 changes: 4 additions & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ on:

jobs:
build:

runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]

steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
Expand All @@ -22,6 +22,6 @@ jobs:
- name: Create config mock
run: mkdir ~/.ssh && touch ~/.ssh/config
- name: Install dependencies
run: pip3 install . && pip3 install pytest
run: pip3 install -e . && pip3 install pytest
- name: Run Test
run: pytest
run: pytest
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ RUN python -m pip install --upgrade pip
RUN pip install bitssh

# Command to run when container starts
CMD ["bitssh"]
CMD ["bitssh"]
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Install from source

cd bitssh

python3 -m pip3 install .
python3 -m pip3 install -e .

bitssh
```
Expand Down
8 changes: 4 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
[build-system]
requires = ["setuptools>=61.0.0", "wheel"]
requires = ["setuptools>=80.9.0", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "bitssh"
version = "3.0.0"
version = "3.1.0"
description = "A New and Modern SSH connector written in Python."
readme = "README.md"
authors = [
Expand All @@ -23,7 +23,7 @@ classifiers = [ # List of https://pypi.org/classifiers/
]
keywords = ["bitssh", "sshmanager", "commandline"]
dependencies = [
"rich",
"rich>=14.0.0",
"pre-commit>=3.6.0",
"path>=16.9.0",
"InquirerPy>=0.3.4",
Expand All @@ -41,7 +41,7 @@ Docs = "https://github.com/Mr-Sunglasses/bitssh/blob/master/docs/docs.md"
bitssh = "bitssh.__main__:main"

[tool.bumpver]
current_version = "3.0.0"
current_version = "3.1.0"
version_pattern = "MAJOR.MINOR.PATCH"
commit_message = "Bump version {old_version} -> {new_version}"
commit = true
Expand Down
3 changes: 0 additions & 3 deletions setup.py

This file was deleted.

2 changes: 1 addition & 1 deletion src/bitssh/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
pass


__version__ = "3.0.0"
__version__ = "3.1.0"
1 change: 1 addition & 0 deletions src/bitssh/argument_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ def __init__(self) -> None:
)
parser.add_argument(
"--version",
"-v",
action="store_true",
default=False,
help="Show the bitssh version.",
Expand Down
2 changes: 0 additions & 2 deletions src/bitssh/cli.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import os

from bitssh import __version__

from .argument_parser import Config
Expand Down
25 changes: 16 additions & 9 deletions src/bitssh/prompt.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,38 @@
import os
import subprocess
from typing import Dict, List, Optional

from InquirerPy import inquirer

from .ui import console
from .utils import ConfigPathUtility
from .utils import get_config_file_host_data


def ask_host_prompt():
HOST: List[str] = ConfigPathUtility.get_config_file_host_data()
questions: List[List] = inquirer.fuzzy(
HOST: List[str] = get_config_file_host_data()
questions = inquirer.fuzzy(
message="Select the Host Given in the Above List: ",
choices=HOST,
)
try:
answers: Optional[Dict[str, str]] = questions.execute()
answers = questions.execute()
if answers is None:
return

cmd: str = answers
cmd = cmd[7::]
cmd = f"ssh {cmd}"
os.system("cls" if os.name == "nt" else "clear")
try:
_cmd_exec_data = cmd.split('-> ')[1] # clean the data from answers
except IndexError:
raise ValueError("Invalid format: expected '-> ' delimiter in the answer.")
if os.name == "nt": # Windows
subprocess.run(["cls"], shell=True, check=True)
else: # Unix-like systems
subprocess.run(["clear"], check=True)
console.print(
"Please Wait While Your System is Connecting to the Remote Server 🖥️",
style="green",
)
os.system(cmd)
subprocess.run(["ssh", _cmd_exec_data], check=True)
except subprocess.CalledProcessError as e:
print(f"Error: {e.stderr}")
except Exception as Error:
print(f"\nInterrupted by {Error}")
6 changes: 4 additions & 2 deletions src/bitssh/ui.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
from rich.console import Console
from rich.table import Table
from .utils import ConfigPathUtility

from .utils import get_config_file_row_data

console = Console()


def draw_table():
table: Table = Table(title="SSH Servers in Config File 📁")
table.add_column("Hostname", justify="left", style="cyan", no_wrap=True)
table.add_column("Host", style="magenta")
table.add_column("Port", justify="right", style="green")
table.add_column("User", justify="right", style="yellow")

for i in ConfigPathUtility.get_config_file_row_data():
for i in get_config_file_row_data():
table.add_row(i[0], i[1], i[2], i[3])

console.print(table)
90 changes: 45 additions & 45 deletions src/bitssh/utils.py
Original file line number Diff line number Diff line change
@@ -1,62 +1,62 @@
import os
import re
from typing import Dict, List, Match, Optional, Pattern, Tuple
from typing import Dict, List, Tuple

from path import Path
CONFIG_FILE_PATH: str = os.path.expanduser("~/.ssh/config")


class ConfigPathUtility:
config_file_path: str = os.path.expanduser("~/.ssh/config")
def _validate_config_file() -> None:
if not os.path.exists(CONFIG_FILE_PATH):
raise FileNotFoundError(
f"Config file not found at {CONFIG_FILE_PATH}. "
"Please see the documentation for bitssh."
)

@classmethod
def _validate_config_file(cls) -> None:
if not os.path.exists(cls.config_file_path):
raise FileNotFoundError(
f"Config file not found at {cls.config_file_path}. Please see the documentation for bitssh."
)

@classmethod
def get_config_content(cls) -> Dict[str, Dict[str, str]]:
cls._validate_config_file()
def get_config_content():
_validate_config_file()

with open(cls.config_file_path, "r") as file:
lines = file.read()
with open(CONFIG_FILE_PATH, "r") as file:
lines = file.read()

host_pattern: Pattern[str] = re.compile(r"Host\s+(\w+)", re.MULTILINE)
hostname_pattern: Pattern[str] = re.compile(
r"(?:HostName|Hostname)\s+(\S+)", re.MULTILINE
)
user_pattern: Pattern[str] = re.compile(r"User\s+(\S+)", re.MULTILINE)
host_pattern = re.compile(r"Host\s+(\w+)", re.MULTILINE)
hostname_pattern = re.compile(r"(?:HostName|Hostname)\s+(\S+)", re.MULTILINE)
user_pattern = re.compile(r"User\s+(\S+)", re.MULTILINE)
port_pattern = re.compile(r"port\s+(\d+)", re.MULTILINE | re.IGNORECASE)

host_dict = {}
for match in host_pattern.finditer(lines):
host = match.group(1)
host_end = match.end()

hostname_match = hostname_pattern.search(lines, host_end)
hostname = hostname_match.group(1) if hostname_match else host

host_dict: Dict[str, Dict[str, str]] = {}
for match in host_pattern.finditer(lines):
host: str = match.group(1)
hostname_match: Optional[Match[str]] = hostname_pattern.search(
lines, match.end()
)
hostname: str = hostname_match.group(1) if hostname_match else host
user_match = user_pattern.search(lines, host_end)
user = user_match.group(1) if user_match else None

user_match: Optional[Match[str]] = user_pattern.search(lines, match.end())
user: Optional[str] = user_match.group(1) if user_match else None
port_match = port_pattern.search(lines, host_end)
port = port_match.group(1) if port_match else "22"

host_dict[host]: Dict[str, str] = {
"Hostname": hostname,
"User": user,
}
host_dict[host] = {
"Hostname": hostname,
"User": user,
"Port": port,
}

return host_dict
return host_dict

@classmethod
def get_config_file_row_data(cls) -> List[Tuple[str, str, str, str]]:
config_content = cls.get_config_content()

rows: List[Tuple[str, str, str, str]] = [
(attributes["Hostname"], host, "22", attributes["User"])
for host, attributes in config_content.items()
]
def get_config_file_row_data():
config_content = get_config_content()
rows = []
for host, attributes in config_content.items():
hostname = attributes["Hostname"]
user = attributes["User"]
port = attributes["Port"]
rows.append((hostname, host, port, user))
return rows

return rows

@classmethod
def get_config_file_host_data(cls) -> List[str]:
return [f"🖥️ -> {host[1]}" for host in cls.get_config_file_row_data()]
def get_config_file_host_data() -> List[str]:
return [f"🖥️ -> {row[1]}" for row in get_config_file_row_data()]
Loading