github-sync-agent 0.1.4__py3-none-any.whl → 0.2.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.
github_sync/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.1.4"
1
+ __version__ = "0.2.0"
github_sync/api.py ADDED
@@ -0,0 +1,111 @@
1
+ """HTTP API for the GitHub Sync Agent."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+
8
+ from fastapi import Depends, FastAPI, HTTPException, Security
9
+ from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
10
+ from pydantic import BaseModel, Field
11
+
12
+ from . import __version__
13
+ from . import agent
14
+
15
+ LOG = logging.getLogger(__name__)
16
+ _security = HTTPBearer(auto_error=False)
17
+
18
+
19
+ class SyncRequest(BaseModel):
20
+ dry_run: bool = False
21
+
22
+
23
+ class SyncResult(BaseModel):
24
+ project: str
25
+ success: bool
26
+
27
+
28
+ class SyncResponse(BaseModel):
29
+ success: bool
30
+ synced: int
31
+ failed: int
32
+ results: list[SyncResult]
33
+
34
+
35
+ class StatusRow(BaseModel):
36
+ project: str
37
+ root: str
38
+ git: bool
39
+ remote: bool
40
+ changes: bool
41
+ on_github: bool
42
+
43
+
44
+ class StatusResponse(BaseModel):
45
+ github_user: str
46
+ projects_root: list[str]
47
+ projects: list[StatusRow]
48
+
49
+
50
+ def create_app(cfg: dict) -> FastAPI:
51
+ api_key: str = cfg.get("api_key") or ""
52
+
53
+ app = FastAPI(
54
+ title="GitHub Sync Agent API",
55
+ description="Trigger and inspect GitHub sync operations over HTTP.",
56
+ version=__version__,
57
+ )
58
+
59
+ async def require_auth(
60
+ credentials: HTTPAuthorizationCredentials | None = Security(_security),
61
+ ) -> None:
62
+ if not api_key:
63
+ return
64
+ if credentials is None or credentials.credentials != api_key:
65
+ raise HTTPException(status_code=401, detail="Invalid or missing API key")
66
+
67
+ @app.get("/health")
68
+ async def health() -> dict[str, str]:
69
+ return {"status": "ok", "version": __version__}
70
+
71
+ @app.get("/status", response_model=StatusResponse, dependencies=[Depends(require_auth)])
72
+ async def status() -> StatusResponse:
73
+ rows = await asyncio.to_thread(agent.get_status, cfg)
74
+ return StatusResponse(
75
+ github_user=cfg["github_user"],
76
+ projects_root=cfg["projects_root"],
77
+ projects=[StatusRow(**row) for row in rows],
78
+ )
79
+
80
+ @app.post("/sync", response_model=SyncResponse, dependencies=[Depends(require_auth)])
81
+ async def sync_all(body: SyncRequest = SyncRequest()) -> SyncResponse:
82
+ return await _run_sync(cfg, project=None, dry_run=body.dry_run)
83
+
84
+ @app.post("/sync/{project}", response_model=SyncResponse, dependencies=[Depends(require_auth)])
85
+ async def sync_one(project: str, body: SyncRequest = SyncRequest()) -> SyncResponse:
86
+ return await _run_sync(cfg, project=project, dry_run=body.dry_run)
87
+
88
+ return app
89
+
90
+
91
+ async def _run_sync(cfg: dict, project: str | None, dry_run: bool) -> SyncResponse:
92
+ try:
93
+ projects = await asyncio.to_thread(agent.discover_projects, cfg, project)
94
+ except ValueError as exc:
95
+ raise HTTPException(status_code=404, detail=str(exc)) from exc
96
+
97
+ results: list[SyncResult] = []
98
+ for path in projects:
99
+ ok = await asyncio.to_thread(agent.sync_project, path, cfg, dry_run)
100
+ results.append(SyncResult(project=path.name, success=ok))
101
+ level = logging.INFO if ok else logging.ERROR
102
+ LOG.log(level, "%s %s", path.name, "synced" if ok else "FAILED")
103
+
104
+ synced = sum(1 for r in results if r.success)
105
+ failed = len(results) - synced
106
+ return SyncResponse(
107
+ success=failed == 0,
108
+ synced=synced,
109
+ failed=failed,
110
+ results=results,
111
+ )
github_sync/cli.py CHANGED
@@ -253,9 +253,44 @@ def init() -> None:
253
253
  click.echo(f" github-sync status # see all your projects")
254
254
  click.echo(f" github-sync sync --dry-run # preview what will be synced")
255
255
  click.echo(f" github-sync sync # sync everything to GitHub")
256
+ click.echo(f" github-sync serve # start HTTP API (requires [api] extra)")
256
257
  click.echo()
257
258
 
258
259
 
260
+ # ---------------------------------------------------------------------------
261
+ # serve
262
+ # ---------------------------------------------------------------------------
263
+
264
+ @cli.command()
265
+ @click.option("--host", default=None, help="Bind address (default: api_host from config)")
266
+ @click.option("--port", default=None, type=int, help="Port (default: api_port from config)")
267
+ @click.pass_context
268
+ def serve(ctx: click.Context, host: str | None, port: int | None) -> None:
269
+ """Start the HTTP API server."""
270
+ try:
271
+ import uvicorn
272
+ except ImportError:
273
+ click.echo("Error: API dependencies not installed.", err=True)
274
+ click.echo("Run: pip install 'github-sync-agent[api]'", err=True)
275
+ sys.exit(1)
276
+
277
+ cfg = ctx.obj["cfg"]
278
+ _setup_logging(cfg)
279
+
280
+ from .api import create_app
281
+
282
+ bind_host = host or cfg.get("api_host", "127.0.0.1")
283
+ bind_port = port or int(cfg.get("api_port", 8741))
284
+ app = create_app(cfg)
285
+
286
+ auth_note = "API key required" if cfg.get("api_key") else "no auth (localhost only recommended)"
287
+ click.echo(f"Starting GitHub Sync API on http://{bind_host}:{bind_port} ({auth_note})")
288
+ click.echo("Endpoints: GET /health GET /status POST /sync POST /sync/{project}")
289
+ click.echo()
290
+
291
+ uvicorn.run(app, host=bind_host, port=bind_port, log_level=cfg.get("log_level", "info").lower())
292
+
293
+
259
294
  # ---------------------------------------------------------------------------
260
295
  # Entry point
261
296
  # ---------------------------------------------------------------------------
github_sync/config.py CHANGED
@@ -26,6 +26,9 @@ def _apply_defaults(cfg: dict) -> None:
26
26
  cfg.setdefault("exclude", [])
27
27
  cfg.setdefault("log_file", str(Path.home() / ".local" / "share" / "github-sync" / "github-sync.log"))
28
28
  cfg.setdefault("log_level", "INFO")
29
+ cfg.setdefault("api_host", "127.0.0.1")
30
+ cfg.setdefault("api_port", 8741)
31
+ cfg.setdefault("api_key", "")
29
32
 
30
33
  if not cfg.get("github_user"):
31
34
  raise ValueError("Missing required config key: 'github_user'")
github_sync/setup.py CHANGED
@@ -276,6 +276,11 @@ exclude: []
276
276
  # Logging
277
277
  log_file: {log_file}
278
278
  log_level: INFO
279
+
280
+ # HTTP API (github-sync serve)
281
+ api_host: "127.0.0.1"
282
+ api_port: 8741
283
+ # api_key: "change-me" # uncomment to require: Authorization: Bearer <key>
279
284
  """
