diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e8e5fde --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,42 @@ +name: CI Checks + +on: + pull_request: + branches: [main] # Or your default branch + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Cache pip dependencies + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt # Adjust if you have dev requirements + # Install dev tools and type stubs if not in requirements.txt + pip install black flake8 mypy pytest pytest-cov types-tqdm types-PyYAML + + - name: Run Linter and Formatter Check + run: | + flake8 src/ + black --check src/ + + - name: Run MyPy (Type Checking) + run: | + mypy src/ diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..d74d291 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,49 @@ +name: Publish Python Package to PyPI + +on: + push: + tags: + - "v*.*.*" # Trigger on tags like v0.1.0, v1.2.3 + +jobs: + deploy: + runs-on: ubuntu-latest + + # Optional: Use environments for protection rules + # environment: + # name: pypi + # url: https://pypi.org/p/build-influence # Replace with your package name + + # Grant GITHUB_TOKEN permissions to authenticate with PyPI using OIDC (more secure, recommended) + permissions: + id-token: write # Required for trusted publishing + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install build dependencies + run: python -m pip install --upgrade build twine + + - name: Build package + run: python -m build + + # Preferred: Publish using Trusted Publishing (OIDC) + - name: Publish package to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + # No API token needed here if PyPI project is configured for trusted publishing + + # --- Alternative: Publish using API Token (keep only ONE publish step) --- + # Uncomment the step below and comment out the Trusted Publishing step above + # if you prefer using the API token secret. + # Ensure you have configured the PYPI_API_TOKEN secret in GitHub repo settings. + # - name: Publish package to PyPI using API Token + # uses: pypa/gh-action-pypi-publish@release/v1 + # with: + # password: ${{ secrets.PYPI_API_TOKEN }} + # --------------------------------------------------------------------- diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..e6ce05f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,66 @@ +# Contributing to Build Influence + +First off, thank you for considering contributing to Build Influence! We welcome any help, from reporting bugs and suggesting features to submitting code changes. + +## How Can I Contribute? + +### Reporting Bugs + +- **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/drorivry/build-influence/issues). (Replace `drorivry/build-influence` with your actual repository path if different). +- If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/drorivry/build-influence/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible, and a **code sample or an executable test case** demonstrating the expected behavior that is not occurring. + +### Suggesting Enhancements + +- Open a new issue using the Feature Request template. +- Clearly describe the enhancement and the motivation for it. +- Explain why this enhancement would be useful. +- Provide code examples if applicable. + +### Pull Requests + +We actively welcome your pull requests: + +1. Fork the repo and create your branch from `main`. +2. If you've added code that should be tested, add tests. +3. If you've changed APIs, update the documentation. +4. Ensure the test suite passes (`pytest tests/`). +5. Make sure your code lints (`flake8 src/ tests/`) and is formatted (`black src/ tests/`). +6. Ensure type checks pass (`mypy src/ tests/`). +7. Issue that pull request! + +## Development Setup + +1. Fork the repository on GitHub. +2. Clone your fork locally: + ```bash + git clone git@github.com:YOUR_USERNAME/build-influence.git + cd build-influence + ``` +3. Create a virtual environment (recommended): + ```bash + python3.12 -m venv venv + source venv/bin/activate # On Windows use `venv\Scripts\activate` + ``` +4. Install dependencies: + ```bash + pip install -r requirements.txt + # Install development/testing tools and type stubs if they are not in requirements.txt + pip install black flake8 mypy pytest pytest-cov build twine types-tqdm types-PyYAML + ``` + +## Coding Standards + +- Please follow the [PEP 8](https://www.python.org/dev/peps/pep-0008/) style guide. +- Use `flake8` for linting and `black` for formatting. Our CI pipeline checks this, so please run `flake8 src/ tests/` and `black src/ tests/` before committing. +- Use type hints (`mypy`) for static analysis. Our CI pipeline checks this. +- Write tests using `pytest` for new functionality. + +## Code of Conduct + +Please note that this project is released with a Contributor Code of Conduct. By participating in this project you agree to abide by its terms. (Consider adding a `CODE_OF_CONDUCT.md` file). + +## Any questions? + +Feel free to open an issue if you have questions about contributing. + +Thank you for your contribution! diff --git a/src/build_influence/analysis/analyzer.py b/src/build_influence/analysis/analyzer.py index 26e5f22..32ddc86 100644 --- a/src/build_influence/analysis/analyzer.py +++ b/src/build_influence/analysis/analyzer.py @@ -1,6 +1,6 @@ import subprocess from pathlib import Path -from typing import List, Dict, Any +from typing import List, Dict, Any, Optional import litellm import json from tqdm import tqdm @@ -128,7 +128,7 @@ def analyze(self) -> Dict[str, Any]: # -------------------------------------------- # --- Final Result --- - final_analysis_result = { + final_analysis_result: Dict[str, Any] = { **interim_result, "high_level_features": high_level_features, } @@ -185,7 +185,7 @@ def _extract_metadata(self) -> Dict[str, Any]: def _build_file_tree(self) -> List[Dict[str, Any]]: """Scan directory structure and build a list of files to consider.""" logger.debug("Building file tree...") - file_tree = [] + file_tree: List[Dict[str, Any]] = [] potential_files_count = 0 try: @@ -196,7 +196,7 @@ def _build_file_tree(self) -> List[Dict[str, Any]]: try: relative_path = item.relative_to(self.repo_path) - file_info = { + file_info: Dict[str, Any] = { "path": str(relative_path), "absolute_path": str(item), "size": item.stat().st_size, @@ -269,7 +269,7 @@ def _analyze_file_content_with_ai( Generic helper to analyze file content using LiteLLM with a specific prompt. """ - insights = {"error": None} + insights: Dict[str, Any] = {"error": None} file_name = file_path.name try: @@ -406,15 +406,17 @@ def _analyze_code_file_with_ai(self, file_path: Path) -> Dict[str, Any]: # logger.debug( # f"Attempting code key extraction from: {raw_insights!r}" # ) - purpose = raw_insights.get("purpose") + purpose: Optional[str] = raw_insights.get("purpose") # logger.debug(f"Code Purpose extracted: {purpose!r}") - elements = raw_insights.get("key_elements", []) + elements: Optional[List[str]] = raw_insights.get("key_elements", []) # logger.debug(f"Code Elements extracted: {elements!r}") - dependencies = raw_insights.get("dependencies", []) + dependencies: Optional[List[str]] = raw_insights.get("dependencies", []) # logger.debug( # f"Code Dependencies extracted: {dependencies!r}" # ) - aspects = raw_insights.get("interesting_aspects", []) + aspects: Optional[List[str]] = raw_insights.get( + "interesting_aspects", [] + ) # logger.debug(f"Code Aspects extracted: {aspects!r}") # --- End Detailed Logging --- @@ -456,10 +458,12 @@ def _analyze_doc_file_with_ai(self, file_path: Path) -> Dict[str, Any]: return raw_insights # Return error dict as is elif isinstance(raw_insights, dict): try: - summary = raw_insights.get("summary") - features = raw_insights.get("features", []) - setup_steps = raw_insights.get("setup_steps", []) - usage_examples = raw_insights.get("usage_examples", []) + summary: Optional[str] = raw_insights.get("summary") + features: Optional[List[str]] = raw_insights.get("features", []) + setup_steps: Optional[List[str]] = raw_insights.get("setup_steps", []) + usage_examples: Optional[List[str]] = raw_insights.get( + "usage_examples", [] + ) return { "summary": summary, @@ -506,7 +510,7 @@ def _analyze_doc_file_with_ai(self, file_path: Path) -> Dict[str, Any]: # Print first few files with AI insights if available for i, file_info in enumerate(result["file_tree"][:5]): print( - f"\n File {i+1}: {file_info['path']} " + f"\n File {i + 1}: {file_info['path']} " f"({file_info['type']}, {file_info['size']}b)" ) insights = None diff --git a/src/build_influence/generation/generator.py b/src/build_influence/generation/generator.py index 1fe39fc..a9f07e3 100644 --- a/src/build_influence/generation/generator.py +++ b/src/build_influence/generation/generator.py @@ -85,13 +85,13 @@ def _build_prompt( **Instructions:** 1. Synthesize the provided information. 2. Write a compelling '{content_type}' post in well-formatted Markdown. - 3. Ensure the tone is appropriate for the target audience and platform + 3. Ensure the tone is appropriate for the target audience and platform (general technical audience for Markdown). - 4. If generating a 'deepdive', elaborate on technical aspects. If + 4. If generating a 'deepdive', elaborate on technical aspects. If 'announcement', focus on highlights and purpose. - 5. Make sure the output is only the Markdown content, without any preamble + 5. Make sure the output is only the Markdown content, without any preamble or explanation. - + **Generated Markdown Post:** """.strip() diff --git a/src/build_influence/generation/linkedin_generator.py b/src/build_influence/generation/linkedin_generator.py index b33c10a..c988b5b 100644 --- a/src/build_influence/generation/linkedin_generator.py +++ b/src/build_influence/generation/linkedin_generator.py @@ -76,8 +76,8 @@ def _build_prompt( 2. Focus on the value proposition, achievements, or key learnings. 3. Maintain a professional and engaging tone suitable for LinkedIn. 4. Use appropriate formatting (bullet points, maybe bolding key terms). -5. If '{content_type}' is 'announcement', highlight the launch/update - professionally. If 'deepdive', focus on technical achievements or +5. If '{content_type}' is 'announcement', highlight the launch/update + professionally. If 'deepdive', focus on technical achievements or learnings. 6. Consider adding relevant professional hashtags (e.g., #SoftwareDevelopment, #Tech, #ProjectManagement). diff --git a/src/build_influence/publication/__init__.py b/src/build_influence/publication/__init__.py index 9b2eb55..e63f80f 100644 --- a/src/build_influence/publication/__init__.py +++ b/src/build_influence/publication/__init__.py @@ -1,5 +1,7 @@ # Publication module +from typing import Type + from .base_publisher import BasePublisher, PublicationContent, PublishResult from .linkedin_publisher import LinkedInPublisher from .devto_publisher import DevToPublisher @@ -17,14 +19,15 @@ def get_publisher(platform_name: str) -> BasePublisher | None: """Factory function to get a publisher instance for the platform name.""" - publisher_class = _publisher_map.get(platform_name.lower()) + publisher_class: Type[BasePublisher] | None = _publisher_map.get( + platform_name.lower() + ) if publisher_class: return publisher_class() return None __all__ = [ - "BasePublisher", "PublicationContent", "PublishResult", "LinkedInPublisher", diff --git a/src/build_influence/publication/base_publisher.py b/src/build_influence/publication/base_publisher.py index fa3d040..67c1184 100644 --- a/src/build_influence/publication/base_publisher.py +++ b/src/build_influence/publication/base_publisher.py @@ -24,7 +24,7 @@ class BasePublisher(ABC): platform_name: str = "Base" @abstractmethod - async def publish(self, content: PublicationContent, config: dict) -> PublishResult: + def publish(self, content: PublicationContent, config: dict) -> PublishResult: """ Publishes the given content to the specific platform. diff --git a/src/build_influence/publication/linkedin_publisher.py b/src/build_influence/publication/linkedin_publisher.py index d268b3e..d47852c 100644 --- a/src/build_influence/publication/linkedin_publisher.py +++ b/src/build_influence/publication/linkedin_publisher.py @@ -6,7 +6,7 @@ class LinkedInPublisher(BasePublisher): platform_name = "LinkedIn" - async def publish(self, content: PublicationContent, config: dict) -> PublishResult: + def publish(self, content: PublicationContent, config: dict) -> PublishResult: """Publishes content to LinkedIn. Placeholder implementation.""" print(f"Publishing to {self.platform_name}:") print(f"Title: {content.title}")