diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e45dd60 --- /dev/null +++ b/.gitignore @@ -0,0 +1,184 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be added to the global gitignore or merged into this project gitignore. For a PyCharm +# project, it is recommended to include the following files in version control: +# - .idea/modules.xml +# - .idea/*.iml +# - .idea/misc.xml +# - .idea/vcs.xml +.idea/ + +# VS Code +.vscode/ + +# Ruff +.ruff_cache/ + +# ComfyUI specific +*.db +*.sqlite +queue_data/ +logs/ + +# OS specific +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..c55a63b --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,34 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + - id: check-case-conflict + - id: check-merge-conflict + - id: check-json + - id: pretty-format-json + args: ['--autofix'] + - id: check-toml + - id: check-xml + - id: debug-statements + - id: check-docstring-first + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.8 + hooks: + # Run the linter. + - id: ruff + args: [--fix] + # Run the formatter. + - id: ruff-format + + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.7.1 + hooks: + - id: mypy + additional_dependencies: [types-all] + args: [--ignore-missing-imports, --no-strict-optional] + exclude: ^(tests/|web/) \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9035b1f --- /dev/null +++ b/Makefile @@ -0,0 +1,40 @@ +.PHONY: help install install-dev format lint check test clean setup-hooks + +help: ## Show this help message + @echo 'Usage: make [target]' + @echo '' + @echo 'Targets:' + @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " %-15s %s\n", $$1, $$2}' $(MAKEFILE_LIST) + +install: ## Install production dependencies + pip install -r requirements.txt + +install-dev: ## Install development dependencies + pip install -e ".[dev]" + +setup-hooks: ## Install pre-commit hooks + pre-commit install + pre-commit install --hook-type commit-msg + +format: ## Format code with ruff + ruff format . + ruff check --fix . + +lint: ## Lint code with ruff + ruff check . + +check: ## Run all checks (lint + format check) + ruff check . + ruff format --check . + +test: ## Run tests (placeholder for future implementation) + @echo "Tests will be implemented in future tasks" + +clean: ## Clean up cache files + find . -type f -name "*.pyc" -delete + find . -type d -name "__pycache__" -delete + find . -type d -name "*.egg-info" -exec rm -rf {} + + find . -type d -name ".ruff_cache" -exec rm -rf {} + + +ci: check ## Run CI checks + @echo "All checks passed!" \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..19879cc --- /dev/null +++ b/README.md @@ -0,0 +1,122 @@ +# AC ComfyUI Queue Manager + +A comprehensive queue management system for ComfyUI workflows with persistence, control features, and a web-based interface. + +## Features + +- Queue workflow executions with persistence +- Pause/resume queue processing +- Archive and restore completed workflows +- Web-based management interface +- Export/import queue configurations +- Filtering and search capabilities + +## Development Setup + +### Prerequisites + +- Python 3.8+ +- ComfyUI installation + +### Installation + +1. Clone or copy this custom node to your ComfyUI custom_nodes directory: + ```bash + cd ComfyUI/custom_nodes/ + # Copy the comfyui-queue-manager directory here + ``` + +2. Set up the development environment (recommended): + ```bash + cd comfyui-queue-manager + python setup.py + ``` + + Or manually install dependencies: + ```bash + make install-dev + make setup-hooks + ``` + +### Development Commands + +- **Format code**: `make format` +- **Lint code**: `make lint` +- **Run all checks**: `make check` +- **Clean cache files**: `make clean` +- **Show help**: `make help` + +### Code Quality + +This project uses modern Python tooling for code quality: + +- **Ruff** - Fast Python linter and formatter (replaces flake8, black, isort, and more) +- **Pre-commit hooks** - Automatic code quality checks before commits +- **MyPy** - Optional static type checking + +#### Ruff Configuration + +The project is configured with: +- Modern Python 3.8+ syntax (using `|` for unions instead of `Union`) +- PEP 585 collection types (`list[str]` instead of `List[str]`) +- Comprehensive rule set covering code style, security, and best practices +- ComfyUI-specific exceptions (e.g., allowing uppercase method names like `INPUT_TYPES`) + +#### Pre-commit Hooks + +The hooks automatically: +- Format code with Ruff +- Fix common linting issues +- Check for trailing whitespace and file endings +- Validate YAML, JSON, and TOML files +- Check for merge conflicts and large files +- Run type checking with MyPy + +#### Manual Commands + +```bash +# Format code +make format + +# Check for issues +make lint + +# Run all checks +make check + +# Clean cache files +make clean +``` + +### Project Structure + +``` +comfyui-queue-manager/ +├── __init__.py # Node registration +├── queue_manager_node.py # Main ComfyUI custom node +├── models.py # Data models and enums +├── interfaces.py # Abstract interfaces +├── web/ # Frontend assets +│ ├── index.html +│ ├── queue_manager.js +│ └── styles.css +├── pyproject.toml # Project configuration +├── .pre-commit-config.yaml # Pre-commit hooks +├── Makefile # Development commands +└── README.md # This file +``` + +## Usage + +After installation, the Queue Manager node will be available in ComfyUI under the "Queue Management" category. The web interface can be accessed through ComfyUI's web server. + +## Contributing + +1. Make sure pre-commit hooks are installed: `make setup-hooks` +2. Format your code: `make format` +3. Run checks: `make check` +4. Commit your changes (pre-commit hooks will run automatically) + +## License + +MIT License \ No newline at end of file diff --git a/__init__.py b/__init__.py index 429bdb8..ab717be 100644 --- a/__init__.py +++ b/__init__.py @@ -5,12 +5,8 @@ A comprehensive queue management system for ComfyUI workflows. from .queue_manager_node import QueueManagerNode -NODE_CLASS_MAPPINGS = { - "QueueManagerNode": QueueManagerNode -} +NODE_CLASS_MAPPINGS = {"QueueManagerNode": QueueManagerNode} -NODE_DISPLAY_NAME_MAPPINGS = { - "QueueManagerNode": "Queue Manager" -} +NODE_DISPLAY_NAME_MAPPINGS = {"QueueManagerNode": "Queue Manager"} -__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"] \ No newline at end of file +__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"] diff --git a/interfaces.py b/interfaces.py index de8933f..393fc7a 100644 --- a/interfaces.py +++ b/interfaces.py @@ -2,9 +2,13 @@ Base interfaces for queue service and database operations. """ +from __future__ import annotations + from abc import ABC, abstractmethod -from typing import List, Optional, Dict, Any -from .models import QueueItem, QueueFilter, QueueConfig, QueueStatus, QueueState +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from .models import QueueConfig, QueueFilter, QueueItem, QueueState, QueueStatus class DatabaseInterface(ABC): @@ -13,164 +17,139 @@ class DatabaseInterface(ABC): @abstractmethod def initialize(self) -> bool: """Initialize the database and create tables if needed.""" - pass @abstractmethod def create_queue_item(self, item: QueueItem) -> bool: """Create a new queue item in the database.""" - pass @abstractmethod - def get_queue_item(self, item_id: str) -> Optional[QueueItem]: + def get_queue_item(self, item_id: str) -> QueueItem | None: """Retrieve a queue item by ID.""" - pass @abstractmethod - def get_all_queue_items(self) -> List[QueueItem]: + def get_all_queue_items(self) -> list[QueueItem]: """Retrieve all queue items.""" - pass @abstractmethod def update_queue_item(self, item: QueueItem) -> bool: """Update an existing queue item.""" - pass @abstractmethod def delete_queue_item(self, item_id: str) -> bool: """Delete a queue item by ID.""" - pass @abstractmethod - def get_items_by_status(self, status: QueueStatus) -> List[QueueItem]: + def get_items_by_status(self, status: QueueStatus) -> list[QueueItem]: """Retrieve queue items by status.""" - pass @abstractmethod - def filter_queue_items(self, filter_criteria: QueueFilter) -> List[QueueItem]: + def filter_queue_items(self, filter_criteria: QueueFilter) -> list[QueueItem]: """Filter queue items based on criteria.""" - pass @abstractmethod - def bulk_update_status(self, item_ids: List[str], status: QueueStatus) -> bool: + def bulk_update_status(self, item_ids: list[str], status: QueueStatus) -> bool: """Update status for multiple items.""" - pass @abstractmethod - def get_config(self, key: str) -> Optional[str]: + def get_config(self, key: str) -> str | None: """Get a configuration value.""" - pass @abstractmethod def set_config(self, key: str, value: str) -> bool: """Set a configuration value.""" - pass @abstractmethod def close(self) -> None: """Close the database connection.""" - pass class QueueServiceInterface(ABC): """Abstract interface for queue service operations.""" @abstractmethod - def add_workflow(self, workflow_data: Dict[str, Any], workflow_name: str = "") -> str: + def add_workflow( + self, workflow_data: dict[str, Any], workflow_name: str = "" + ) -> str: """Add a workflow to the queue and return the item ID.""" - pass @abstractmethod - def get_queue_items(self, status: Optional[QueueStatus] = None) -> List[QueueItem]: + def get_queue_items(self, status: QueueStatus | None = None) -> list[QueueItem]: """Get queue items, optionally filtered by status.""" - pass @abstractmethod - def get_queue_item(self, item_id: str) -> Optional[QueueItem]: + def get_queue_item(self, item_id: str) -> QueueItem | None: """Get a specific queue item by ID.""" - pass @abstractmethod - def update_item_status(self, item_id: str, status: QueueStatus, - error_message: Optional[str] = None, - result_data: Optional[Dict[str, Any]] = None) -> bool: + def update_item_status( + self, + item_id: str, + status: QueueStatus, + error_message: str | None = None, + result_data: dict[str, Any] | None = None, + ) -> bool: """Update the status of a queue item.""" - pass @abstractmethod - def archive_items(self, item_ids: List[str]) -> bool: + def archive_items(self, item_ids: list[str]) -> bool: """Archive the specified queue items.""" - pass @abstractmethod - def restore_items(self, item_ids: List[str]) -> bool: + def restore_items(self, item_ids: list[str]) -> bool: """Restore archived queue items.""" - pass @abstractmethod - def delete_items(self, item_ids: List[str]) -> bool: + def delete_items(self, item_ids: list[str]) -> bool: """Delete the specified queue items.""" - pass @abstractmethod def pause_queue(self) -> bool: """Pause queue processing.""" - pass @abstractmethod def resume_queue(self) -> bool: """Resume queue processing.""" - pass @abstractmethod def get_queue_state(self) -> QueueState: """Get the current queue processing state.""" - pass @abstractmethod - def export_queue(self, item_ids: Optional[List[str]] = None) -> Dict[str, Any]: + def export_queue(self, item_ids: list[str] | None = None) -> dict[str, Any]: """Export queue items to a dictionary format.""" - pass @abstractmethod - def import_queue(self, queue_data: Dict[str, Any], merge: bool = True) -> bool: + def import_queue(self, queue_data: dict[str, Any], merge: bool = True) -> bool: """Import queue items from a dictionary format.""" - pass @abstractmethod - def filter_items(self, filter_criteria: QueueFilter) -> List[QueueItem]: + def filter_items(self, filter_criteria: QueueFilter) -> list[QueueItem]: """Filter queue items based on criteria.""" - pass @abstractmethod def get_config(self) -> QueueConfig: """Get the current queue configuration.""" - pass @abstractmethod def update_config(self, config: QueueConfig) -> bool: """Update the queue configuration.""" - pass class WorkflowExecutorInterface(ABC): """Abstract interface for workflow execution integration.""" @abstractmethod - def execute_workflow(self, workflow_data: Dict[str, Any]) -> Dict[str, Any]: + def execute_workflow(self, workflow_data: dict[str, Any]) -> dict[str, Any]: """Execute a workflow and return the results.""" - pass @abstractmethod def is_workflow_running(self, workflow_id: str) -> bool: """Check if a workflow is currently running.""" - pass @abstractmethod def cancel_workflow(self, workflow_id: str) -> bool: """Cancel a running workflow.""" - pass @abstractmethod - def get_workflow_status(self, workflow_id: str) -> Optional[QueueStatus]: + def get_workflow_status(self, workflow_id: str) -> QueueStatus | None: """Get the status of a workflow execution.""" - pass \ No newline at end of file diff --git a/models.py b/models.py index c440ece..a699eb5 100644 --- a/models.py +++ b/models.py @@ -2,15 +2,18 @@ Core data models and enums for the ComfyUI Queue Manager. """ +from __future__ import annotations + from dataclasses import dataclass, field -from datetime import datetime +from datetime import datetime, timezone from enum import Enum -from typing import Optional, Dict, Any, List +from typing import Any import uuid class QueueStatus(Enum): """Enumeration of possible queue item statuses.""" + PENDING = "pending" RUNNING = "running" COMPLETED = "completed" @@ -20,6 +23,7 @@ class QueueStatus(Enum): class QueueState(Enum): """Enumeration of queue processing states.""" + RUNNING = "running" PAUSED = "paused" @@ -27,90 +31,115 @@ class QueueState(Enum): @dataclass class QueueItem: """Data model for a queue item representing a workflow execution.""" + id: str = field(default_factory=lambda: str(uuid.uuid4())) workflow_name: str = "" - workflow_data: Dict[str, Any] = field(default_factory=dict) + workflow_data: dict[str, Any] = field(default_factory=dict) status: QueueStatus = QueueStatus.PENDING - created_at: datetime = field(default_factory=datetime.now) - updated_at: datetime = field(default_factory=datetime.now) - started_at: Optional[datetime] = None - completed_at: Optional[datetime] = None - error_message: Optional[str] = None - result_data: Optional[Dict[str, Any]] = None + created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) + updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) + started_at: datetime | None = None + completed_at: datetime | None = None + error_message: str | None = None + result_data: dict[str, Any] | None = None - def to_dict(self) -> Dict[str, Any]: + def to_dict(self) -> dict[str, Any]: """Convert the queue item to a dictionary for serialization.""" return { - 'id': self.id, - 'workflow_name': self.workflow_name, - 'workflow_data': self.workflow_data, - 'status': self.status.value, - 'created_at': self.created_at.isoformat(), - 'updated_at': self.updated_at.isoformat(), - 'started_at': self.started_at.isoformat() if self.started_at else None, - 'completed_at': self.completed_at.isoformat() if self.completed_at else None, - 'error_message': self.error_message, - 'result_data': self.result_data + "id": self.id, + "workflow_name": self.workflow_name, + "workflow_data": self.workflow_data, + "status": self.status.value, + "created_at": self.created_at.isoformat(), + "updated_at": self.updated_at.isoformat(), + "started_at": self.started_at.isoformat() if self.started_at else None, + "completed_at": self.completed_at.isoformat() + if self.completed_at + else None, + "error_message": self.error_message, + "result_data": self.result_data, } @classmethod - def from_dict(cls, data: Dict[str, Any]) -> 'QueueItem': + def from_dict(cls, data: dict[str, Any]) -> QueueItem: """Create a QueueItem from a dictionary.""" item = cls() - item.id = data.get('id', str(uuid.uuid4())) - item.workflow_name = data.get('workflow_name', '') - item.workflow_data = data.get('workflow_data', {}) - item.status = QueueStatus(data.get('status', QueueStatus.PENDING.value)) - item.created_at = datetime.fromisoformat(data['created_at']) if data.get('created_at') else datetime.now() - item.updated_at = datetime.fromisoformat(data['updated_at']) if data.get('updated_at') else datetime.now() - item.started_at = datetime.fromisoformat(data['started_at']) if data.get('started_at') else None - item.completed_at = datetime.fromisoformat(data['completed_at']) if data.get('completed_at') else None - item.error_message = data.get('error_message') - item.result_data = data.get('result_data') + item.id = data.get("id", str(uuid.uuid4())) + item.workflow_name = data.get("workflow_name", "") + item.workflow_data = data.get("workflow_data", {}) + item.status = QueueStatus(data.get("status", QueueStatus.PENDING.value)) + item.created_at = ( + datetime.fromisoformat(data["created_at"]) + if data.get("created_at") + else datetime.now(timezone.utc) + ) + item.updated_at = ( + datetime.fromisoformat(data["updated_at"]) + if data.get("updated_at") + else datetime.now(timezone.utc) + ) + item.started_at = ( + datetime.fromisoformat(data["started_at"]) + if data.get("started_at") + else None + ) + item.completed_at = ( + datetime.fromisoformat(data["completed_at"]) + if data.get("completed_at") + else None + ) + item.error_message = data.get("error_message") + item.result_data = data.get("result_data") return item @dataclass class QueueFilter: """Data model for filtering queue items.""" - status: Optional[List[QueueStatus]] = None - workflow_name: Optional[str] = None - date_range: Optional[tuple[datetime, datetime]] = None - search_term: Optional[str] = None - def to_dict(self) -> Dict[str, Any]: + status: list[QueueStatus] | None = None + workflow_name: str | None = None + date_range: tuple[datetime, datetime] | None = None + search_term: str | None = None + + def to_dict(self) -> dict[str, Any]: """Convert the filter to a dictionary.""" return { - 'status': [s.value for s in self.status] if self.status else None, - 'workflow_name': self.workflow_name, - 'date_range': [d.isoformat() for d in self.date_range] if self.date_range else None, - 'search_term': self.search_term + "status": [s.value for s in self.status] if self.status else None, + "workflow_name": self.workflow_name, + "date_range": [d.isoformat() for d in self.date_range] + if self.date_range + else None, + "search_term": self.search_term, } @dataclass class QueueConfig: """Configuration settings for the queue manager.""" + queue_state: QueueState = QueueState.RUNNING max_concurrent_workflows: int = 1 auto_archive_completed: bool = False auto_archive_days: int = 30 - def to_dict(self) -> Dict[str, Any]: + def to_dict(self) -> dict[str, Any]: """Convert the configuration to a dictionary.""" return { - 'queue_state': self.queue_state.value, - 'max_concurrent_workflows': self.max_concurrent_workflows, - 'auto_archive_completed': self.auto_archive_completed, - 'auto_archive_days': self.auto_archive_days + "queue_state": self.queue_state.value, + "max_concurrent_workflows": self.max_concurrent_workflows, + "auto_archive_completed": self.auto_archive_completed, + "auto_archive_days": self.auto_archive_days, } @classmethod - def from_dict(cls, data: Dict[str, Any]) -> 'QueueConfig': + def from_dict(cls, data: dict[str, Any]) -> QueueConfig: """Create a QueueConfig from a dictionary.""" config = cls() - config.queue_state = QueueState(data.get('queue_state', QueueState.RUNNING.value)) - config.max_concurrent_workflows = data.get('max_concurrent_workflows', 1) - config.auto_archive_completed = data.get('auto_archive_completed', False) - config.auto_archive_days = data.get('auto_archive_days', 30) - return config \ No newline at end of file + config.queue_state = QueueState( + data.get("queue_state", QueueState.RUNNING.value) + ) + config.max_concurrent_workflows = data.get("max_concurrent_workflows", 1) + config.auto_archive_completed = data.get("auto_archive_completed", False) + config.auto_archive_days = data.get("auto_archive_days", 30) + return config diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..424ab6c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,115 @@ +[build-system] +requires = ["setuptools>=45", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "ac-comfyui-queue-manager" +version = "0.1.0" +description = "A comprehensive queue management system for ComfyUI workflows" +authors = [{name = "Abdullah Ceylan"}] +license = {text = "MIT"} +requires-python = ">=3.8" +dependencies = [ + "flask>=2.3.0", +] + +[project.optional-dependencies] +dev = [ + "ruff>=0.1.0", + "pre-commit>=3.0.0", +] + +[project.urls] +bugs = "https://github.com/abdullahceylan/ac-comfyui-queue-manager/issues" +homepage = "https://github.com/abdullahceylan/ac-comfyui-queue-manager" +Repository = "https://github.com/abdullahceylan/ac-comfyui-queue-manager" + +[tool.comfy] +PublisherId = "abdullahceylan" +DisplayName = "AC ComfyUI Queue Manager" +Icon = "https://raw.githubusercontent.com/abdullahceylan/ac-comfyui-queue-manager/refs/heads/main/queue-icon.svg" + +[tool.ruff] + +# Exclude a variety of commonly ignored directories. +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".mypy_cache", + ".nox", + ".pants.d", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "venv", +] + +# Same as Black. +line-length = 88 +indent-width = 4 + +# Assume Python 3.8+ +target-version = "py38" + +[tool.ruff.format] +# Like Black, use double quotes for strings. +quote-style = "double" + +# Like Black, indent with spaces, rather than tabs. +indent-style = "space" + +# Like Black, respect magic trailing commas. +skip-magic-trailing-comma = false + +# Like Black, automatically detect the appropriate line ending. +line-ending = "auto" + +[tool.ruff.lint] +# Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default. +select = ["E", "F", "W", "I", "N", "UP", "YTT", "ANN", "S", "BLE", "FBT", "B", "A", "COM", "C4", "DTZ", "T10", "DJ", "EM", "EXE", "FA", "ISC", "ICN", "G", "INP", "PIE", "T20", "PYI", "PT", "Q", "RSE", "RET", "SLF", "SLOT", "SIM", "TID", "TCH", "INT", "ARG", "PTH", "TD", "FIX", "ERA", "PD", "PGH", "PL", "TRY", "FLY", "NPY", "AIR", "PERF", "FURB", "LOG", "RUF"] + +# Ignore specific rules that might be too strict for this project +ignore = [ + "D", # pydocstyle (we'll enable specific ones we want) + "COM812", # Trailing comma missing (conflicts with formatter) + "ISC001", # Implicitly concatenated string literals (conflicts with formatter) + "S101", # Use of assert detected (common in tests) + "T201", # print found (useful for debugging) + "FBT001", # Boolean positional arg in function definition + "FBT002", # Boolean default value in function definition + "ARG002", # Unused method argument (common in interfaces) + "PLR0913", # Too many arguments to function call + "PLR2004", # Magic value used in comparison +] + +# Allow fix for all enabled rules (when `--fix`) is provided. +fixable = ["ALL"] +unfixable = [] + +[tool.ruff.lint.per-file-ignores] +# Tests can use magic values, assertions, and fixtures +"test_*.py" = ["PLR2004", "S101", "ARG001"] +"**/tests/**/*.py" = ["PLR2004", "S101", "ARG001"] + +# __init__.py files can have unused imports and module naming issues +"__init__.py" = ["F401", "N999"] + +[tool.ruff.lint.isort] +known-first-party = ["comfyui_queue_manager"] +force-sort-within-sections = true + +[tool.ruff.lint.flake8-annotations] +allow-star-arg-any = true +ignore-fully-untyped = true \ No newline at end of file diff --git a/queue_manager_node.py b/queue_manager_node.py index f1c1d06..2c76881 100644 --- a/queue_manager_node.py +++ b/queue_manager_node.py @@ -2,7 +2,9 @@ Main ComfyUI custom node for the Queue Manager. """ -from typing import Dict, Any, Tuple +from __future__ import annotations + +from typing import Any class QueueManagerNode: @@ -12,50 +14,52 @@ class QueueManagerNode: """ @classmethod - def INPUT_TYPES(cls) -> Dict[str, Any]: + def INPUT_TYPES(cls) -> dict[str, Any]: # noqa: N802 """Define the input types for the node.""" return { "required": {}, "optional": { "workflow_name": ("STRING", {"default": ""}), "auto_queue": ("BOOLEAN", {"default": True}), - } + }, } RETURN_TYPES = ("STRING",) RETURN_NAMES = ("queue_item_id",) FUNCTION = "process" CATEGORY = "Queue Management" - DESCRIPTION = "Manages workflow execution queues with persistence and control features" + DESCRIPTION = ( + "Manages workflow execution queues with persistence and control features" + ) def __init__(self): """Initialize the queue manager node.""" self.queue_service = None # Will be initialized when needed - - def process(self, workflow_name: str = "", auto_queue: bool = True) -> Tuple[str]: + + def process(self, workflow_name: str = "", auto_queue: bool = True) -> tuple[str]: """ Process the node execution. - + Args: workflow_name: Optional name for the workflow auto_queue: Whether to automatically add to queue - + Returns: Tuple containing the queue item ID """ # For now, return a placeholder ID # This will be implemented in later tasks when the queue service is available queue_item_id = "placeholder_id" - + return (queue_item_id,) @classmethod - def IS_CHANGED(cls, **kwargs) -> float: + def IS_CHANGED(cls, **kwargs: Any) -> float: # noqa: N802, ARG003 """Determine if the node needs to be re-executed.""" # Always execute for now return float("nan") @classmethod - def VALIDATE_INPUTS(cls, **kwargs) -> bool: + def VALIDATE_INPUTS(cls, **kwargs: Any) -> bool: # noqa: N802, ARG003 """Validate the inputs to the node.""" - return True \ No newline at end of file + return True diff --git a/requirements.txt b/requirements.txt index bc30ebe..00feaf6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,5 @@ flask>=2.3.0 -sqlite3 -uuid -datetime -dataclasses -enum34 -typing -json \ No newline at end of file + +# Development dependencies +ruff>=0.1.0 +pre-commit>=3.0.0 \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..4d47f39 --- /dev/null +++ b/setup.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +"""Setup script for ComfyUI Queue Manager development environment.""" + +from __future__ import annotations + +from pathlib import Path +import subprocess +import sys + + +def run_command(cmd: list[str], description: str) -> bool: + """Run a command and return success status.""" + print(f"Running: {description}") + try: + subprocess.run(cmd, check=True, capture_output=True, text=True) # noqa: S603 + except subprocess.CalledProcessError as e: + print(f"✗ {description} failed:") + print(f" Command: {' '.join(cmd)}") + print(f" Error: {e.stderr}") + return False + else: + print(f"✓ {description} completed successfully") + return True + + +def main() -> None: + """Set up the development environment.""" + print("Setting up AC ComfyUI Queue Manager development environment...") + + # Change to the project directory + project_dir = Path(__file__).parent + print(f"Working in: {project_dir}") + + # Install development dependencies + if not run_command( + [sys.executable, "-m", "pip", "install", "-e", ".[dev]"], + "Installing development dependencies", + ): + sys.exit(1) + + # Install pre-commit hooks + if not run_command(["pre-commit", "install"], "Installing pre-commit hooks"): + sys.exit(1) + + # Install commit-msg hook + if not run_command( + ["pre-commit", "install", "--hook-type", "commit-msg"], + "Installing commit-msg hook", + ): + print("Warning: commit-msg hook installation failed (this is optional)") + + # Run initial formatting + if not run_command(["ruff", "format", "."], "Running initial code formatting"): + print("Warning: Initial formatting failed") + + # Run initial linting + if not run_command(["ruff", "check", "--fix", "."], "Running initial linting"): + print("Warning: Initial linting found issues") + + print("\n🎉 Development environment setup complete!") + print("\nNext steps:") + print("1. Make your changes to the code") + print("2. Run 'make format' to format your code") + print("3. Run 'make check' to check for issues") + print("4. Commit your changes (pre-commit hooks will run automatically)") + + +if __name__ == "__main__": + main()