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 +1 -1
- github_sync/api.py +111 -0
- github_sync/cli.py +35 -0
- github_sync/config.py +3 -0
- github_sync/setup.py +5 -0
- {github_sync_agent-0.1.4.dist-info → github_sync_agent-0.2.0.dist-info}/METADATA +66 -2
- github_sync_agent-0.2.0.dist-info/RECORD +11 -0
- github_sync_agent-0.1.4.dist-info/RECORD +0 -10
- {github_sync_agent-0.1.4.dist-info → github_sync_agent-0.2.0.dist-info}/WHEEL +0 -0
- {github_sync_agent-0.1.4.dist-info → github_sync_agent-0.2.0.dist-info}/entry_points.txt +0 -0
- {github_sync_agent-0.1.4.dist-info → github_sync_agent-0.2.0.dist-info}/top_level.txt +0 -0
github_sync/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.
|
|
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.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|