diff --git a/.dockerignore b/.dockerignore index 661c8a1..6392985 100644 --- a/.dockerignore +++ b/.dockerignore @@ -41,7 +41,4 @@ ENV/ htmlcov/ # Project specific -rules_backup/ - -# Documentation -*.md \ No newline at end of file +rules_backup/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 969df2b..80a63d2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,6 +12,7 @@ RUN apt-get update && \ libc6-dev \ python3-dev \ python3-pip \ + tree \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* \ && python3 -m pip install --no-cache-dir --upgrade "pip>=21.3" setuptools wheel @@ -26,23 +27,27 @@ RUN mkdir -p /app/src/simpleguardhome && \ # Copy source code, maintaining directory structure COPY . /app/ -# Set execute permission for entrypoint script -RUN chmod +x /app/docker-entrypoint.sh && \ +# Debug: Show the copied files and set execute permission for entrypoint script +RUN echo "Project structure:" && \ + tree /app && \ + echo "Package directory contents:" && \ + ls -la /app/src/simpleguardhome/ && \ + chmod +x /app/docker-entrypoint.sh && \ cp /app/docker-entrypoint.sh /usr/local/bin/ # Set PYTHONPATH ENV PYTHONPATH=/app/src -# Install Python requirements -RUN pip install --no-cache-dir -r requirements.txt - -# Install and verify the package -RUN set -e && \ +# Install Python requirements and verify the package +RUN pip install --no-cache-dir -r requirements.txt && \ + set -e && \ echo "Installing package..." && \ pip uninstall -y simpleguardhome || true && \ - # Verify source files exist - echo "Verifying source files..." && \ - ls -la /app/src/simpleguardhome/ && \ + # Debug: Show package files + echo "Python path:" && \ + python3 -c "import sys; print('\n'.join(sys.path))" && \ + echo "Source directory contents:" && \ + ls -R /app/src && \ # Install package in editable mode with compatibility mode enabled pip install --use-pep517 -e . --config-settings editable_mode=compat && \ echo "Verifying installation..." && \ @@ -50,13 +55,15 @@ RUN set -e && \ # List all package files echo "Package contents:" && \ find /app/src/simpleguardhome -type f -ls && \ - # Verify import works + # Verify package can be imported echo "Testing import..." && \ - python3 -c "import simpleguardhome; from simpleguardhome.main import app; print(f'Package found at: {simpleguardhome.__file__}')" && \ - echo "Package installation successful" - -# Create rules backup directory with proper permissions -RUN mkdir -p /app/rules_backup && \ + python3 -c "import simpleguardhome; print(f'Package found at: {simpleguardhome.__file__}')" && \ + # Verify app can be imported + echo "Testing app import..." && \ + python3 -c "from simpleguardhome.main import app; print('App imported successfully')" && \ + echo "Package installation successful" && \ + # Create rules backup directory with proper permissions + mkdir -p /app/rules_backup && \ chmod 777 /app/rules_backup # Default environment variables diff --git a/MANIFEST.in b/MANIFEST.in index 08be7a6..60b83ff 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,18 @@ -recursive-include src/simpleguardhome/templates * +# Include all package Python files +graft src/simpleguardhome + +# Include package data files include src/simpleguardhome/favicon.ico -recursive-include src/simpleguardhome *.py \ No newline at end of file +include src/simpleguardhome/templates/*.html + +# Include important project files +include README.md +include LICENSE +include requirements.txt +include pyproject.toml +include setup.py + +# Exclude bytecode files +global-exclude *.py[cod] +global-exclude __pycache__ +global-exclude *.so \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index a328803..f82f47d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,9 +22,21 @@ dependencies = [ ] [tool.setuptools] +# Using explicit package configuration package-dir = {"" = "src"} packages = ["simpleguardhome"] -include-package-data = true +# Include all package data [tool.setuptools.package-data] -simpleguardhome = ["templates/*", "favicon.ico"] \ No newline at end of file +"*" = ["*.ico", "templates/*.html"] + +# Explicitly include the package data +[options.package_data] +simpleguardhome = [ + "templates/*", + "favicon.ico" +] + +# Make sure data files are included +[options] +include_package_data = true \ No newline at end of file diff --git a/src/simpleguardhome/config.py b/src/simpleguardhome/config.py index 5be14ce..780bed4 100644 --- a/src/simpleguardhome/config.py +++ b/src/simpleguardhome/config.py @@ -1,22 +1,23 @@ -from pydantic_settings import BaseSettings from typing import Optional +from pydantic_settings import BaseSettings # type: ignore + class Settings(BaseSettings): """Application settings using environment variables.""" - + ADGUARD_HOST: str = "http://localhost" ADGUARD_PORT: int = 3000 ADGUARD_USERNAME: Optional[str] = None ADGUARD_PASSWORD: Optional[str] = None - + @property def adguard_base_url(self) -> str: """Get the base URL for AdGuard Home API.""" return f"{self.ADGUARD_HOST}:{self.ADGUARD_PORT}" - + class Config: env_file = ".env" -settings = Settings() \ No newline at end of file +settings = Settings() diff --git a/src/simpleguardhome/main.py b/src/simpleguardhome/main.py index ab13ca9..33fbe64 100644 --- a/src/simpleguardhome/main.py +++ b/src/simpleguardhome/main.py @@ -1,25 +1,33 @@ -from fastapi import FastAPI, Request, Form, HTTPException, status -from fastapi.templating import Jinja2Templates -from fastapi.staticfiles import StaticFiles -from fastapi.responses import HTMLResponse, JSONResponse -from fastapi.middleware.cors import CORSMiddleware -from pathlib import Path -import httpx import logging +from pathlib import Path from typing import Dict -from . import adguard -from .config import settings -from .adguard import ( - AdGuardError, - AdGuardConnectionError, - AdGuardAPIError, - AdGuardValidationError, - FilterStatus, - FilterCheckHostResponse, - SetRulesRequest + +import httpx # noqa: F401 +from fastapi import ( # type: ignore # noqa: F401 + FastAPI, + Form, + HTTPException, + Request, + status, ) +from fastapi.middleware.cors import CORSMiddleware # type: ignore +from fastapi.responses import HTMLResponse, JSONResponse # type: ignore +from fastapi.staticfiles import StaticFiles # type: ignore +from fastapi.templating import Jinja2Templates # type: ignore from pydantic import BaseModel, Field +from . import adguard +from .adguard import ( + AdGuardAPIError, + AdGuardConnectionError, + AdGuardError, + AdGuardValidationError, + FilterCheckHostResponse, + FilterStatus, + SetRulesRequest, +) +from .config import settings # noqa: F401 + # Configure logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -53,13 +61,17 @@ app.mount("/static", StaticFiles(directory=str(Path(__file__).parent)), name="st # Mount favicon.ico at root static_files_path = Path(__file__).parent -app.mount("/favicon.ico", StaticFiles(directory=str(static_files_path)), name="favicon") +app.mount("/favicon.ico", + StaticFiles(directory=str(static_files_path)), name="favicon") # Response models matching AdGuard spec + + class ErrorResponse(BaseModel): """Error response model according to AdGuard spec.""" message: str = Field(..., description="The error message") + @app.get("/", response_class=HTMLResponse) async def home(request: Request): """Render the home page.""" @@ -68,6 +80,7 @@ async def home(request: Request): {"request": request} ) + @app.get( "/control/filtering/check_host", response_model=FilterCheckHostResponse, @@ -85,7 +98,7 @@ async def check_domain(name: str) -> FilterCheckHostResponse: status_code=status.HTTP_400_BAD_REQUEST, detail="Domain name is required" ) - + logger.info(f"Checking domain: {name}") try: async with adguard.AdGuardClient() as client: @@ -96,11 +109,12 @@ async def check_domain(name: str) -> FilterCheckHostResponse: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=str(e) - ) + ) from e except Exception as e: logger.error(f"Error checking domain {name}: {str(e)}") raise + @app.post( "/control/filtering/set_rules", response_model=Dict, @@ -118,7 +132,7 @@ async def add_to_whitelist(request: SetRulesRequest) -> Dict: status_code=status.HTTP_400_BAD_REQUEST, detail="Rules are required" ) - + # Extract domain from whitelist rule rule = request.rules[0] if not rule.startswith("@@||") or not rule.endswith("^"): @@ -126,10 +140,10 @@ async def add_to_whitelist(request: SetRulesRequest) -> Dict: status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid whitelist rule format" ) - + domain = rule[4:-1] # Remove @@|| prefix and ^ suffix logger.info(f"Adding domain to whitelist: {domain}") - + try: async with adguard.AdGuardClient() as client: success = await client.add_allowed_domain(domain) @@ -144,11 +158,12 @@ async def add_to_whitelist(request: SetRulesRequest) -> Dict: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=str(e) - ) + ) from e except Exception as e: logger.error(f"Error adding domain to whitelist: {str(e)}") raise + @app.get( "/control/filtering/status", response_model=FilterStatus, @@ -167,8 +182,9 @@ async def get_filtering_status() -> FilterStatus: logger.error(f"Error getting filter status: {str(e)}") raise + @app.exception_handler(AdGuardError) -async def adguard_exception_handler(request: Request, exc: AdGuardError) -> JSONResponse: +async def adguard_exception_handler(_request: Request, exc: AdGuardError) -> JSONResponse: """Handle AdGuard-related exceptions according to spec.""" if isinstance(exc, AdGuardConnectionError): status_code = status.HTTP_503_SERVICE_UNAVAILABLE @@ -178,15 +194,16 @@ async def adguard_exception_handler(request: Request, exc: AdGuardError) -> JSON status_code = status.HTTP_502_BAD_GATEWAY else: status_code = status.HTTP_500_INTERNAL_SERVER_ERROR - + return JSONResponse( status_code=status_code, content={"message": str(exc)} ) + def start(): """Start the application using uvicorn.""" - import uvicorn + import uvicorn # type: ignore uvicorn.run( app, host="0.0.0.0", @@ -194,5 +211,6 @@ def start(): reload=False # Disable reload in Docker ) + if __name__ == "__main__": - start() \ No newline at end of file + start()