280
285
 
281
286
 
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: github-sync-agent
3
- Version: 0.1.4
3
+ Version: 0.2.0
4
4
  Summary: Sync all your local projects to GitHub automatically
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/yuvalavni/Agents
7
7
  Project-URL: Repository, https://github.com/yuvalavni/Agents
8
8
  Project-URL: Bug Tracker, https://github.com/yuvalavni/Agents/issues
9
- Keywords: github,git,sync,backup,automation,cli
9
+ Keywords: github,git,sync,backup,automation,cli,api
10
10
  Classifier: Programming Language :: Python :: 3
11
11
  Classifier: License :: OSI Approved :: MIT License
12
12
  Classifier: Operating System :: POSIX :: Linux
@@ -16,6 +16,9 @@ Requires-Python: >=3.10
16
16
  Description-Content-Type: text/markdown
17
17
  Requires-Dist: pyyaml>=6.0
18
18
  Requires-Dist: click>=8.0
19
+ Provides-Extra: api
20
+ Requires-Dist: fastapi>=0.100; extra == "api"
21
+ Requires-Dist: uvicorn>=0.23; extra == "api"
19
22
 
20
23
  # GitHub Sync Agent
21
24
 
@@ -74,6 +77,64 @@ github-sync status
74
77
 
75
78
  ---
76
79
 
