polycoding 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
cli/utils.py ADDED
@@ -0,0 +1,70 @@
1
+ """Shared utilities for CLI commands."""
2
+
3
+ import logging
4
+ from typing import Any
5
+
6
+ from rich.console import Console
7
+ from rich.logging import RichHandler
8
+
9
+ console = Console()
10
+
11
+ NOISY_LOGGERS = [
12
+ "urllib3.connectionpool",
13
+ "httpcore.connection",
14
+ "httpcore",
15
+ "openai._base_client",
16
+ ]
17
+
18
+
19
+ def setup_logging(level: int | str = logging.INFO) -> None:
20
+ """Configure Rich logging.
21
+
22
+ Args:
23
+ level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL) as int or str
24
+ """
25
+ if isinstance(level, str):
26
+ level = getattr(logging, level.upper(), logging.INFO)
27
+
28
+ logging.basicConfig(
29
+ level=level,
30
+ format="%(name)s - %(message)s",
31
+ handlers=[
32
+ RichHandler(
33
+ console=console,
34
+ rich_tracebacks=True,
35
+ )
36
+ ],
37
+ )
38
+
39
+ for logger_name in NOISY_LOGGERS:
40
+ logging.getLogger(logger_name).setLevel(logging.WARNING)
41
+
42
+
43
+ def get_logger(name: str) -> logging.Logger:
44
+ """Get logger instance.
45
+
46
+ Args:
47
+ name: Logger name (typically __name__)
48
+
49
+ Returns:
50
+ Logger instance
51
+ """
52
+ return logging.getLogger(f"polycode.{name}")
53
+
54
+
55
+ def load_bootstrap() -> Any:
56
+ """Load bootstrap system for full runtime initialization.
57
+
58
+ Returns:
59
+ ModuleContext with engine, hook manager, and config
60
+ """
61
+ from bootstrap import bootstrap
62
+
63
+ log = get_logger("cli.utils")
64
+ log.debug("🔧 Loading bootstrap system...")
65
+
66
+ context = bootstrap()
67
+
68
+ log.debug(f"✅ Bootstrap loaded: {len(context.config)} modules configured")
69
+
70
+ return context
cli/worker.py ADDED
@@ -0,0 +1,124 @@
1
+ """Worker commands for Polycode CLI."""
2
+
3
+ import sys
4
+
5
+ import typer
6
+ from rich.console import Console
7
+
8
+ from cli import print_error, print_info, print_success
9
+ from cli.utils import get_logger
10
+
11
+ log = get_logger(__name__)
12
+ console = Console()
13
+
14
+ worker_app = typer.Typer(help="Celery worker management commands")
15
+
16
+
17
+ @worker_app.command("start")
18
+ def worker_start(
19
+ queues: str = typer.Option("celery,default", "--queues", "-q", help="Comma-separated queue names"),
20
+ concurrency: int = typer.Option(0, "--concurrency", "-c", help="Number of worker processes (0 = auto)"),
21
+ loglevel: str = typer.Option("info", "--loglevel", "-l", help="Log level"),
22
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose logging"),
23
+ ) -> None:
24
+ """Start Celery worker for processing async tasks.
25
+
26
+ Worker handles flow execution, webhook processing, and periodic tasks.
27
+ """
28
+ if verbose:
29
+ import cli.utils
30
+
31
+ cli.utils.setup_logging("DEBUG")
32
+
33
+ queues_list = [q.strip() for q in queues.split(",") if q.strip()]
34
+ concurrency_str = str(concurrency) if concurrency > 0 else 10
35
+
36
+ print_info("⚙️ Starting Celery worker")
37
+ print_info(f" Queues: {', '.join(queues_list)}")
38
+ print_info(f" Concurrency: {concurrency_str}")
39
+ print_info(f" Log level: {loglevel}")
40
+
41
+ try:
42
+ from bootstrap import bootstrap
43
+ from tasks import app as celery_app
44
+ from tasks import worker
45
+
46
+ assert worker
47
+
48
+ print_info("📦 Initializing modules...")
49
+ bootstrap()
50
+
51
+ registered = worker.register_module_tasks()
52
+ if registered:
53
+ print_info(f" Module tasks: {len(registered)} registered")
54
+
55
+ celery_app.worker_main(
56
+ [
57
+ "worker",
58
+ f"--queues={','.join(queues_list)}",
59
+ f"--concurrency={concurrency_str}",
60
+ f"--loglevel={loglevel}",
61
+ "-Ofair",
62
+ "--task-events",
63
+ ]
64
+ )
65
+ except KeyboardInterrupt:
66
+ print_info("⏹ Worker stopped by user")
67
+ sys.exit(0)
68
+ except Exception as e:
69
+ print_error(f"Failed to start worker: {e}")
70
+ log.exception(f"Worker startup failed: {e}")
71
+ sys.exit(1)
72
+
73
+
74
+ @worker_app.command("status")
75
+ def worker_status(
76
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose logging"),
77
+ ) -> None:
78
+ """Check Celery worker status using inspect.
79
+
80
+ Shows active workers, registered tasks, and queue statistics.
81
+ """
82
+ if verbose:
83
+ import cli.utils
84
+
85
+ cli.utils.setup_logging("DEBUG")
86
+
87
+ print_info("🔍 Checking worker status...")
88
+
89
+ try:
90
+ from tasks import app as celery_app
91
+
92
+ inspect = celery_app.control.inspect()
93
+
94
+ active = inspect.active()
95
+ registered = inspect.registered()
96
+ stats = inspect.stats()
97
+
98
+ if active:
99
+ for worker_name, tasks in active.items():
100
+ console.print(f"\n[bold cyan]Worker: {worker_name}[/]")
101
+ console.print(f" Active tasks: {len(tasks)}")
102
+ else:
103
+ console.print("[yellow]⚠️ No active workers found[/]")
104
+
105
+ if registered:
106
+ console.print("\n[bold cyan]Registered tasks:[/]")
107
+ for worker_name, tasks in registered.items():
108
+ for task in tasks:
109
+ console.print(f" • {task}")
110
+
111
+ if stats:
112
+ console.print("\n[bold cyan]Statistics:[/]")
113
+ for worker_name, stat in stats.items():
114
+ console.print(f" {worker_name}:")
115
+ for key, value in stat.items():
116
+ if key == "pool":
117
+ console.print(f" Pool: {value.get('max-concurrency', 'N/A')} processes")
118
+
119
+ print_success("Worker status check complete")
120
+
121
+ except Exception as e:
122
+ print_error(f"Failed to check worker status: {e}")
123
+ log.exception(f"Worker status check failed: {e}")
124
+ sys.exit(1)
github_app/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ """GitHub App module for multi-repo CrewAI flow automation."""
2
+
3
+ from github_app.auth import GitHubAppAuth
4
+ from github_app.installation_manager import InstallationManager
5
+ from github_app.label_mapper import LabelFlowMapper
6
+ from github_app.webhook_handler import GitHubAppWebhookHandler
7
+
8
+ __all__ = [
9
+ "GitHubAppAuth",
10
+ "InstallationManager",
11
+ "LabelFlowMapper",
12
+ "GitHubAppWebhookHandler",
13
+ ]
github_app/app.py ADDED
@@ -0,0 +1,224 @@
1
+ """GitHub App FastAPI webhook server - unified webhook endpoint.
2
+
3
+ Merges functionality from:
4
+ - src/github_app/webhook_handler.py (GitHub App webhooks)
5
+ - src/project_manager/webhook.py (signature validation, ping handling)
6
+
7
+ This replaces the separate FastAPI instance in project_manager.
8
+ """
9
+
10
+ import logging
11
+ from typing import Optional
12
+
13
+ from fastapi import FastAPI, HTTPException, Request
14
+ from fastapi.responses import JSONResponse
15
+
16
+ from github_app.auth import GitHubAppAuth
17
+ from github_app.config import settings
18
+ from github_app.installation_manager import InstallationManager
19
+ from github_app.label_mapper import LabelFlowMapper
20
+ from github_app.webhook_handler import GitHubAppWebhookHandler
21
+ from persistence.postgres import SessionLocal
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ app = FastAPI(
26
+ title=settings.GITHUB_APP_NAME,
27
+ description="GitHub App for multi-repo CrewAI flow automation",
28
+ version="2.0.0",
29
+ )
30
+
31
+ github_auth = GitHubAppAuth(
32
+ app_id=settings.GITHUB_APP_ID,
33
+ private_key=settings.GITHUB_APP_PRIVATE_KEY.replace("\\n", "\n"),
34
+ )
35
+
36
+
37
+ def get_db_session():
38
+ """Get database session from postgres.py."""
39
+ return SessionLocal()
40
+
41
+
42
+ def get_webhook_handler(db_session) -> GitHubAppWebhookHandler:
43
+ """Create webhook handler with dependencies."""
44
+ installation_manager = InstallationManager(db_session, github_auth)
45
+ label_mapper = LabelFlowMapper(db_session)
46
+
47
+ return GitHubAppWebhookHandler(
48
+ github_auth=github_auth,
49
+ installation_manager=installation_manager,
50
+ label_mapper=label_mapper,
51
+ webhook_secret=settings.GITHUB_APP_WEBHOOK_SECRET,
52
+ )
53
+
54
+
55
+ # ============================================================================
56
+ # Health & Status Endpoints (from project_manager/webhook.py)
57
+ # ============================================================================
58
+
59
+
60
+ @app.get("/")
61
+ async def root():
62
+ return {
63
+ "name": settings.GITHUB_APP_NAME,
64
+ "version": "2.0.0",
65
+ "status": "running",
66
+ "mode": "github_app",
67
+ }
68
+
69
+
70
+ @app.get("/health")
71
+ async def health():
72
+ """Health check endpoint (from project_manager/webhook.py)."""
73
+ return {
74
+ "status": "healthy",
75
+ "redis": "connected", # TODO: Check actual Redis connection
76
+ "database": "connected", # TODO: Check actual DB connection
77
+ }
78
+
79
+
80
+ # ============================================================================
81
+ # Main Webhook Endpoint (unified)
82
+ # ============================================================================
83
+
84
+
85
+ @app.post("/webhook/github")
86
+ async def github_webhook(request: Request):
87
+ """Handle GitHub webhook events.
88
+
89
+ Unified endpoint for:
90
+ - GitHub App webhooks (multi-repo, installation-based)
91
+ - Legacy webhooks (single repo, from project_manager/webhook.py)
92
+
93
+ Supported events:
94
+ - ping: Webhook verification
95
+ - installation: GitHub App installation lifecycle
96
+ - issues: Issue events
97
+ """
98
+ db_session = get_db_session()
99
+
100
+ try:
101
+ handler = get_webhook_handler(db_session)
102
+ result = await handler.handle_webhook(request)
103
+ return result
104
+ except HTTPException:
105
+ raise
106
+ except Exception as e:
107
+ logger.error(f"Webhook processing error: {e}", exc_info=True)
108
+ raise HTTPException(status_code=500, detail="Internal server error")
109
+ finally:
110
+ db_session.close()
111
+
112
+
113
+ # ============================================================================
114
+ # Installation Management API
115
+ # ============================================================================
116
+
117
+
118
+ @app.get("/installations")
119
+ async def list_installations():
120
+ """List all active installations."""
121
+ db_session = get_db_session()
122
+
123
+ try:
124
+ installation_manager = InstallationManager(db_session, github_auth)
125
+ installations = installation_manager.list_installations()
126
+
127
+ return {
128
+ "installations": [
129
+ {
130
+ "id": inst.installation_id,
131
+ "account": inst.account_login,
132
+ "active": inst.is_active,
133
+ "repos_count": len(inst.repositories.get("repos", []) if inst.repositories else []),
134
+ }
135
+ for inst in installations
136
+ ]
137
+ }
138
+ finally:
139
+ db_session.close()
140
+
141
+
142
+ # ============================================================================
143
+ # Label Mapping API
144
+ # ============================================================================
145
+
146
+
147
+ @app.post("/mappings")
148
+ async def create_label_mapping(
149
+ installation_id: int,
150
+ label_name: str,
151
+ flow_name: str,
152
+ repo_pattern: Optional[str] = None,
153
+ priority: int = 0,
154
+ ):
155
+ """Create a label-to-flow mapping."""
156
+ db_session = get_db_session()
157
+ label_mapper = LabelFlowMapper(db_session)
158
+
159
+ try:
160
+ mapping = label_mapper.create_mapping(
161
+ installation_id=installation_id,
162
+ label_name=label_name,
163
+ flow_name=flow_name,
164
+ repo_pattern=repo_pattern,
165
+ priority=priority,
166
+ )
167
+
168
+ return {
169
+ "id": mapping.id,
170
+ "label": mapping.label_name,
171
+ "flow": mapping.flow_name,
172
+ "pattern": mapping.repo_pattern,
173
+ "priority": mapping.priority,
174
+ }
175
+ finally:
176
+ db_session.close()
177
+
178
+
179
+ @app.get("/mappings")
180
+ async def list_label_mappings(installation_id: Optional[int] = None):
181
+ """List label-to-flow mappings."""
182
+ db_session = get_db_session()
183
+ label_mapper = LabelFlowMapper(db_session)
184
+
185
+ try:
186
+ mappings = label_mapper.list_mappings(installation_id=installation_id)
187
+
188
+ return {
189
+ "mappings": [
190
+ {
191
+ "id": m.id,
192
+ "label": m.label_name,
193
+ "flow": m.flow_name,
194
+ "pattern": m.repo_pattern,
195
+ "priority": m.priority,
196
+ "active": m.is_active,
197
+ }
198
+ for m in mappings
199
+ ]
200
+ }
201
+ finally:
202
+ db_session.close()
203
+
204
+
205
+ # ============================================================================
206
+ # Error Handlers
207
+ # ============================================================================
208
+
209
+
210
+ @app.exception_handler(Exception)
211
+ async def global_exception_handler(request: Request, exc: Exception):
212
+ logger.error(f"Unhandled exception: {exc}", exc_info=True)
213
+ return JSONResponse(status_code=500, content={"detail": "Internal server error"})
214
+
215
+
216
+ if __name__ == "__main__":
217
+ import uvicorn
218
+
219
+ uvicorn.run(
220
+ "app:app",
221
+ host=settings.WEBHOOK_HOST,
222
+ port=settings.WEBHOOK_PORT,
223
+ reload=True,
224
+ )
github_app/auth.py ADDED
@@ -0,0 +1,137 @@
1
+ """GitHub App authentication using PyGithub's native GitHub App support."""
2
+
3
+ import hashlib
4
+ import hmac
5
+ import logging
6
+
7
+ import github
8
+ from github import Auth, Github, GithubIntegration
9
+ from github.Installation import Installation
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class GitHubAppAuth:
15
+ """GitHub App authentication using PyGithub's GithubIntegration.
16
+
17
+ Wraps PyGithub's native GitHub App support with:
18
+ - Webhook signature verification
19
+ - Convenient access to Github client instances
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ app_id: str | int,
25
+ private_key: str,
26
+ ):
27
+ self.app_id = str(app_id)
28
+ self.private_key = private_key
29
+
30
+ # Create PyGithub GithubIntegration
31
+ self._auth = Auth.AppAuth(app_id=self.app_id, private_key=private_key)
32
+ self._integration = GithubIntegration(auth=self._auth)
33
+
34
+ # Cache for Github clients per installation
35
+ self._clients: dict[int, Github] = {}
36
+
37
+ @property
38
+ def integration(self) -> GithubIntegration:
39
+ """Get the GithubIntegration instance."""
40
+ return self._integration
41
+
42
+ def get_installation_token(self, installation_id: int) -> str | None:
43
+ """Get an installation access token.
44
+
45
+ Args:
46
+ installation_id: GitHub App installation ID
47
+
48
+ Returns:
49
+ Installation token or None if failed
50
+ """
51
+ # Get fresh token
52
+ try:
53
+ # Fallback: create token directly via API
54
+ return self._create_installation_token(installation_id)
55
+ except github.GithubException as e:
56
+ logger.error(f"Failed to get installation token: {e}")
57
+ return None
58
+
59
+ def _create_installation_token(self, installation_id: int) -> str | None:
60
+ """Create installation token via GitHub API (fallback method)."""
61
+ return self._integration.get_access_token(installation_id=installation_id).token
62
+
63
+ def get_installation(self, installation_id: int) -> Installation | None:
64
+ """Get installation by ID using PyGithub.
65
+
66
+ Args:
67
+ installation_id: GitHub App installation ID
68
+
69
+ Returns:
70
+ Installation object or None
71
+ """
72
+ try:
73
+ return self._integration.get_app_installation(installation_id)
74
+ except github.GithubException as e:
75
+ logger.error(f"Failed to get installation {installation_id}: {e}")
76
+ return None
77
+
78
+ def get_repo_installation(self, owner: str, repo: str) -> Installation | None:
79
+ """Get installation for a specific repository.
80
+
81
+ Args:
82
+ owner: Repository owner
83
+ repo: Repository name
84
+
85
+ Returns:
86
+ Installation object or None
87
+ """
88
+ try:
89
+ return self._integration.get_repo_installation(owner, repo)
90
+ except github.GithubException as e:
91
+ logger.error(f"Failed to get installation for {owner}/{repo}: {e}")
92
+ return None
93
+
94
+ def get_org_installation(self, org: str) -> Installation | None:
95
+ """Get installation for an organization.
96
+
97
+ Args:
98
+ org: Organization name
99
+
100
+ Returns:
101
+ Installation object or None
102
+ """
103
+ try:
104
+ return self._integration.get_org_installation(org)
105
+ except github.GithubException as e:
106
+ logger.error(f"Failed to get installation for org {org}: {e}")
107
+ return None
108
+
109
+ def list_installations(self) -> list[Installation]:
110
+ """List all installations for this GitHub App.
111
+
112
+ Returns:
113
+ List of Installation objects
114
+ """
115
+ try:
116
+ return list(self._integration.get_installations())
117
+ except github.GithubException as e:
118
+ logger.error(f"Failed to list installations: {e}")
119
+ return []
120
+
121
+ def verify_webhook_payload(self, payload: str | bytes, signature: str, secret: str) -> bool:
122
+ """Verify GitHub webhook signature.
123
+
124
+ Args:
125
+ payload: Raw webhook payload (string or bytes)
126
+ signature: X-Hub-Signature-256 header value
127
+ secret: Webhook secret
128
+
129
+ Returns:
130
+ True if signature is valid
131
+ """
132
+ if isinstance(payload, str):
133
+ payload = payload.encode()
134
+
135
+ expected_signature = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
136
+
137
+ return hmac.compare_digest(f"sha256={expected_signature}", signature)
github_app/config.py ADDED
@@ -0,0 +1,38 @@
1
+ from typing import Optional
2
+
3
+ from pydantic_settings import BaseSettings, SettingsConfigDict
4
+
5
+
6
+ class GitHubAppSettings(BaseSettings):
7
+ # GitHub App Configuration
8
+ GITHUB_APP_ID: str = ""
9
+ GITHUB_APP_PRIVATE_KEY: str = ""
10
+ GITHUB_APP_WEBHOOK_SECRET: str = ""
11
+ GITHUB_APP_NAME: str = "Polycode GitHub App"
12
+
13
+ # Redis Configuration
14
+ REDIS_URL: str = "redis://localhost:6379/0"
15
+ REDIS_HOST: str = "localhost"
16
+ REDIS_PORT: int = 6379
17
+ REDIS_DB: int = 0
18
+
19
+ # Database Configuration
20
+ DATABASE_URL: str = "sqlite:///polycode.db"
21
+
22
+ # Webhook Server Configuration
23
+ WEBHOOK_HOST: str = "0.0.0.0"
24
+ WEBHOOK_PORT: int = 8000
25
+ WEBHOOK_PATH: str = "/webhook/github"
26
+ WEBHOOK_URL: Optional[str] = None
27
+
28
+ # GitHub API Configuration
29
+ GITHUB_API_URL: str = "https://api.github.com"
30
+
31
+ # Celery Configuration
32
+ CELERY_BROKER_URL: str = "redis://localhost:6379/0"
33
+ CELERY_RESULT_BACKEND: str = "redis://localhost:6379/0"
34
+
35
+ model_config = SettingsConfigDict(extra="ignore", env_file=".env", case_sensitive=True)
36
+
37
+
38
+ settings = GitHubAppSettings() # pyright:ignore # ty:ignore