diff --git a/.gitignore b/.gitignore index 40ab5b4..1b5b5d6 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,14 @@ Fonts/USERSCALE SEO/Resources/agency bseodef.ini Fonts/USERFONT + +# Test artifacts +.pytest_cache/ +.coverage +htmlcov/ +*.pyc +*.pyo +*.pyd +.Python +.tox/ +.hypothesis/ diff --git a/.python-version b/.python-version index e4fba21..24ee5b1 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.12 +3.13 diff --git a/BackSEOAgencies.py b/BackSEOAgencies.py index de0c5cd..49142de 100755 --- a/BackSEOAgencies.py +++ b/BackSEOAgencies.py @@ -1,4 +1,4 @@ -#!/usr/bin python3.12 +#!/usr/bin/env python3 # from core import coreUI["Core"] import multiprocessing diff --git a/README.md b/README.md index c6b0ec8..11e1258 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,167 @@ # Pure Python Back SEO Agencies + ![Screenshot from 2025-03-16 13-24-59](https://github.com/user-attachments/assets/7f52445f-2ff2-4bf5-9b07-7fcbf9a0dd29) +[![Python Version](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + ## Introduction -Pure Python Back SEO Agencies is a tool designed to help SEO agencies manage and create reports for their clients using Python. Back SEO Agencies comes with 3 primary features: -- Inspecting the contents of Google Results from just 1 page (Keywords, Content length, Headers, readability, Schema, etc.) -- Auditing YOUR clients websites very quickly (1000 pages in a sitemap takes less than 5 minutes) -- Local rank gridmap reporting + +Pure Python Back SEO Agencies is a comprehensive SEO analysis and reporting tool designed to help SEO agencies manage and create professional reports for their clients. Built entirely in Python, this tool provides powerful features for analyzing search engine results, auditing websites, and generating detailed local ranking reports. + +## Features + +### 🔍 **Google Results Inspector** +- Analyze search results from a single page +- Extract keywords, content length, headers, and readability metrics +- Identify Schema markup and structured data +- Generate detailed competitor analysis reports + +### 🚀 **Fast Website Auditing** +- Audit up to 1000+ pages in under 5 minutes +- Analyze sitemaps for comprehensive site coverage +- Identify technical SEO issues and optimization opportunities +- Generate actionable recommendations + +### 📍 **Local Rank Grid Map Reporting** +- Track local search rankings across different locations +- Visualize ranking performance on interactive maps +- Monitor competitor positions in local search +- Generate geo-targeted ranking reports ## Prerequisites + Before you can install and run this tool, ensure you have the following installed: -- Python 3.12 or later -- UV (Python package installer) + +- **Python 3.12 or later** (Python 3.13 recommended) +- **UV** (Fast Python package installer) - [Install UV](https://github.com/astral-sh/uv) +- **Git** for cloning the repository ## Installation -1. Clone the repository: - ```bash - git clone https://github.com/awcook97/Pure-Python-Back-SEO.git - ``` -2. Navigate to the project directory: - ```bash - cd Pure-Python-Back-SEO - ``` -3. Install the required dependencies: - ```bash - uv venv - source .venv/bin/activate - uv pip install -r requirements.txt - playwright install firefox - ``` - -## Running the Application -To run the application, execute the following command: + +### Quick Start + +1. **Clone the repository:** + ```bash + git clone https://github.com/awcook97/Pure-Python-Back-SEO.git + cd Pure-Python-Back-SEO + ``` + +2. **Set up virtual environment and install dependencies:** + ```bash + uv venv + source .venv/bin/activate # On Windows: .venv\Scripts\activate + uv pip install -r requirements.txt + ``` + +3. **Install Playwright browsers:** + ```bash + playwright install firefox + ``` + +### Alternative Installation (using pip) + +If you prefer using pip instead of uv: +```bash +python -m venv .venv +source .venv/bin/activate # On Windows: .venv\Scripts\activate +pip install -r requirements.txt +playwright install firefox +``` + +## Usage + +### Running the Main Application + +Start the GUI application: ```bash python BackSEOAgencies.py ``` -Go ahead, Star this repo, download it, and start playing with it. Let me know how many clients you land using this reporting software. +Or use the main entry point: +```bash +python __main__.py +``` + +### Running the Flask Web Interface + +For web-based access: +```bash +python FlaskApp.py +``` + +Then open your browser and navigate to `http://localhost:5000` + +## Configuration + +The application can be configured through the `BackSEOSettings.py` file. Key settings include: + +- **Performance settings**: CPU cores, thread count, FPS limits +- **Display settings**: Window size, theme preferences +- **Report settings**: Output formats, template selection + +## Testing + +Run the test suite to ensure everything is working correctly: + +```bash +pytest +``` + +Run tests with coverage: +```bash +pytest --cov=. --cov-report=html +``` + +## Troubleshooting + +### Common Issues + +**Issue: Playwright browser not found** +```bash +# Solution: Reinstall Playwright browsers +playwright install firefox +``` + +**Issue: Module not found errors** +```bash +# Solution: Ensure you're in the virtual environment and dependencies are installed +source .venv/bin/activate +uv pip install -r requirements.txt +``` + +**Issue: Permission denied when running on Linux/Mac** +```bash +# Solution: Make the script executable +chmod +x BackSEOAgencies.py +``` + +## Contributing + +We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details on: +- How to submit bug reports +- How to propose new features +- Development setup and workflow +- Code style guidelines + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +## Acknowledgments + +- Built with [DearPyGUI](https://github.com/hoffstadt/DearPyGui) for the user interface +- Powered by [Playwright](https://playwright.dev/) for web automation +- SEO analysis tools built on top of industry-standard libraries + +## Support + +If you find this tool useful, please: +- ⭐ Star this repository +- 🐛 Report bugs via [GitHub Issues](https://github.com/awcook97/Pure-Python-Back-SEO/issues) +- 💡 Share your feedback and suggestions +- 📢 Spread the word about this tool + +--- + +**Ready to improve your SEO reporting?** Clone this repo, set it up, and start creating professional reports for your clients today! diff --git a/pyproject.toml b/pyproject.toml index 5a0c817..cfdf1e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "pure-python-back-seo" version = "0.1.0" -description = "Add your description here" +description = "A comprehensive SEO analysis and reporting tool for agencies" readme = "README.md" requires-python = ">=3.12" dependencies = [ @@ -21,3 +21,10 @@ dependencies = [ "urllib3>=2.3.0", "xdialog>=1.2.0.1", ] + +[project.optional-dependencies] +test = [ + "pytest>=8.0.0", + "pytest-cov>=4.1.0", + "pytest-mock>=3.12.0", +] diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..b0c5aad --- /dev/null +++ b/pytest.ini @@ -0,0 +1,27 @@ +[pytest] +minversion = 6.0 +addopts = -ra -q --strict-markers +testpaths = tests +python_files = test_*.py +python_classes = Test* +python_functions = test_* + +[coverage:run] +source = . +omit = + */tests/* + */venv/* + */.venv/* + */site-packages/* + setup.py + +[coverage:report] +exclude_lines = + pragma: no cover + def __repr__ + raise AssertionError + raise NotImplementedError + if __name__ == .__main__.: + if TYPE_CHECKING: + @abstractmethod + diff --git a/requirements.txt b/requirements.txt index 741af15..be2470e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,4 +12,9 @@ readability>=0.3.2 regex>=2024.11.6 requests>=2.32.3 urllib3>=2.3.0 -xdialog>=1.2.0.1 \ No newline at end of file +xdialog>=1.2.0.1 + +# Testing dependencies +pytest>=8.0.0 +pytest-cov>=4.1.0 +pytest-mock>=3.12.0 \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..bd33754 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Test suite for Pure Python Back SEO Agencies.""" diff --git a/tests/test_backseo_datahandler.py b/tests/test_backseo_datahandler.py new file mode 100644 index 0000000..7840b80 --- /dev/null +++ b/tests/test_backseo_datahandler.py @@ -0,0 +1,191 @@ +"""Unit tests for BackSEODataHandler module.""" +import pytest +import json +import os +import tempfile +import shutil +from multiprocessing import Queue + +# Skip all tests if dearpygui is not installed +pytest.importorskip("dearpygui", reason="dearpygui not installed") + +from BackSEODataHandler import BackSEODataHandler, getBackSEODataHandler + + +class TestBackSEODataHandler: + """Tests for BackSEODataHandler class.""" + + @pytest.fixture(autouse=True) + def setup_and_teardown(self): + """Set up test fixtures and clean up after tests.""" + # Create temporary directory for test outputs + self.test_dir = tempfile.mkdtemp() + self.original_cwd = os.getcwd() + os.chdir(self.test_dir) + + # Create queues for testing + self.job_queue = Queue() + self.results_queue = Queue() + + yield + + # Clean up after test + os.chdir(self.original_cwd) + shutil.rmtree(self.test_dir, ignore_errors=True) + + def test_data_handler_initialization(self): + """Test that BackSEODataHandler initializes correctly.""" + handler = BackSEODataHandler(self.job_queue, self.results_queue) + + assert handler.job_queue is self.job_queue + assert handler.results_queue is self.results_queue + assert handler.updateall is False + assert handler.updateMessage == ("", "") + assert isinstance(handler.localReports, dict) + assert isinstance(handler.searchReports, dict) + assert isinstance(handler.siteAuditReports, dict) + assert isinstance(handler.bseoObjects, dict) + assert isinstance(handler.bseoSaveObjects, dict) + + def test_add_object_without_save_state(self): + """Test adding an object without save state.""" + handler = BackSEODataHandler(self.job_queue, self.results_queue) + test_obj = {"test": "data"} + + handler.add_object(test_obj, "test_object", saveState=False) + + assert "test_object" in handler.bseoObjects + assert handler.bseoObjects["test_object"] is test_obj + assert "test_object" not in handler.bseoSaveObjects + + def test_add_object_with_save_state(self): + """Test adding an object with save state.""" + handler = BackSEODataHandler(self.job_queue, self.results_queue) + test_obj = {"test": "data"} + + handler.add_object(test_obj, "test_object", saveState=True) + + assert "test_object" in handler.bseoSaveObjects + assert handler.bseoSaveObjects["test_object"] is test_obj + assert "test_object" not in handler.bseoObjects + + def test_save_data_creates_file(self): + """Test that save_data creates a data file.""" + handler = BackSEODataHandler(self.job_queue, self.results_queue) + handler.dataHolders = {} + + handler.saveData("category1", "key1", "value1") + + assert os.path.exists("bseodata") + with open("bseodata", "r") as f: + data = json.load(f) + + assert "category1" in data + assert data["category1"]["key1"] == "value1" + + def test_save_data_updates_flag(self): + """Test that save_data sets update flag.""" + handler = BackSEODataHandler(self.job_queue, self.results_queue) + handler.dataHolders = {} + + handler.saveData("category1", "key1", "value1") + + assert handler.updateall is True + assert handler.updateMessage == ("fileupdate", "value1") + + def test_savenoupdate_does_not_set_flag(self): + """Test that savenoupdate does not set update flag.""" + handler = BackSEODataHandler(self.job_queue, self.results_queue) + handler.dataHolders = {} + + handler.savenoupdate("category1", "key1", "value1") + + assert handler.updateall is False + assert os.path.exists("bseodata") + + def test_finished_updates(self): + """Test that finishedUpdates resets update flag.""" + handler = BackSEODataHandler(self.job_queue, self.results_queue) + handler.updateall = True + + handler.finishedUpdates() + + assert handler.updateall is False + + def test_load_returns_empty_dict_when_no_file(self): + """Test that load returns empty dict when no data file exists.""" + handler = BackSEODataHandler(self.job_queue, self.results_queue) + + result = handler.load() + + assert result == {} + + def test_load_returns_data_when_file_exists(self): + """Test that load returns data when file exists.""" + handler = BackSEODataHandler(self.job_queue, self.results_queue) + + # Create test data file + test_data = {"category": {"key": "value"}} + with open("bseodata", "w") as f: + json.dump(test_data, f) + + result = handler.load() + + assert result == test_data + + def test_loaddata_returns_none_for_missing_category(self): + """Test that loaddata returns None for missing category.""" + handler = BackSEODataHandler(self.job_queue, self.results_queue) + handler.dataHolders = {} + + result = handler.loaddata("missing_category", "key") + + assert result is None + assert "missing_category" in handler.dataHolders + + def test_loaddata_returns_none_for_missing_key(self): + """Test that loaddata returns None for missing key.""" + handler = BackSEODataHandler(self.job_queue, self.results_queue) + handler.dataHolders = {"category": {}} + + result = handler.loaddata("category", "missing_key") + + assert result is None + assert "missing_key" in handler.dataHolders["category"] + + def test_loaddata_returns_value_when_exists(self): + """Test that loaddata returns value when it exists.""" + handler = BackSEODataHandler(self.job_queue, self.results_queue) + handler.dataHolders = {"category": {"key": "test_value"}} + + result = handler.loaddata("category", "key") + + assert result == "test_value" + + +class TestGetBackSEODataHandler: + """Tests for getBackSEODataHandler function.""" + + @pytest.fixture(autouse=True) + def setup_and_teardown(self): + """Reset singleton state before each test.""" + # This would need access to the singleton pattern used in the module + # For now, we'll just test that it returns an instance + yield + + def test_get_back_seo_data_handler_returns_instance(self): + """Test that getBackSEODataHandler returns a BackSEODataHandler instance.""" + handler = getBackSEODataHandler() + + assert isinstance(handler, BackSEODataHandler) + + def test_get_back_seo_data_handler_returns_same_instance(self): + """Test that getBackSEODataHandler returns the same instance (singleton).""" + handler1 = getBackSEODataHandler() + handler2 = getBackSEODataHandler() + + assert handler1 is handler2 + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/tests/test_backseo_settings.py b/tests/test_backseo_settings.py new file mode 100644 index 0000000..c049053 --- /dev/null +++ b/tests/test_backseo_settings.py @@ -0,0 +1,161 @@ +"""Unit tests for BackSEOSettings module.""" +import pytest +import os +import tempfile +import shutil +from pathlib import Path +from unittest.mock import patch, MagicMock + +# Skip all tests if dearpygui is not installed +pytest.importorskip("dearpygui", reason="dearpygui not installed") + +from BackSEOSettings import BackSEOSettings, user_settings_dir, user_local_dir + + +class TestUserDirectoryFunctions: + """Tests for user directory utility functions.""" + + def test_user_settings_dir_returns_path(self): + """Test that user_settings_dir returns a Path object.""" + result = user_settings_dir() + assert isinstance(result, Path) + assert "Back SEO Marketing Software" in str(result) + + def test_user_local_dir_returns_path(self): + """Test that user_local_dir returns a Path object.""" + result = user_local_dir() + assert isinstance(result, Path) + assert "Back SEO Marketing Software" in str(result) + + @patch('platform.system') + def test_user_settings_dir_windows(self, mock_system): + """Test user_settings_dir returns correct path for Windows.""" + mock_system.return_value = "Windows" + result = user_settings_dir() + assert "AppData" in str(result) + assert "Local" in str(result) + + @patch('platform.system') + def test_user_settings_dir_mac(self, mock_system): + """Test user_settings_dir returns correct path for Mac.""" + mock_system.return_value = "Darwin" + result = user_settings_dir() + assert "Library" in str(result) + assert "Caches" in str(result) + + @patch('platform.system') + def test_user_settings_dir_linux(self, mock_system): + """Test user_settings_dir returns correct path for Linux.""" + mock_system.return_value = "Linux" + result = user_settings_dir() + assert ".cache" in str(result) + + +class TestBackSEOSettings: + """Tests for BackSEOSettings class.""" + + @pytest.fixture(autouse=True) + def setup_and_teardown(self): + """Set up test fixtures and clean up after tests.""" + # Clear singleton instance before each test + BackSEOSettings._instances.clear() + + # Create temporary directory for test outputs + self.test_dir = tempfile.mkdtemp() + self.original_cwd = os.getcwd() + os.chdir(self.test_dir) + + # Patch user_settings_dir to use temp directory + self.settings_temp_dir = Path(self.test_dir) / "settings" + self.settings_temp_dir.mkdir(exist_ok=True) + + self.patcher = patch('BackSEOSettings.user_settings_dir', return_value=self.settings_temp_dir) + self.patcher.start() + + yield + + # Clean up after test + self.patcher.stop() + os.chdir(self.original_cwd) + shutil.rmtree(self.test_dir, ignore_errors=True) + + def test_settings_creates_output_directories(self): + """Test that BackSEOSettings creates necessary output directories.""" + BackSEOSettings() + + # Check that output directories are created + expected_dirs = [ + "output/custom", + "output/htmls", + "output/images", + "output/localclients", + "output/reports", + "output/screenshots", + "output/search_results_audit", + "output/sitemap_audits", + "Plugins", + ] + + for dir_path in expected_dirs: + assert os.path.exists(dir_path), f"Directory {dir_path} should exist" + + def test_settings_default_values(self): + """Test that BackSEOSettings initializes with correct default values.""" + settings = BackSEOSettings() + + assert settings.vsync is True + assert settings.animations is True + assert settings.fps == 120 + assert settings.width == 1280 + assert settings.height == 800 + assert settings.agencyOutput == "output" + assert settings.helperthreads == 4 + + def test_settings_cpu_cores_reasonable(self): + """Test that CPU cores settings are reasonable.""" + settings = BackSEOSettings() + + import multiprocessing + cpu_count = multiprocessing.cpu_count() + + assert settings.cpucores == cpu_count / 2 + assert settings.maxcpucores == cpu_count + assert settings.cpucores > 0 + assert settings.maxcpucores > 0 + + def test_settings_singleton_pattern(self): + """Test that BackSEOSettings follows singleton pattern.""" + settings1 = BackSEOSettings() + settings2 = BackSEOSettings() + + assert settings1 is settings2, "BackSEOSettings should be a singleton" + + def test_settings_file_path_created(self): + """Test that settings file path is properly set.""" + settings = BackSEOSettings() + + assert isinstance(settings.settingsOutpath, Path) + assert isinstance(settings.settingsFile, Path) + assert settings.settingsFile.name == "settings.bseo" + + def test_change_vsync(self): + """Test changing vsync setting.""" + settings = BackSEOSettings() + + settings.changeVsync(False) + assert settings.vsync is False + + settings.changeVsync(True) + assert settings.vsync is True + + def test_set_viewport(self): + """Test setting viewport.""" + settings = BackSEOSettings() + mock_viewport = MagicMock() + + settings.setVp(mock_viewport) + assert settings.vp is mock_viewport + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100644 index 0000000..7da4ae9 --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,115 @@ +"""Integration tests for Pure Python Back SEO.""" +import pytest +import os +import sys +from pathlib import Path + +# Compute project root from this file's location +PROJECT_ROOT = Path(__file__).parent.parent.resolve() + +# Add project root to path to ensure imports work +sys.path.insert(0, str(PROJECT_ROOT)) + + +class TestProjectStructure: + """Test that the project structure is correct.""" + + def test_main_modules_exist(self): + """Test that main modules exist.""" + modules = [ + "BackSEOAgencies.py", + "BackSEOSettings.py", + "BackSEODataHandler.py", + "BackSEOApplicationManager.py", + "PluginController.py", + "FlaskApp.py", + "core.py", + "__main__.py", + ] + + for module in modules: + module_path = PROJECT_ROOT / module + assert module_path.exists(), f"Module {module} should exist at {module_path}" + + def test_directories_exist(self): + """Test that important directories exist.""" + directories = [ + "SEO", + "Utils", + "Plugins", + "core_ui", + "templates", + "static", + ] + + for directory in directories: + dir_path = PROJECT_ROOT / directory + assert dir_path.exists(), f"Directory {directory} should exist at {dir_path}" + + def test_config_files_exist(self): + """Test that configuration files exist.""" + files = [ + "pyproject.toml", + "requirements.txt", + ".python-version", + "README.md", + "LICENSE", + ] + + for file in files: + file_path = PROJECT_ROOT / file + assert file_path.exists(), f"File {file} should exist at {file_path}" + + def test_python_version_file_content(self): + """Test that .python-version has correct version.""" + version_file = PROJECT_ROOT / ".python-version" + with open(version_file, "r") as f: + version = f.read().strip() + + # Should be pinned to Python 3.13 + assert version == "3.13", f"Python version should be 3.13, got {version}" + + def test_shebang_lines_correct(self): + """Test that shebang lines are correct in executable scripts.""" + script_file = PROJECT_ROOT / "BackSEOAgencies.py" + with open(script_file, "r") as f: + first_line = f.readline().strip() + + # Should be portable shebang + assert first_line == "#!/usr/bin/env python3", \ + f"Shebang should be '#!/usr/bin/env python3', got '{first_line}'" + + +class TestImports: + """Test that key modules can be imported.""" + + def test_import_backseo_settings(self): + """Test that BackSEOSettings can be imported.""" + pytest.importorskip("dearpygui", reason="dearpygui not installed") + try: + from BackSEOSettings import BackSEOSettings + assert BackSEOSettings is not None + except ImportError as e: + pytest.fail(f"Failed to import BackSEOSettings: {e}") + + def test_import_backseo_datahandler(self): + """Test that BackSEODataHandler can be imported.""" + pytest.importorskip("dearpygui", reason="dearpygui not installed") + try: + from BackSEODataHandler import BackSEODataHandler + assert BackSEODataHandler is not None + except ImportError as e: + pytest.fail(f"Failed to import BackSEODataHandler: {e}") + + def test_import_plugin_controller(self): + """Test that PluginController can be imported.""" + pytest.importorskip("dearpygui", reason="dearpygui not installed") + try: + from PluginController import PluginController + assert PluginController is not None + except ImportError as e: + pytest.fail(f"Failed to import PluginController: {e}") + + +if __name__ == "__main__": + pytest.main([__file__, "-v"])