80
+ ## HTTP API
81
+
82
+ Install API dependencies:
83
+
84
+ ```bash
85
+ pip install 'github-sync-agent[api]'
86
+ ```
87
+
88
+ Start the server:
89
+
90
+ ```bash
91
+ github-sync serve
92
+ # → http://127.0.0.1:8741
93
+ ```
94
+
95
+ ### Endpoints
96
+
97
+ | Method | Path | Description |
98
+ |--------|------|-------------|
99
+ | `GET` | `/health` | Health check (no auth) |
100
+ | `GET` | `/status` | Sync status for all projects |
101
+ | `POST` | `/sync` | Sync all projects |
102
+ | `POST` | `/sync/{project}` | Sync one project |
103
+
104
+ Optional body for POST: `{"dry_run": false}`
105
+
106
+ ### Examples
107
+
108
+ ```bash
109
+ curl http://127.0.0.1:8741/health
110
+ curl http://127.0.0.1:8741/status
111
+ curl -X POST http://127.0.0.1:8741/sync
112
+ curl -X POST http://127.0.0.1:8741/sync/Agents
113
+ ```
114
+
115
+ ### API authentication (optional)
116
+
117
+ Add to `config.yaml`:
118
+
119
+ ```yaml
120
+ api_key: "your-secret-key"
121
+ ```
122
+
123
+ Then pass the key on every request (except `/health`):
124
+
125
+ ```bash
126
+ curl -H "Authorization: Bearer your-secret-key" http://127.0.0.1:8741/status
127
+ ```
128
+
129
+ ### Run as a systemd service
130
+
131
+ ```bash
132
+ cp systemd/github-sync-api.service ~/.config/systemd/user/
133
+ systemctl --user enable --now github-sync-api.service
134
+ ```
135
+
136
+ ---
137
+
77
138
  ## Configuration (`~/.config/github-sync/config.yaml`)
78
139
 
79
140
  Generated by `github-sync init`. You can edit it at any time.
@@ -88,6 +149,9 @@ Generated by `github-sync init`. You can edit it at any time.
88
149
  | `exclude` | `[]` | Directory names to skip |
89
150
  | `log_file` | `~/.local/share/github-sync/github-sync.log` | Log file path |
90
151
  | `log_level` | `INFO` | `DEBUG` / `INFO` / `WARNING` / `ERROR` |
152
+ | `api_host` | `127.0.0.1` | API bind address |
153
+ | `api_port` | `8741` | API port |
154
+ | `api_key` | _(empty)_ | Optional Bearer token for API auth |
91
155
 
92
156
  ### Excluding a project
93
157
 
@@ -0,0 +1,11 @@
1
+ github_sync/__init__.py,sha256=Zn1KFblwuFHiDRdRAiRnDBRkbPttWh44jKa5zG2ov0E,22
2
+ github_sync/agent.py,sha256=2WnJCwupDwT3C9bNZLoj2CAvZW-eoqZChqFTk4wNRKw,6692
3
+ github_sync/api.py,sha256=4EK1ZXgfQyGD3WBPsRlMipwyZTS0DWZLSLuvRV8tU7Y,3320
4
+ github_sync/cli.py,sha256=fOC532dUM5aZUaKcR0ljYc8OcoUs299JWzT6uEdrAf8,11407
5
+ github_sync/config.py,sha256=mnBYyIbtAnMGtjrs3gl-mg35Argos0sk_zOBeUdJpfA,1559
6
+ github_sync/setup.py,sha256=yxast9nVJiTC2OCxaEnKk0wwTmH2kOO2pLG2CKJDOmg,8650
7
+ github_sync_agent-0.2.0.dist-info/METADATA,sha256=HFlq_rklk8ExcLmRxTpm_HcgcPBInOMCA932Dz7TP6o,4115
8
+ github_sync_agent-0.2.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
9
+ github_sync_agent-0.2.0.dist-info/entry_points.txt,sha256=-LmJUTvRzj5cvyS-dEWdipouBj3dgkW6hqrTz1GmyGw,53
10
+ github_sync_agent-0.2.0.dist-info/top_level.txt,sha256=fPYTA83USK1N4JwfouV8zcejgWfWmqRAKQVDCAT8a5I,12
11
+ github_sync_agent-0.2.0.dist-info/RECORD,,
@@ -1,10 +0,0 @@
1
- github_sync/__init__.py,sha256=Wzf5T3NBDfhQoTnhnRNHSlAsE0XMqbclXG-M81Vas70,22
2
- github_sync/agent.py,sha256=2WnJCwupDwT3C9bNZLoj2CAvZW-eoqZChqFTk4wNRKw,6692
3
- github_sync/cli.py,sha256=72iJIhveIXoYJUdi375iazVf21Q1RAaCwqgaX-VRJJk,9976
4
- github_sync/config.py,sha256=BQrSW7DyhcZB6gZnDKkc-W6u1vtD-l76Rn_gW8Egypk,1444
5
- github_sync/setup.py,sha256=8QS0bBjp1dZfUlfxmnoNu87ULDsUxDzqTyVG7zuXtuE,8504
6
- github_sync_agent-0.1.4.dist-info/METADATA,sha256=IU6Z_jprTGtNZq6kKO47jrXzCzRTaJkaTWqQlQ0tCfA,2782
7
- github_sync_agent-0.1.4.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
8
- github_sync_agent-0.1.4.dist-info/entry_points.txt,sha256=-LmJUTvRzj5cvyS-dEWdipouBj3dgkW6hqrTz1GmyGw,53
9
- github_sync_agent-0.1.4.dist-info/top_level.txt,sha256=fPYTA83USK1N4JwfouV8zcejgWfWmqRAKQVDCAT8a5I,12
10
- github_sync_agent-0.1.4.dist-info/RECORD,,