gitcast 1.0.5__tar.gz → 1.0.6__tar.gz
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.
- {gitcast-1.0.5 → gitcast-1.0.6}/.env.example +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/LICENSE +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/MANIFEST.in +0 -0
- {gitcast-1.0.5/gitcast.egg-info → gitcast-1.0.6}/PKG-INFO +77 -50
- {gitcast-1.0.5 → gitcast-1.0.6}/README.md +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/ai/__init__.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/ai/formatter.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/ai/generator.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/ai/prompts.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/ai/viral_patterns.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/api/__init__.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/api/analytics.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/api/auth.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/api/auth_middleware.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/api/auth_routes.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/api/monitoring.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/api/payload.py +0 -0
- gitcast-1.0.6/api/ratelimit.py +18 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/api/routes.py +22 -41
- {gitcast-1.0.5 → gitcast-1.0.6}/api/server.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/api/validators.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/assets/__init__.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/assets/favicon-16x16.png +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/assets/favicon-32x32.png +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/assets/favicon-64x64.png +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/assets/favicon.ico +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/assets/icon.png +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/cli/.env.example +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/cli/__init__.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/cli/gitcast.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/config/__init__.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/config/settings.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/core/__init__.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/core/capture.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/core/codebase_reader.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/core/framing.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/core/hotkey.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/core/log_stream.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/core/ocr.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/core/screenshot_session.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/core/security.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/core/tray.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/docs/api-keys.md +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/docs/article-generator.md +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/docs/capture-flow.md +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/docs/changelog.md +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/docs/contributing.md +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/docs/dashboard.md +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/docs/insights.md +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/docs/introduction.md +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/docs/post-formats.md +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/docs/providers.md +0 -0
- gitcast-1.0.6/docs/pypi-errors.md +68 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/docs/quickstart.md +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/docs/sprint-mode.md +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6/gitcast.egg-info}/PKG-INFO +77 -50
- {gitcast-1.0.5 → gitcast-1.0.6}/gitcast.egg-info/SOURCES.txt +3 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/gitcast.egg-info/dependency_links.txt +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/gitcast.egg-info/entry_points.txt +0 -1
- {gitcast-1.0.5 → gitcast-1.0.6}/gitcast.egg-info/requires.txt +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/gitcast.egg-info/top_level.txt +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/publisher/__init__.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/publisher/clipboard.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/publisher/twitter.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/pyproject.toml +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/requirements.txt +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/setup.cfg +4 -4
- {gitcast-1.0.5 → gitcast-1.0.6}/setup.py +33 -1
- {gitcast-1.0.5 → gitcast-1.0.6}/storage/__init__.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/storage/cleanup.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/storage/engagement.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/storage/insights.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/storage/key_manager.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/storage/logger.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/storage/metrics.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/storage/sprint.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/storage/streak.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/storage/supabase_client.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/storage/tone_memory.py +0 -0
- gitcast-1.0.6/tests/test_keys.py +86 -0
- gitcast-1.0.6/tests/test_ocr_fix.py +77 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/ui/__init__.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/web/__init__.py +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/web/index.html +0 -0
- {gitcast-1.0.5 → gitcast-1.0.6}/web/landing.html +0 -0
- gitcast-1.0.5/api/ratelimit.py +0 -9
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -1,50 +1,77 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
2
|
-
Name: gitcast
|
|
3
|
-
Version: 1.0.
|
|
4
|
-
Summary: git diff → published post. under 60 seconds.
|
|
5
|
-
Home-page: https://github.com/drizzy765/gitcast
|
|
6
|
-
Author: Timilehin Agoro
|
|
7
|
-
Author-email: agorotimilehi05@gmail.com
|
|
8
|
-
License: MIT
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gitcast
|
|
3
|
+
Version: 1.0.6
|
|
4
|
+
Summary: git diff → published post. under 60 seconds.
|
|
5
|
+
Home-page: https://github.com/drizzy765/gitcast
|
|
6
|
+
Author: Timilehin Agoro
|
|
7
|
+
Author-email: agorotimilehi05@gmail.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: pynput
|
|
13
|
+
Requires-Dist: pystray
|
|
14
|
+
Requires-Dist: Pillow
|
|
15
|
+
Requires-Dist: fastapi
|
|
16
|
+
Requires-Dist: uvicorn
|
|
17
|
+
Requires-Dist: python-dotenv
|
|
18
|
+
Requires-Dist: groq
|
|
19
|
+
Requires-Dist: tweepy
|
|
20
|
+
Requires-Dist: mss
|
|
21
|
+
Requires-Dist: pytesseract
|
|
22
|
+
Requires-Dist: httpx
|
|
23
|
+
Requires-Dist: supabase
|
|
24
|
+
Requires-Dist: cryptography
|
|
25
|
+
Requires-Dist: python-jose[cryptography]
|
|
26
|
+
Requires-Dist: rich
|
|
27
|
+
Requires-Dist: keyboard
|
|
28
|
+
Requires-Dist: slowapi
|
|
29
|
+
Requires-Dist: limits
|
|
30
|
+
Requires-Dist: sentry-sdk[fastapi]
|
|
31
|
+
Requires-Dist: posthog
|
|
32
|
+
Dynamic: author
|
|
33
|
+
Dynamic: author-email
|
|
34
|
+
Dynamic: description
|
|
35
|
+
Dynamic: description-content-type
|
|
36
|
+
Dynamic: home-page
|
|
37
|
+
Dynamic: license
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
Dynamic: requires-dist
|
|
40
|
+
Dynamic: requires-python
|
|
41
|
+
Dynamic: summary
|
|
42
|
+
|
|
43
|
+
# Gitcast
|
|
44
|
+
|
|
45
|
+
On-demand, privacy-first desktop utility that captures active window context,
|
|
46
|
+
extracts code changes, and generates platform-ready X (Twitter) posts with one keystroke.
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install gitcast
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Setup
|
|
55
|
+
|
|
56
|
+
1. Install Tesseract OCR:
|
|
57
|
+
- Windows: https://github.com/UB-Mannheim/tesseract/wiki
|
|
58
|
+
- Mac: `brew install tesseract`
|
|
59
|
+
- Linux: `sudo apt install tesseract-ocr`
|
|
60
|
+
|
|
61
|
+
2. Add your API keys:
|
|
62
|
+
```bash
|
|
63
|
+
gitcast --setup
|
|
64
|
+
```
|
|
65
|
+
(opens `.env` file for editing)
|
|
66
|
+
|
|
67
|
+
3. Run:
|
|
68
|
+
```bash
|
|
69
|
+
gitcast
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Dashboard opens at http://127.0.0.1:8000
|
|
73
|
+
Press Ctrl+Shift+P from anywhere to capture.
|
|
74
|
+
|
|
75
|
+
## Stack
|
|
76
|
+
- Python 3.11+
|
|
77
|
+
- FastAPI, pynput, mss, Tesseract, Groq API, Tweepy
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from slowapi import Limiter
|
|
2
|
+
from slowapi.util import get_remote_address
|
|
3
|
+
from fastapi import Request
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_client_ip(request: Request) -> str:
|
|
7
|
+
client_host = request.client.host if request.client else None
|
|
8
|
+
if client_host in ["127.0.0.1", "localhost", "::1"]:
|
|
9
|
+
return None
|
|
10
|
+
return get_remote_address(request)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
limiter = Limiter(key_func=get_client_ip, default_limits=["60/minute"], headers_enabled=False)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
if __name__ == "__main__":
|
|
17
|
+
print("[RateLimit] Default limit: 60/minute (bypassed for localhost)")
|
|
18
|
+
|
|
@@ -541,8 +541,7 @@ def _update_settings_for_user(user_id: str, values: dict) -> dict:
|
|
|
541
541
|
|
|
542
542
|
|
|
543
543
|
@router.get("/keys/status")
|
|
544
|
-
def get_ai_keys_status():
|
|
545
|
-
user_id = LOCAL_USER_ID
|
|
544
|
+
def get_ai_keys_status(user_id: str = Depends(get_current_user)):
|
|
546
545
|
if _is_local_user(user_id):
|
|
547
546
|
env_values = dotenv_values(_env_path())
|
|
548
547
|
return {
|
|
@@ -668,9 +667,8 @@ def add_to_waitlist(request: WaitlistRequest):
|
|
|
668
667
|
|
|
669
668
|
@router.patch("/settings")
|
|
670
669
|
@router.post("/settings")
|
|
671
|
-
def update_settings(update: SettingsUpdate):
|
|
670
|
+
def update_settings(update: SettingsUpdate, user_id: str = Depends(get_current_user)):
|
|
672
671
|
"""Updates user project settings."""
|
|
673
|
-
user_id = LOCAL_USER_ID
|
|
674
672
|
values = update.dict(exclude_unset=True)
|
|
675
673
|
if "project_narrative" in values and values["project_narrative"] is not None:
|
|
676
674
|
values["project_narrative"] = sanitize_text(values["project_narrative"])
|
|
@@ -680,9 +678,8 @@ def update_settings(update: SettingsUpdate):
|
|
|
680
678
|
|
|
681
679
|
@router.post("/publish")
|
|
682
680
|
@limiter.limit("10/minute")
|
|
683
|
-
async def publish(request: Request, body: PublishRequest):
|
|
681
|
+
async def publish(request: Request, body: PublishRequest, user_id: str = Depends(get_current_user)):
|
|
684
682
|
"""Publishes a post to X (Twitter)."""
|
|
685
|
-
user_id = LOCAL_USER_ID
|
|
686
683
|
from publisher.twitter import publish_post
|
|
687
684
|
try:
|
|
688
685
|
post_text = sanitize_text(body.post_text)
|
|
@@ -783,8 +780,7 @@ async def get_framed_screenshot(filename: str):
|
|
|
783
780
|
|
|
784
781
|
@router.post("/generate", response_model=GenerateResponse)
|
|
785
782
|
@limiter.limit("10/minute")
|
|
786
|
-
async def generate(request: Request, body: GenerateRequest):
|
|
787
|
-
user_id = LOCAL_USER_ID
|
|
783
|
+
async def generate(request: Request, body: GenerateRequest, user_id: str = Depends(get_current_user)):
|
|
788
784
|
payload = body.dict()
|
|
789
785
|
for key, value in list(payload.items()):
|
|
790
786
|
if isinstance(value, str):
|
|
@@ -866,9 +862,8 @@ def get_current_draft():
|
|
|
866
862
|
|
|
867
863
|
@router.post("/chat")
|
|
868
864
|
@limiter.limit("20/minute")
|
|
869
|
-
async def chat_refine(request: Request, body: ChatRequest):
|
|
865
|
+
async def chat_refine(request: Request, body: ChatRequest, user_id: str = Depends(get_current_user)):
|
|
870
866
|
"""Refines a post variation using AI chat."""
|
|
871
|
-
user_id = LOCAL_USER_ID
|
|
872
867
|
if not CURRENT_DRAFT.exists():
|
|
873
868
|
raise HTTPException(status_code=400, detail="No active draft to refine.")
|
|
874
869
|
|
|
@@ -926,9 +921,8 @@ async def chat_refine(request: Request, body: ChatRequest):
|
|
|
926
921
|
|
|
927
922
|
|
|
928
923
|
@router.post("/cli/trigger")
|
|
929
|
-
async def cli_trigger(request: CliTriggerRequest):
|
|
924
|
+
async def cli_trigger(request: CliTriggerRequest, user_id: str = Depends(get_current_user)):
|
|
930
925
|
"""Triggers a capture workflow with a specific thought from the CLI."""
|
|
931
|
-
user_id = LOCAL_USER_ID
|
|
932
926
|
from core.capture import run_capture
|
|
933
927
|
from core.ocr import run_ocr
|
|
934
928
|
from api.payload import build_payload
|
|
@@ -964,9 +958,8 @@ async def cli_trigger(request: CliTriggerRequest):
|
|
|
964
958
|
|
|
965
959
|
|
|
966
960
|
@router.post("/capture/trigger")
|
|
967
|
-
async def ui_trigger_capture(request: CaptureTriggerRequest):
|
|
961
|
+
async def ui_trigger_capture(request: CaptureTriggerRequest, user_id: str = Depends(get_current_user)):
|
|
968
962
|
"""Triggers a capture workflow from the UI."""
|
|
969
|
-
user_id = LOCAL_USER_ID
|
|
970
963
|
from core.capture import run_capture
|
|
971
964
|
from core.ocr import run_ocr
|
|
972
965
|
from api.payload import build_payload
|
|
@@ -1045,9 +1038,8 @@ def list_screenshots():
|
|
|
1045
1038
|
|
|
1046
1039
|
|
|
1047
1040
|
@router.post("/screenshot/recommend")
|
|
1048
|
-
async def recommend_screenshot(request: RecommendRequest):
|
|
1041
|
+
async def recommend_screenshot(request: RecommendRequest, user_id: str = Depends(get_current_user)):
|
|
1049
1042
|
"""Uses AI to recommend where to post a specific screenshot."""
|
|
1050
|
-
user_id = LOCAL_USER_ID
|
|
1051
1043
|
from config.settings import STORAGE_DIR
|
|
1052
1044
|
path = STORAGE_DIR / "screenshots" / sanitize_text(request.filename)
|
|
1053
1045
|
if not path.exists():
|
|
@@ -1137,16 +1129,14 @@ def get_formats():
|
|
|
1137
1129
|
|
|
1138
1130
|
|
|
1139
1131
|
@router.get("/settings/plan")
|
|
1140
|
-
def get_plan():
|
|
1141
|
-
user_id = LOCAL_USER_ID
|
|
1132
|
+
def get_plan(user_id: str = Depends(get_current_user)):
|
|
1142
1133
|
settings = _settings_for_user(user_id)
|
|
1143
1134
|
preferred = settings.get("preferred_providers") or {}
|
|
1144
1135
|
return {"plan": preferred.get("twitter_plan", get_twitter_plan())}
|
|
1145
1136
|
|
|
1146
1137
|
|
|
1147
1138
|
@router.put("/settings/plan")
|
|
1148
|
-
def update_plan(update: PlanUpdate):
|
|
1149
|
-
user_id = LOCAL_USER_ID
|
|
1139
|
+
def update_plan(update: PlanUpdate, user_id: str = Depends(get_current_user)):
|
|
1150
1140
|
plan = sanitize_text(update.plan).lower()
|
|
1151
1141
|
settings = _settings_for_user(user_id)
|
|
1152
1142
|
preferred = settings.get("preferred_providers") or {}
|
|
@@ -1156,14 +1146,12 @@ def update_plan(update: PlanUpdate):
|
|
|
1156
1146
|
|
|
1157
1147
|
|
|
1158
1148
|
@router.get("/settings")
|
|
1159
|
-
def get_all_settings():
|
|
1160
|
-
user_id = LOCAL_USER_ID
|
|
1149
|
+
def get_all_settings(user_id: str = Depends(get_current_user)):
|
|
1161
1150
|
return _settings_for_user(user_id)
|
|
1162
1151
|
|
|
1163
1152
|
|
|
1164
1153
|
@router.post("/settings/sprint/toggle")
|
|
1165
|
-
def sprint_toggle():
|
|
1166
|
-
user_id = LOCAL_USER_ID
|
|
1154
|
+
def sprint_toggle(user_id: str = Depends(get_current_user)):
|
|
1167
1155
|
settings = _settings_for_user(user_id)
|
|
1168
1156
|
new_state = not bool(settings.get("sprint_mode"))
|
|
1169
1157
|
_update_settings_for_user(user_id, {"sprint_mode": new_state})
|
|
@@ -1171,15 +1159,13 @@ def sprint_toggle():
|
|
|
1171
1159
|
|
|
1172
1160
|
|
|
1173
1161
|
@router.get("/settings/keys")
|
|
1174
|
-
def get_keys_status():
|
|
1175
|
-
user_id
|
|
1176
|
-
return get_ai_keys_status()
|
|
1162
|
+
def get_keys_status(user_id: str = Depends(get_current_user)):
|
|
1163
|
+
return get_ai_keys_status(user_id=user_id)
|
|
1177
1164
|
|
|
1178
1165
|
|
|
1179
1166
|
@router.get("/diagnose")
|
|
1180
|
-
async def diagnose_connectivity():
|
|
1167
|
+
async def diagnose_connectivity(user_id: str = Depends(get_current_user)):
|
|
1181
1168
|
"""Diagnoses network and authentication status of all configured AI providers."""
|
|
1182
|
-
user_id = LOCAL_USER_ID
|
|
1183
1169
|
import ai.generator
|
|
1184
1170
|
import httpx
|
|
1185
1171
|
|
|
@@ -1248,9 +1234,8 @@ async def diagnose_connectivity():
|
|
|
1248
1234
|
|
|
1249
1235
|
@router.post("/article/generate")
|
|
1250
1236
|
@limiter.limit("5/minute")
|
|
1251
|
-
async def generate_article(request: Request, body: ArticleGenerateRequest):
|
|
1237
|
+
async def generate_article(request: Request, body: ArticleGenerateRequest, user_id: str = Depends(get_current_user)):
|
|
1252
1238
|
"""Generates a full technical article from sprint context."""
|
|
1253
|
-
user_id = LOCAL_USER_ID
|
|
1254
1239
|
if not CURRENT_DRAFT.exists():
|
|
1255
1240
|
raise HTTPException(status_code=400, detail="No active draft to generate article from.")
|
|
1256
1241
|
|
|
@@ -1286,9 +1271,8 @@ async def generate_article(request: Request, body: ArticleGenerateRequest):
|
|
|
1286
1271
|
|
|
1287
1272
|
|
|
1288
1273
|
@router.post("/article/refine")
|
|
1289
|
-
async def refine_article(request: ArticleRefineRequest):
|
|
1274
|
+
async def refine_article(request: ArticleRefineRequest, user_id: str = Depends(get_current_user)):
|
|
1290
1275
|
"""Refines an article draft based on user instructions."""
|
|
1291
|
-
user_id = LOCAL_USER_ID
|
|
1292
1276
|
current_article = sanitize_text(request.current_article)
|
|
1293
1277
|
instruction = sanitize_text(request.instruction)
|
|
1294
1278
|
injection = check_prompt_injection(instruction)
|
|
@@ -1316,8 +1300,7 @@ def thread_split(request: ThreadSplitRequest):
|
|
|
1316
1300
|
|
|
1317
1301
|
|
|
1318
1302
|
@router.post("/posts/verify")
|
|
1319
|
-
async def verify_logged_post(request: PostVerifyRequest):
|
|
1320
|
-
user_id = LOCAL_USER_ID
|
|
1303
|
+
async def verify_logged_post(request: PostVerifyRequest, user_id: str = Depends(get_current_user)):
|
|
1321
1304
|
post_id = sanitize_text(request.post_id)
|
|
1322
1305
|
post_url = sanitize_text(request.post_url or "")
|
|
1323
1306
|
result = await safe_update_post(post_id, {
|
|
@@ -1333,8 +1316,7 @@ async def verify_logged_post(request: PostVerifyRequest):
|
|
|
1333
1316
|
|
|
1334
1317
|
|
|
1335
1318
|
@router.post("/posts/decline")
|
|
1336
|
-
async def decline_logged_post(request: PostDeclineRequest):
|
|
1337
|
-
user_id = LOCAL_USER_ID
|
|
1319
|
+
async def decline_logged_post(request: PostDeclineRequest, user_id: str = Depends(get_current_user)):
|
|
1338
1320
|
result = await safe_update_post(sanitize_text(request.post_id), {"declined": True})
|
|
1339
1321
|
if not result:
|
|
1340
1322
|
raise HTTPException(status_code=404, detail="post not found")
|
|
@@ -1347,8 +1329,7 @@ async def unverified_posts():
|
|
|
1347
1329
|
|
|
1348
1330
|
|
|
1349
1331
|
@router.post("/metrics/save")
|
|
1350
|
-
async def save_post_metrics(request: MetricsSaveRequest):
|
|
1351
|
-
user_id = LOCAL_USER_ID
|
|
1332
|
+
async def save_post_metrics(request: MetricsSaveRequest, user_id: str = Depends(get_current_user)):
|
|
1352
1333
|
values = {
|
|
1353
1334
|
"impressions": request.impressions,
|
|
1354
1335
|
"likes": request.likes,
|
|
@@ -1423,9 +1404,9 @@ async def get_post_metrics(post_id: str):
|
|
|
1423
1404
|
|
|
1424
1405
|
|
|
1425
1406
|
@router.get("/insights")
|
|
1426
|
-
async def get_insights():
|
|
1407
|
+
async def get_insights(user_id: str = Depends(get_current_user)):
|
|
1427
1408
|
now = time.time()
|
|
1428
|
-
cache_key = f"data:{
|
|
1409
|
+
cache_key = f"data:{user_id}"
|
|
1429
1410
|
if _INSIGHTS_CACHE.get(cache_key) is not None and now - _INSIGHTS_CACHE["timestamp"] < 3600:
|
|
1430
1411
|
return _INSIGHTS_CACHE[cache_key]
|
|
1431
1412
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# PyPI Release & Execution Issues Log
|
|
2
|
+
|
|
3
|
+
This document records the issues faced during the testing, packaging, and execution of `gitcast` on PyPI, along with their resolutions and current active status.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Resolved Issues
|
|
8
|
+
|
|
9
|
+
### Fixed in `gitcast==1.0.6`
|
|
10
|
+
|
|
11
|
+
#### Issue 1.4: FastAPI Server Rate Limiting on Localhost
|
|
12
|
+
* **Symptom**: Browser console logs API call failures when calling `api/article/generate` or other endpoints via the dashboard UI with a 429 Too Many Requests status code.
|
|
13
|
+
* **Root Cause**: The FastAPI server used slowapi's default `get_remote_address` rate-limiting key, which rate-limited local requests when they exceeded the threshold (e.g. 5/minute for article generation).
|
|
14
|
+
* **Resolution**: Replaced the default `get_remote_address` with a custom `get_client_ip` function in `api/ratelimit.py`. This function returns `None` for any localhost client (`127.0.0.1`, `localhost`, `::1`), which tells slowapi to bypass rate limiting completely for local executions.
|
|
15
|
+
|
|
16
|
+
#### Issue 1.5: BYOK (Bring Your Own Key) Feature Ignored Authenticated Users
|
|
17
|
+
* **Symptom**: When a user logged in via GitHub/Supabase and configured their own provider keys (BYOK), the server would still hit rate limits or token exhaustion on the developer's default keys.
|
|
18
|
+
* **Root Cause**: In `api/routes.py`, 19 routes hardcoded `user_id = LOCAL_USER_ID`. This caused the server to completely ignore the user ID of logged-in users, preventing their custom BYOK keys (stored in the Supabase DB) from being resolved and loaded.
|
|
19
|
+
* **Resolution**: Updated all 19 endpoints in `api/routes.py` to accept and use the dynamically resolved user ID using FastAPI's dependency injection (`user_id: str = Depends(get_current_user)`). This allows logged-in users to load their custom keys dynamically.
|
|
20
|
+
|
|
21
|
+
### Fixed in `gitcast==1.0.5`
|
|
22
|
+
|
|
23
|
+
#### Issue 1.1: Supabase Configuration Load Failure
|
|
24
|
+
* **Symptom**: Running `gitcast` from an external directory (e.g. `C:\Users\USER\Documents\snakegame`) outputs:
|
|
25
|
+
```text
|
|
26
|
+
[STORAGE] Supabase get_posts failed: SUPABASE_URL and SUPABASE_SERVICE_KEY must be configured. Falling back to local JSON.
|
|
27
|
+
```
|
|
28
|
+
* **Root Cause**: The dynamic resolution of `BASE_DIR = Path(__file__).resolve().parent.parent` in `settings.py` evaluated to Python's virtualenv `site-packages/gitcast/` directory rather than the active workspace directory. As a result, the `.env` file containing the Supabase keys was not found.
|
|
29
|
+
* **Resolution**: Replaced the direct loading of `BASE_DIR / ".env"` with a multi-path environment file loader. It now searches the following locations in order of priority:
|
|
30
|
+
1. A custom path defined in `GITCAST_ENV_PATH`
|
|
31
|
+
2. The current working directory (`os.getcwd() / ".env"`)
|
|
32
|
+
3. The parent directory of the active virtual environment (`sys.prefix / ".." / ".env"`), resolving to `context-engine/.env`
|
|
33
|
+
4. Standard user directories (`~/.gitcast/.env` and `~/.gitcast.env`)
|
|
34
|
+
5. The fallback package directory (`BASE_DIR / ".env"`)
|
|
35
|
+
|
|
36
|
+
#### Issue 1.2: UI Settings/BYOK Writes to Package Internals
|
|
37
|
+
* **Symptom**: Saving API keys via the dashboard (BYOK feature) or running `gitcast --setup` would write to `site-packages/gitcast/.env`, which was volatile (deleted on package updates) and sometimes generated permission access errors.
|
|
38
|
+
* **Resolution**: Updated `api/routes.py` and `cli/gitcast.py` to write to the resolved active `.env` file, falling back to a persistent user-home folder `~/.gitcast/.env` if no local env file existed.
|
|
39
|
+
|
|
40
|
+
#### Issue 1.3: Package Build Failure (`bdist_wheel` Invalid Command)
|
|
41
|
+
* **Symptom**: Attempting to package the build output using `python setup.py sdist bdist_wheel` generated the error:
|
|
42
|
+
```text
|
|
43
|
+
error: invalid command 'bdist_wheel'
|
|
44
|
+
```
|
|
45
|
+
* **Root Cause**: The `wheel` package was not installed in the python environment, causing `setuptools` to not register the command.
|
|
46
|
+
* **Resolution**: Installed `wheel` package (`pip install wheel`) prior to building, enabling setuptools to build the binary wheel package successfully.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## 2. Active Issues (To be Addressed)
|
|
51
|
+
|
|
52
|
+
### Issue 2.1: Post Generation Fails silently (gitcast>=1.0.5)
|
|
53
|
+
* **Symptom**: `gitcast` starts up successfully, the FastAPI server listens on `http://127.0.0.1:8000`, the pygame modules load, and the CLI runs, but the application does not generate posts.
|
|
54
|
+
* **Console Logs**:
|
|
55
|
+
```text
|
|
56
|
+
(venv) PS C:\Users\USER\Documents\snakegame> gitcast
|
|
57
|
+
[Monitoring] Sentry not configured - skipping
|
|
58
|
+
|
|
59
|
+
[Auth] Session Token: 549cfdc2-c987-416d-8a70-8b0dc5f2315b
|
|
60
|
+
[Server] Starting Gitcast API on http://127.0.0.1:8000
|
|
61
|
+
|
|
62
|
+
[Auth] Session Token: 549cfdc2-c987-416d-8a70-8b0dc5f2315b
|
|
63
|
+
[Server] Starting Gitcast API on http://127.0.0.1:8000
|
|
64
|
+
pygame 2.6.1 (SDL 2.28.4, Python 3.10.0)
|
|
65
|
+
Hello from the pygame community. https://www.pygame.org/contribute.html
|
|
66
|
+
```
|
|
67
|
+
* **Status**: Logged and waiting for future triage. The application starts, but key bindings or endpoints triggers are failing to generate the actual post output.
|
|
68
|
+
|
|
File without changes
|
|
File without changes
|
|
@@ -1,50 +1,77 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
2
|
-
Name: gitcast
|
|
3
|
-
Version: 1.0.
|
|
4
|
-
Summary: git diff → published post. under 60 seconds.
|
|
5
|
-
Home-page: https://github.com/drizzy765/gitcast
|
|
6
|
-
Author: Timilehin Agoro
|
|
7
|
-
Author-email: agorotimilehi05@gmail.com
|
|
8
|
-
License: MIT
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gitcast
|
|
3
|
+
Version: 1.0.6
|
|
4
|
+
Summary: git diff → published post. under 60 seconds.
|
|
5
|
+
Home-page: https://github.com/drizzy765/gitcast
|
|
6
|
+
Author: Timilehin Agoro
|
|
7
|
+
Author-email: agorotimilehi05@gmail.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: pynput
|
|
13
|
+
Requires-Dist: pystray
|
|
14
|
+
Requires-Dist: Pillow
|
|
15
|
+
Requires-Dist: fastapi
|
|
16
|
+
Requires-Dist: uvicorn
|
|
17
|
+
Requires-Dist: python-dotenv
|
|
18
|
+
Requires-Dist: groq
|
|
19
|
+
Requires-Dist: tweepy
|
|
20
|
+
Requires-Dist: mss
|
|
21
|
+
Requires-Dist: pytesseract
|
|
22
|
+
Requires-Dist: httpx
|
|
23
|
+
Requires-Dist: supabase
|
|
24
|
+
Requires-Dist: cryptography
|
|
25
|
+
Requires-Dist: python-jose[cryptography]
|
|
26
|
+
Requires-Dist: rich
|
|
27
|
+
Requires-Dist: keyboard
|
|
28
|
+
Requires-Dist: slowapi
|
|
29
|
+
Requires-Dist: limits
|
|
30
|
+
Requires-Dist: sentry-sdk[fastapi]
|
|
31
|
+
Requires-Dist: posthog
|
|
32
|
+
Dynamic: author
|
|
33
|
+
Dynamic: author-email
|
|
34
|
+
Dynamic: description
|
|
35
|
+
Dynamic: description-content-type
|
|
36
|
+
Dynamic: home-page
|
|
37
|
+
Dynamic: license
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
Dynamic: requires-dist
|
|
40
|
+
Dynamic: requires-python
|
|
41
|
+
Dynamic: summary
|
|
42
|
+
|
|
43
|
+
# Gitcast
|
|
44
|
+
|
|
45
|
+
On-demand, privacy-first desktop utility that captures active window context,
|
|
46
|
+
extracts code changes, and generates platform-ready X (Twitter) posts with one keystroke.
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install gitcast
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Setup
|
|
55
|
+
|
|
56
|
+
1. Install Tesseract OCR:
|
|
57
|
+
- Windows: https://github.com/UB-Mannheim/tesseract/wiki
|
|
58
|
+
- Mac: `brew install tesseract`
|
|
59
|
+
- Linux: `sudo apt install tesseract-ocr`
|
|
60
|
+
|
|
61
|
+
2. Add your API keys:
|
|
62
|
+
```bash
|
|
63
|
+
gitcast --setup
|
|
64
|
+
```
|
|
65
|
+
(opens `.env` file for editing)
|
|
66
|
+
|
|
67
|
+
3. Run:
|
|
68
|
+
```bash
|
|
69
|
+
gitcast
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Dashboard opens at http://127.0.0.1:8000
|
|
73
|
+
Press Ctrl+Shift+P from anywhere to capture.
|
|
74
|
+
|
|
75
|
+
## Stack
|
|
76
|
+
- Python 3.11+
|
|
77
|
+
- FastAPI, pynput, mss, Tesseract, Groq API, Tweepy
|
|
@@ -52,6 +52,7 @@ docs/insights.md
|
|
|
52
52
|
docs/introduction.md
|
|
53
53
|
docs/post-formats.md
|
|
54
54
|
docs/providers.md
|
|
55
|
+
docs/pypi-errors.md
|
|
55
56
|
docs/quickstart.md
|
|
56
57
|
docs/sprint-mode.md
|
|
57
58
|
gitcast.egg-info/PKG-INFO
|
|
@@ -74,6 +75,8 @@ storage/sprint.py
|
|
|
74
75
|
storage/streak.py
|
|
75
76
|
storage/supabase_client.py
|
|
76
77
|
storage/tone_memory.py
|
|
78
|
+
tests/test_keys.py
|
|
79
|
+
tests/test_ocr_fix.py
|
|
77
80
|
ui/__init__.py
|
|
78
81
|
web/__init__.py
|
|
79
82
|
web/index.html
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
[egg_info]
|
|
2
|
-
tag_build =
|
|
3
|
-
tag_date = 0
|
|
4
|
-
|
|
1
|
+
[egg_info]
|
|
2
|
+
tag_build =
|
|
3
|
+
tag_date = 0
|
|
4
|
+
|
|
@@ -1,6 +1,38 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import shutil
|
|
2
3
|
from setuptools import setup, find_packages
|
|
3
4
|
|
|
5
|
+
# Monkeypatch copystat, os.link, chmod, and utime to avoid "Operation not permitted" errors under WSL mounts
|
|
6
|
+
try:
|
|
7
|
+
if hasattr(os, 'link'):
|
|
8
|
+
del os.link
|
|
9
|
+
|
|
10
|
+
orig_chmod = os.chmod
|
|
11
|
+
def patched_chmod(path, mode, *args, **kwargs):
|
|
12
|
+
try:
|
|
13
|
+
orig_chmod(path, mode, *args, **kwargs)
|
|
14
|
+
except OSError:
|
|
15
|
+
pass
|
|
16
|
+
os.chmod = patched_chmod
|
|
17
|
+
|
|
18
|
+
orig_utime = os.utime
|
|
19
|
+
def patched_utime(path, times=None, *args, **kwargs):
|
|
20
|
+
try:
|
|
21
|
+
orig_utime(path, times, *args, **kwargs)
|
|
22
|
+
except OSError:
|
|
23
|
+
pass
|
|
24
|
+
os.utime = patched_utime
|
|
25
|
+
|
|
26
|
+
orig_copystat = shutil.copystat
|
|
27
|
+
def patched_copystat(src, dst, *args, **kwargs):
|
|
28
|
+
try:
|
|
29
|
+
orig_copystat(src, dst, *args, **kwargs)
|
|
30
|
+
except OSError:
|
|
31
|
+
pass
|
|
32
|
+
shutil.copystat = patched_copystat
|
|
33
|
+
except Exception:
|
|
34
|
+
pass
|
|
35
|
+
|
|
4
36
|
# Read the contents of README.md
|
|
5
37
|
this_directory = os.path.abspath(os.path.dirname(__file__))
|
|
6
38
|
with open(os.path.join(this_directory, "README.md"), encoding="utf-8") as f:
|
|
@@ -19,7 +51,7 @@ if os.path.exists(requirements_path):
|
|
|
19
51
|
|
|
20
52
|
setup(
|
|
21
53
|
name="gitcast",
|
|
22
|
-
version="1.0.
|
|
54
|
+
version="1.0.6",
|
|
23
55
|
description="git diff → published post. under 60 seconds.",
|
|
24
56
|
long_description=long_description,
|
|
25
57
|
long_description_content_type="text/markdown",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import httpx
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
# Add project root to path so we can import config
|
|
8
|
+
sys.path.append(str(Path(__file__).resolve().parent.parent))
|
|
9
|
+
|
|
10
|
+
from config.settings import (
|
|
11
|
+
GROQ_API_KEY, GEMINI_API_KEY,
|
|
12
|
+
MOONSHOT_API_KEY, CEREBRAS_API_KEY, OPENROUTER_API_KEY
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
async def test_provider(name, url, headers, body, is_gemini=False):
|
|
16
|
+
print(f"Testing {name.upper()}...", end=" ", flush=True)
|
|
17
|
+
async with httpx.AsyncClient(timeout=10) as client:
|
|
18
|
+
try:
|
|
19
|
+
if is_gemini:
|
|
20
|
+
res = await client.post(f"{url}?key={GEMINI_API_KEY}", json=body)
|
|
21
|
+
else:
|
|
22
|
+
res = await client.post(url, headers=headers, json=body)
|
|
23
|
+
|
|
24
|
+
if res.status_code == 200:
|
|
25
|
+
print("✅ WORKING")
|
|
26
|
+
return True
|
|
27
|
+
else:
|
|
28
|
+
print(f"❌ FAILED ({res.status_code})")
|
|
29
|
+
try:
|
|
30
|
+
error_msg = res.json().get('error', {}).get('message', res.text[:100])
|
|
31
|
+
print(f" Reason: {error_msg}")
|
|
32
|
+
except:
|
|
33
|
+
print(f" Reason: {res.text[:100]}")
|
|
34
|
+
return False
|
|
35
|
+
except Exception as e:
|
|
36
|
+
print(f"❌ ERROR: {str(e)}")
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
async def main():
|
|
40
|
+
print("=== Context Engine API Key Connectivity Test ===\n")
|
|
41
|
+
|
|
42
|
+
# 1. Groq
|
|
43
|
+
await test_provider(
|
|
44
|
+
"Groq",
|
|
45
|
+
"https://api.groq.com/openai/v1/chat/completions",
|
|
46
|
+
{"Authorization": f"Bearer {GROQ_API_KEY}"},
|
|
47
|
+
{"model": "llama-3.3-70b-versatile", "messages": [{"role": "user", "content": "hi"}], "max_tokens": 5}
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# 2. OpenRouter
|
|
51
|
+
await test_provider(
|
|
52
|
+
"OpenRouter",
|
|
53
|
+
"https://openrouter.ai/api/v1/chat/completions",
|
|
54
|
+
{"Authorization": f"Bearer {OPENROUTER_API_KEY}"},
|
|
55
|
+
{"model": "qwen/qwen3-coder:free", "messages": [{"role": "user", "content": "hi"}], "max_tokens": 5}
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# 3. Kimi (Moonshot)
|
|
59
|
+
await test_provider(
|
|
60
|
+
"Moonshot (Kimi)",
|
|
61
|
+
"https://api.moonshot.cn/v1/chat/completions",
|
|
62
|
+
{"Authorization": f"Bearer {MOONSHOT_API_KEY}"},
|
|
63
|
+
{"model": "moonshot-v1-8k", "messages": [{"role": "user", "content": "hi"}], "max_tokens": 5}
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# 4. Cerebras
|
|
67
|
+
await test_provider(
|
|
68
|
+
"Cerebras",
|
|
69
|
+
"https://api.cerebras.ai/v1/chat/completions",
|
|
70
|
+
{"Authorization": f"Bearer {CEREBRAS_API_KEY}"},
|
|
71
|
+
{"model": "llama3.3-70b", "messages": [{"role": "user", "content": "hi"}], "max_tokens": 5}
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# 5. Gemini
|
|
75
|
+
await test_provider(
|
|
76
|
+
"Gemini",
|
|
77
|
+
"https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent",
|
|
78
|
+
{},
|
|
79
|
+
{"contents": [{"parts": [{"text": "hi"}]}]},
|
|
80
|
+
is_gemini=True
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
print("\nTest complete.")
|
|
84
|
+
|
|
85
|
+
if __name__ == "__main__":
|
|
86
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import asyncio
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
sys.path.append(str(Path(__file__).resolve().parent.parent))
|
|
7
|
+
|
|
8
|
+
from api.payload import build_payload
|
|
9
|
+
from api.routes import generate_article, ArticleGenerateRequest
|
|
10
|
+
from config.settings import CURRENT_DRAFT
|
|
11
|
+
from fastapi import HTTPException
|
|
12
|
+
|
|
13
|
+
async def test_ocr_text_in_payload():
|
|
14
|
+
print("Testing ocr_text in build_payload...")
|
|
15
|
+
ocr_result = {"text": "Detected Text", "confidence": 0.9}
|
|
16
|
+
capture_result = {
|
|
17
|
+
"screenshot": {"path": "test.png", "timestamp": "123"},
|
|
18
|
+
"git_diff": {"diff": "some diff", "success": True}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
payload = build_payload(
|
|
22
|
+
raw_thought="my thought",
|
|
23
|
+
ocr_result=ocr_result,
|
|
24
|
+
capture_result=capture_result
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
assert "ocr_text" in payload
|
|
28
|
+
assert payload["ocr_text"] == "Detected Text"
|
|
29
|
+
print("✅ build_payload includes ocr_text")
|
|
30
|
+
|
|
31
|
+
async def test_generate_article_robustness():
|
|
32
|
+
print("Testing generate_article robustness with missing ocr_text in draft...")
|
|
33
|
+
original_draft = CURRENT_DRAFT.read_text(encoding="utf-8") if CURRENT_DRAFT.exists() else None
|
|
34
|
+
# Mock CURRENT_DRAFT
|
|
35
|
+
draft_data = {
|
|
36
|
+
"payload": {
|
|
37
|
+
"user_message": "hello",
|
|
38
|
+
# "ocr_text" is missing
|
|
39
|
+
"git_diff": "diff"
|
|
40
|
+
},
|
|
41
|
+
"variations": {},
|
|
42
|
+
"timestamp": "123",
|
|
43
|
+
"status": "ready"
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
with open(CURRENT_DRAFT, "w", encoding="utf-8") as f:
|
|
48
|
+
json.dump(draft_data, f)
|
|
49
|
+
|
|
50
|
+
request = ArticleGenerateRequest(include_codebase=False)
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
# This will still try to call AI, but we want to see if it passes the user_msg construction
|
|
54
|
+
# We can't easily mock the AI call here without more setup,
|
|
55
|
+
# but the KeyError would happen BEFORE the AI call.
|
|
56
|
+
|
|
57
|
+
# To avoid actual AI call, we might just check the logic in routes.py
|
|
58
|
+
# by calling the function and expecting it to fail at AI call but NOT with KeyError.
|
|
59
|
+
await generate_article(request)
|
|
60
|
+
except HTTPException as e:
|
|
61
|
+
if "Article generation failed" in e.detail:
|
|
62
|
+
print("✅ generate_article passed KeyError (failed at AI call as expected)")
|
|
63
|
+
else:
|
|
64
|
+
print(f"❌ generate_article failed with: {e.detail}")
|
|
65
|
+
except KeyError as e:
|
|
66
|
+
print(f"❌ generate_article still has KeyError: {e}")
|
|
67
|
+
except Exception as e:
|
|
68
|
+
print(f"✅ generate_article passed KeyError (failed with: {e})")
|
|
69
|
+
finally:
|
|
70
|
+
if original_draft is None:
|
|
71
|
+
CURRENT_DRAFT.unlink(missing_ok=True)
|
|
72
|
+
else:
|
|
73
|
+
CURRENT_DRAFT.write_text(original_draft, encoding="utf-8")
|
|
74
|
+
|
|
75
|
+
if __name__ == "__main__":
|
|
76
|
+
asyncio.run(test_ocr_text_in_payload())
|
|
77
|
+
asyncio.run(test_generate_article_robustness())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
gitcast-1.0.5/api/ratelimit.py
DELETED