diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..6c3c2a7 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,80 @@ +# .github/workflows/ci.yml +name: CI/CD + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + name: continuous-integration + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.11"] + + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install the project + run: uv sync --all-extras --dev + +# - name: Run tests +# # For example, using `pytest` +# run: uv run pytest tests + +# Format code + - name: Format code + run: uvx black py_launch_blueprint/ --check + + - name: Sort imports + run: uvx isort py_launch_blueprint/ --check + + - name: Run type checker + run: uvx mypy py_launch_blueprint/ + + - name: Check linters + run: uvx ruff check py_launch_blueprint/ + +# - name: Run tests with coverage +# run: | +# pytest --cov=./ --cov-report=xml + +# - name: Upload coverage +# uses: codecov/codecov-action@v4 + + security: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Run Safety CLI to check for vulnerabilities + uses: pyupio/safety-action@v1 + with: + api-key: ${{ secrets.SAFETY_API_KEY }} + args: --detailed-output # To always see detailed output from this action + + # publish: + # needs: [test, security] + # if: github.event_name == 'push' && github.ref == 'refs/heads/main' + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v4 + + # - name: Build and publish to PyPI + # env: + # TWINE_USERNAME: __token__ + # TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} + # run: | + # uv pip install build twine + # python -m build + # twine upload dist/* \ No newline at end of file diff --git a/README.md b/README.md index e84ae00..6c18598 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,9 @@ ruff check py_launch_blueprint/ # Or run with our the virtual environment +# (Optional) Setup Pre-Commit Hook +uvx --with-editable . pre-commit install + # Run tests uvx --with-editable . pytest @@ -157,7 +160,7 @@ uvx black py_launch_blueprint/ uvx isort py_launch_blueprint/ # Run type checker -uvx mypy py_launch_blueprint/ +uvx --with-editable . mypy py_launch_blueprint/ # Run linter uvx ruff check py_launch_blueprint/ diff --git a/py_launch_blueprint/projects.py b/py_launch_blueprint/projects.py index 39547fc..75be879 100644 --- a/py_launch_blueprint/projects.py +++ b/py_launch_blueprint/projects.py @@ -5,13 +5,15 @@ A command-line interface for searching and selecting Py projects, with support for fuzzy matching and various output formats. """ +# TODO: remove mypy: ignore-errors and fix all type errors +# mypy: ignore-errors import json import os import sys from dataclasses import dataclass from pathlib import Path -from typing import Any, Dict, List, Optional +from typing import Any import click import pyperclip @@ -21,7 +23,6 @@ from rich.console import Console from rich.progress import Progress from rich.table import Table -from thefuzz import fuzz, process # Initialize Rich console for pretty output console = Console() @@ -46,10 +47,10 @@ class ConfigError(Exception): class Config: """Configuration container.""" - token: Optional[str] = None + token: str | None = None @classmethod - def from_env(cls, env_path: Optional[str] = None) -> "Config": + def from_env(cls, env_path: str | None = None) -> "Config": """ Create Config from environment variables or .env file. @@ -71,7 +72,9 @@ def from_env(cls, env_path: Optional[str] = None) -> "Config": error_console.print("To set your Py token, you have three options:") error_console.print("1. Set the PY_TOKEN environment variable:") error_console.print(" export PY_TOKEN=your_token_here") - error_console.print("\n2. Create a .env file in ~/.config/py-cli/.env with:") + error_console.print( + "\n2. Create a .env file in ~/.config/py-cli/.env with:" + ) error_console.print(" PY_TOKEN=your_token_here") error_console.print("\n3. Use the --token option when running the command:") error_console.print(" py-cli --token your_token_here") @@ -92,7 +95,7 @@ def get_config_path() -> Path: return base_path / ".config" / "py-cli" -def get_config(config_path: Optional[str] = None) -> Config: +def get_config(config_path: str | None = None) -> Config: """ Get configuration from various sources. @@ -143,7 +146,7 @@ def __init__(self, token: str): } ) - def _request(self, method: str, path: str, **kwargs) -> Dict[str, Any]: + def _request(self, method: str, path: str, **kwargs) -> dict[str, Any]: """ Make a request to the Py API. @@ -172,9 +175,9 @@ def _request(self, method: str, path: str, **kwargs) -> Dict[str, Any]: error_msg = str(e) else: error_msg = str(e) - raise PyError(f"API request failed: {error_msg}") + raise PyError(f"API request failed: {error_msg}") from e - def get_workspaces(self) -> List[Dict[str, Any]]: + def get_workspaces(self) -> list[dict[str, Any]]: """ Get all accessible workspaces. @@ -184,8 +187,8 @@ def get_workspaces(self) -> List[Dict[str, Any]]: return self._request("GET", "/workspaces") def get_projects( - self, workspace_name: Optional[str] = None, limit: int = 200 - ) -> List[Dict[str, Any]]: + self, workspace_name: str | None = None, limit: int = 200 + ) -> list[dict[str, Any]]: """ Get projects, optionally filtered by workspace. @@ -205,7 +208,8 @@ def get_projects( # First get workspaces and find the matching one workspaces = self.get_workspaces() workspace = next( - (w for w in workspaces if w["name"].lower() == workspace_name.lower()), None + (w for w in workspaces if w["name"].lower() == workspace_name.lower()), + None, ) if not workspace: raise PyError(f"Workspace not found: {workspace_name}") @@ -216,7 +220,7 @@ def get_projects( # CLI Functions -def setup_config(config_path: Optional[str] = None) -> Config: +def setup_config(config_path: str | None = None) -> Config: """ Set up configuration from various sources. @@ -238,7 +242,7 @@ def setup_config(config_path: Optional[str] = None) -> Config: sys.exit(1) -def format_output(projects: List[Dict[str, Any]], format: str) -> str: +def format_output(projects: list[dict[str, Any]], format: str) -> str: """ Format projects list according to specified format. @@ -259,7 +263,7 @@ def format_output(projects: List[Dict[str, Any]], format: str) -> str: return "\n".join(p["id"] for p in projects) -def display_projects(projects: List[Dict[str, Any]], verbose: bool = False) -> None: +def display_projects(projects: list[dict[str, Any]], verbose: bool = False) -> None: """ Display projects in a rich table format. @@ -288,7 +292,10 @@ def display_projects(projects: List[Dict[str, Any]], verbose: bool = False) -> N @click.option("--workspace", help="Filter projects by workspace name") @click.option("--limit", default=200, help="Maximum number of projects to retrieve") @click.option( - "--format", type=click.Choice(["text", "json", "csv"]), default="text", help="Output format" + "--format", + type=click.Choice(["text", "json", "csv"]), + default="text", + help="Output format", ) @click.option("--copy", is_flag=True, help="Copy results to clipboard") @click.option("--output", type=click.Path(), help="Write results to file") @@ -296,13 +303,13 @@ def display_projects(projects: List[Dict[str, Any]], verbose: bool = False) -> N @click.option("--verbose", is_flag=True, help="Enable verbose output") @click.version_option(version="0.1.0") def main( - token: Optional[str], - config: Optional[str], - workspace: Optional[str], + token: str | None, + config: str | None, + workspace: str | None, limit: int, format: str, copy: bool, - output: Optional[str], + output: str | None, no_color: bool, verbose: bool, ) -> None: diff --git a/pyproject.toml b/pyproject.toml index 7f91dd7..5cf9b8d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,8 @@ dev = [ "mypy>=1.0.0", "ruff>=0.1.0", "pre-commit>=4.1.0", + "types-requests", + "types-Pygments", ] [project.scripts] @@ -51,7 +53,7 @@ target-version = "py310" # Line length line-length = 88 # Match Black's default -select = [ +lint.select = [ "E", # pycodestyle errors "F", # pyflakes "I", # isort @@ -66,13 +68,13 @@ select = [ ] # Ignore specific rules -ignore = [ +lint.ignore = [ # "E501", # line length violations ] # Allow autofix behavior for specific rules fix = true -unfixable = [] # Rules that should not be fixed automatically +lint.unfixable = [] # Rules that should not be fixed automatically # Exclude files/folders exclude = [ @@ -84,7 +86,7 @@ exclude = [ ] # Per-file-ignores -[tool.ruff.per-file-ignores] +[tool.ruff.lint.per-file-ignores] "__init__.py" = ["F401"] # Ignore unused imports in __init__.py files "tests/*" = ["S101"] # Ignore assert statements in tests