gitcast 1.0.4__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.4/gitcast.egg-info → gitcast-1.0.6}/PKG-INFO +1 -1
- gitcast-1.0.6/api/ratelimit.py +18 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/api/routes.py +24 -42
- {gitcast-1.0.4 → gitcast-1.0.6}/cli/gitcast.py +2 -2
- {gitcast-1.0.4 → gitcast-1.0.6}/config/settings.py +59 -4
- gitcast-1.0.6/docs/pypi-errors.md +68 -0
- {gitcast-1.0.4 → gitcast-1.0.6/gitcast.egg-info}/PKG-INFO +1 -1
- {gitcast-1.0.4 → gitcast-1.0.6}/gitcast.egg-info/SOURCES.txt +1 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/gitcast.egg-info/dependency_links.txt +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/gitcast.egg-info/entry_points.txt +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/gitcast.egg-info/requires.txt +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/gitcast.egg-info/top_level.txt +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/setup.cfg +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/setup.py +33 -1
- {gitcast-1.0.4 → gitcast-1.0.6}/storage/supabase_client.py +14 -4
- gitcast-1.0.4/api/ratelimit.py +0 -9
- {gitcast-1.0.4 → gitcast-1.0.6}/.env.example +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/LICENSE +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/MANIFEST.in +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/README.md +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/ai/__init__.py +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/ai/formatter.py +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/ai/generator.py +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/ai/prompts.py +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/ai/viral_patterns.py +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/api/__init__.py +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/api/analytics.py +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/api/auth.py +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/api/auth_middleware.py +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/api/auth_routes.py +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/api/monitoring.py +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/api/payload.py +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/api/server.py +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/api/validators.py +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/assets/__init__.py +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/assets/favicon-16x16.png +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/assets/favicon-32x32.png +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/assets/favicon-64x64.png +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/assets/favicon.ico +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/assets/icon.png +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/cli/.env.example +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/cli/__init__.py +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/config/__init__.py +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/core/__init__.py +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/core/capture.py +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/core/codebase_reader.py +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/core/framing.py +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/core/hotkey.py +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/core/log_stream.py +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/core/ocr.py +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/core/screenshot_session.py +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/core/security.py +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/core/tray.py +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/docs/api-keys.md +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/docs/article-generator.md +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/docs/capture-flow.md +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/docs/changelog.md +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/docs/contributing.md +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/docs/dashboard.md +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/docs/insights.md +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/docs/introduction.md +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/docs/post-formats.md +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/docs/providers.md +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/docs/quickstart.md +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/docs/sprint-mode.md +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/publisher/__init__.py +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/publisher/clipboard.py +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/publisher/twitter.py +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/pyproject.toml +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/requirements.txt +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/storage/__init__.py +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/storage/cleanup.py +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/storage/engagement.py +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/storage/insights.py +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/storage/key_manager.py +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/storage/logger.py +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/storage/metrics.py +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/storage/sprint.py +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/storage/streak.py +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/storage/tone_memory.py +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/tests/test_keys.py +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/tests/test_ocr_fix.py +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/ui/__init__.py +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/web/__init__.py +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/web/index.html +0 -0
- {gitcast-1.0.4 → gitcast-1.0.6}/web/landing.html +0 -0
|
@@ -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
|
+
|
|
@@ -445,7 +445,8 @@ KEY_GUIDE = {
|
|
|
445
445
|
|
|
446
446
|
|
|
447
447
|
def _env_path():
|
|
448
|
-
|
|
448
|
+
from config.settings import get_active_env_path
|
|
449
|
+
path = get_active_env_path(for_write=True)
|
|
449
450
|
path.touch(exist_ok=True)
|
|
450
451
|
return path
|
|
451
452
|
|
|
@@ -540,8 +541,7 @@ def _update_settings_for_user(user_id: str, values: dict) -> dict:
|
|
|
540
541
|
|
|
541
542
|
|
|
542
543
|
@router.get("/keys/status")
|
|
543
|
-
def get_ai_keys_status():
|
|
544
|
-
user_id = LOCAL_USER_ID
|
|
544
|
+
def get_ai_keys_status(user_id: str = Depends(get_current_user)):
|
|
545
545
|
if _is_local_user(user_id):
|
|
546
546
|
env_values = dotenv_values(_env_path())
|
|
547
547
|
return {
|
|
@@ -667,9 +667,8 @@ def add_to_waitlist(request: WaitlistRequest):
|
|
|
667
667
|
|
|
668
668
|
@router.patch("/settings")
|
|
669
669
|
@router.post("/settings")
|
|
670
|
-
def update_settings(update: SettingsUpdate):
|
|
670
|
+
def update_settings(update: SettingsUpdate, user_id: str = Depends(get_current_user)):
|
|
671
671
|
"""Updates user project settings."""
|
|
672
|
-
user_id = LOCAL_USER_ID
|
|
673
672
|
values = update.dict(exclude_unset=True)
|
|
674
673
|
if "project_narrative" in values and values["project_narrative"] is not None:
|
|
675
674
|
values["project_narrative"] = sanitize_text(values["project_narrative"])
|
|
@@ -679,9 +678,8 @@ def update_settings(update: SettingsUpdate):
|
|
|
679
678
|
|
|
680
679
|
@router.post("/publish")
|
|
681
680
|
@limiter.limit("10/minute")
|
|
682
|
-
async def publish(request: Request, body: PublishRequest):
|
|
681
|
+
async def publish(request: Request, body: PublishRequest, user_id: str = Depends(get_current_user)):
|
|
683
682
|
"""Publishes a post to X (Twitter)."""
|
|
684
|
-
user_id = LOCAL_USER_ID
|
|
685
683
|
from publisher.twitter import publish_post
|
|
686
684
|
try:
|
|
687
685
|
post_text = sanitize_text(body.post_text)
|
|
@@ -782,8 +780,7 @@ async def get_framed_screenshot(filename: str):
|
|
|
782
780
|
|
|
783
781
|
@router.post("/generate", response_model=GenerateResponse)
|
|
784
782
|
@limiter.limit("10/minute")
|
|
785
|
-
async def generate(request: Request, body: GenerateRequest):
|
|
786
|
-
user_id = LOCAL_USER_ID
|
|
783
|
+
async def generate(request: Request, body: GenerateRequest, user_id: str = Depends(get_current_user)):
|
|
787
784
|
payload = body.dict()
|
|
788
785
|
for key, value in list(payload.items()):
|
|
789
786
|
if isinstance(value, str):
|
|
@@ -865,9 +862,8 @@ def get_current_draft():
|
|
|
865
862
|
|
|
866
863
|
@router.post("/chat")
|
|
867
864
|
@limiter.limit("20/minute")
|
|
868
|
-
async def chat_refine(request: Request, body: ChatRequest):
|
|
865
|
+
async def chat_refine(request: Request, body: ChatRequest, user_id: str = Depends(get_current_user)):
|
|
869
866
|
"""Refines a post variation using AI chat."""
|
|
870
|
-
user_id = LOCAL_USER_ID
|
|
871
867
|
if not CURRENT_DRAFT.exists():
|
|
872
868
|
raise HTTPException(status_code=400, detail="No active draft to refine.")
|
|
873
869
|
|
|
@@ -925,9 +921,8 @@ async def chat_refine(request: Request, body: ChatRequest):
|
|
|
925
921
|
|
|
926
922
|
|
|
927
923
|
@router.post("/cli/trigger")
|
|
928
|
-
async def cli_trigger(request: CliTriggerRequest):
|
|
924
|
+
async def cli_trigger(request: CliTriggerRequest, user_id: str = Depends(get_current_user)):
|
|
929
925
|
"""Triggers a capture workflow with a specific thought from the CLI."""
|
|
930
|
-
user_id = LOCAL_USER_ID
|
|
931
926
|
from core.capture import run_capture
|
|
932
927
|
from core.ocr import run_ocr
|
|
933
928
|
from api.payload import build_payload
|
|
@@ -963,9 +958,8 @@ async def cli_trigger(request: CliTriggerRequest):
|
|
|
963
958
|
|
|
964
959
|
|
|
965
960
|
@router.post("/capture/trigger")
|
|
966
|
-
async def ui_trigger_capture(request: CaptureTriggerRequest):
|
|
961
|
+
async def ui_trigger_capture(request: CaptureTriggerRequest, user_id: str = Depends(get_current_user)):
|
|
967
962
|
"""Triggers a capture workflow from the UI."""
|
|
968
|
-
user_id = LOCAL_USER_ID
|
|
969
963
|
from core.capture import run_capture
|
|
970
964
|
from core.ocr import run_ocr
|
|
971
965
|
from api.payload import build_payload
|
|
@@ -1044,9 +1038,8 @@ def list_screenshots():
|
|
|
1044
1038
|
|
|
1045
1039
|
|
|
1046
1040
|
@router.post("/screenshot/recommend")
|
|
1047
|
-
async def recommend_screenshot(request: RecommendRequest):
|
|
1041
|
+
async def recommend_screenshot(request: RecommendRequest, user_id: str = Depends(get_current_user)):
|
|
1048
1042
|
"""Uses AI to recommend where to post a specific screenshot."""
|
|
1049
|
-
user_id = LOCAL_USER_ID
|
|
1050
1043
|
from config.settings import STORAGE_DIR
|
|
1051
1044
|
path = STORAGE_DIR / "screenshots" / sanitize_text(request.filename)
|
|
1052
1045
|
if not path.exists():
|
|
@@ -1136,16 +1129,14 @@ def get_formats():
|
|
|
1136
1129
|
|
|
1137
1130
|
|
|
1138
1131
|
@router.get("/settings/plan")
|
|
1139
|
-
def get_plan():
|
|
1140
|
-
user_id = LOCAL_USER_ID
|
|
1132
|
+
def get_plan(user_id: str = Depends(get_current_user)):
|
|
1141
1133
|
settings = _settings_for_user(user_id)
|
|
1142
1134
|
preferred = settings.get("preferred_providers") or {}
|
|
1143
1135
|
return {"plan": preferred.get("twitter_plan", get_twitter_plan())}
|
|
1144
1136
|
|
|
1145
1137
|
|
|
1146
1138
|
@router.put("/settings/plan")
|
|
1147
|
-
def update_plan(update: PlanUpdate):
|
|
1148
|
-
user_id = LOCAL_USER_ID
|
|
1139
|
+
def update_plan(update: PlanUpdate, user_id: str = Depends(get_current_user)):
|
|
1149
1140
|
plan = sanitize_text(update.plan).lower()
|
|
1150
1141
|
settings = _settings_for_user(user_id)
|
|
1151
1142
|
preferred = settings.get("preferred_providers") or {}
|
|
@@ -1155,14 +1146,12 @@ def update_plan(update: PlanUpdate):
|
|
|
1155
1146
|
|
|
1156
1147
|
|
|
1157
1148
|
@router.get("/settings")
|
|
1158
|
-
def get_all_settings():
|
|
1159
|
-
user_id = LOCAL_USER_ID
|
|
1149
|
+
def get_all_settings(user_id: str = Depends(get_current_user)):
|
|
1160
1150
|
return _settings_for_user(user_id)
|
|
1161
1151
|
|
|
1162
1152
|
|
|
1163
1153
|
@router.post("/settings/sprint/toggle")
|
|
1164
|
-
def sprint_toggle():
|
|
1165
|
-
user_id = LOCAL_USER_ID
|
|
1154
|
+
def sprint_toggle(user_id: str = Depends(get_current_user)):
|
|
1166
1155
|
settings = _settings_for_user(user_id)
|
|
1167
1156
|
new_state = not bool(settings.get("sprint_mode"))
|
|
1168
1157
|
_update_settings_for_user(user_id, {"sprint_mode": new_state})
|
|
@@ -1170,15 +1159,13 @@ def sprint_toggle():
|
|
|
1170
1159
|
|
|
1171
1160
|
|
|
1172
1161
|
@router.get("/settings/keys")
|
|
1173
|
-
def get_keys_status():
|
|
1174
|
-
user_id
|
|
1175
|
-
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)
|
|
1176
1164
|
|
|
1177
1165
|
|
|
1178
1166
|
@router.get("/diagnose")
|
|
1179
|
-
async def diagnose_connectivity():
|
|
1167
|
+
async def diagnose_connectivity(user_id: str = Depends(get_current_user)):
|
|
1180
1168
|
"""Diagnoses network and authentication status of all configured AI providers."""
|
|
1181
|
-
user_id = LOCAL_USER_ID
|
|
1182
1169
|
import ai.generator
|
|
1183
1170
|
import httpx
|
|
1184
1171
|
|
|
@@ -1247,9 +1234,8 @@ async def diagnose_connectivity():
|
|
|
1247
1234
|
|
|
1248
1235
|
@router.post("/article/generate")
|
|
1249
1236
|
@limiter.limit("5/minute")
|
|
1250
|
-
async def generate_article(request: Request, body: ArticleGenerateRequest):
|
|
1237
|
+
async def generate_article(request: Request, body: ArticleGenerateRequest, user_id: str = Depends(get_current_user)):
|
|
1251
1238
|
"""Generates a full technical article from sprint context."""
|
|
1252
|
-
user_id = LOCAL_USER_ID
|
|
1253
1239
|
if not CURRENT_DRAFT.exists():
|
|
1254
1240
|
raise HTTPException(status_code=400, detail="No active draft to generate article from.")
|
|
1255
1241
|
|
|
@@ -1285,9 +1271,8 @@ async def generate_article(request: Request, body: ArticleGenerateRequest):
|
|
|
1285
1271
|
|
|
1286
1272
|
|
|
1287
1273
|
@router.post("/article/refine")
|
|
1288
|
-
async def refine_article(request: ArticleRefineRequest):
|
|
1274
|
+
async def refine_article(request: ArticleRefineRequest, user_id: str = Depends(get_current_user)):
|
|
1289
1275
|
"""Refines an article draft based on user instructions."""
|
|
1290
|
-
user_id = LOCAL_USER_ID
|
|
1291
1276
|
current_article = sanitize_text(request.current_article)
|
|
1292
1277
|
instruction = sanitize_text(request.instruction)
|
|
1293
1278
|
injection = check_prompt_injection(instruction)
|
|
@@ -1315,8 +1300,7 @@ def thread_split(request: ThreadSplitRequest):
|
|
|
1315
1300
|
|
|
1316
1301
|
|
|
1317
1302
|
@router.post("/posts/verify")
|
|
1318
|
-
async def verify_logged_post(request: PostVerifyRequest):
|
|
1319
|
-
user_id = LOCAL_USER_ID
|
|
1303
|
+
async def verify_logged_post(request: PostVerifyRequest, user_id: str = Depends(get_current_user)):
|
|
1320
1304
|
post_id = sanitize_text(request.post_id)
|
|
1321
1305
|
post_url = sanitize_text(request.post_url or "")
|
|
1322
1306
|
result = await safe_update_post(post_id, {
|
|
@@ -1332,8 +1316,7 @@ async def verify_logged_post(request: PostVerifyRequest):
|
|
|
1332
1316
|
|
|
1333
1317
|
|
|
1334
1318
|
@router.post("/posts/decline")
|
|
1335
|
-
async def decline_logged_post(request: PostDeclineRequest):
|
|
1336
|
-
user_id = LOCAL_USER_ID
|
|
1319
|
+
async def decline_logged_post(request: PostDeclineRequest, user_id: str = Depends(get_current_user)):
|
|
1337
1320
|
result = await safe_update_post(sanitize_text(request.post_id), {"declined": True})
|
|
1338
1321
|
if not result:
|
|
1339
1322
|
raise HTTPException(status_code=404, detail="post not found")
|
|
@@ -1346,8 +1329,7 @@ async def unverified_posts():
|
|
|
1346
1329
|
|
|
1347
1330
|
|
|
1348
1331
|
@router.post("/metrics/save")
|
|
1349
|
-
async def save_post_metrics(request: MetricsSaveRequest):
|
|
1350
|
-
user_id = LOCAL_USER_ID
|
|
1332
|
+
async def save_post_metrics(request: MetricsSaveRequest, user_id: str = Depends(get_current_user)):
|
|
1351
1333
|
values = {
|
|
1352
1334
|
"impressions": request.impressions,
|
|
1353
1335
|
"likes": request.likes,
|
|
@@ -1422,9 +1404,9 @@ async def get_post_metrics(post_id: str):
|
|
|
1422
1404
|
|
|
1423
1405
|
|
|
1424
1406
|
@router.get("/insights")
|
|
1425
|
-
async def get_insights():
|
|
1407
|
+
async def get_insights(user_id: str = Depends(get_current_user)):
|
|
1426
1408
|
now = time.time()
|
|
1427
|
-
cache_key = f"data:{
|
|
1409
|
+
cache_key = f"data:{user_id}"
|
|
1428
1410
|
if _INSIGHTS_CACHE.get(cache_key) is not None and now - _INSIGHTS_CACHE["timestamp"] < 3600:
|
|
1429
1411
|
return _INSIGHTS_CACHE[cache_key]
|
|
1430
1412
|
|
|
@@ -7,8 +7,8 @@ import webbrowser
|
|
|
7
7
|
|
|
8
8
|
def main():
|
|
9
9
|
if "--setup" in sys.argv:
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
from config.settings import get_active_env_path
|
|
11
|
+
env_path = str(get_active_env_path(for_write=True))
|
|
12
12
|
example_path = os.path.join(
|
|
13
13
|
os.path.dirname(__file__), '..', '.env.example')
|
|
14
14
|
if not os.path.exists(example_path):
|
|
@@ -5,11 +5,55 @@ from pathlib import Path
|
|
|
5
5
|
from dotenv import load_dotenv
|
|
6
6
|
|
|
7
7
|
|
|
8
|
+
import sys
|
|
9
|
+
|
|
8
10
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
|
9
11
|
CONFIG_DIR = BASE_DIR / "config"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
|
|
13
|
+
def get_env_paths() -> list:
|
|
14
|
+
paths = []
|
|
15
|
+
# 1. Custom env path
|
|
16
|
+
custom_path = os.getenv("GITCAST_ENV_PATH")
|
|
17
|
+
if custom_path:
|
|
18
|
+
paths.append(Path(custom_path))
|
|
19
|
+
# 2. Current working directory
|
|
20
|
+
paths.append(Path(os.getcwd()) / ".env")
|
|
21
|
+
# 3. Virtual environment parent
|
|
22
|
+
if hasattr(sys, "prefix") and sys.prefix:
|
|
23
|
+
paths.append(Path(sys.prefix).parent / ".env")
|
|
24
|
+
# 4. User home directories
|
|
25
|
+
paths.append(Path.home() / ".gitcast" / ".env")
|
|
26
|
+
paths.append(Path.home() / ".gitcast.env")
|
|
27
|
+
# 5. Base directory
|
|
28
|
+
paths.append(BASE_DIR / ".env")
|
|
29
|
+
return [p for p in paths if p]
|
|
30
|
+
|
|
31
|
+
def load_all_dotenvs(override: bool = False) -> None:
|
|
32
|
+
paths = get_env_paths()
|
|
33
|
+
if override:
|
|
34
|
+
for p in reversed(paths):
|
|
35
|
+
if p.exists() and p.is_file():
|
|
36
|
+
load_dotenv(p, override=True)
|
|
37
|
+
else:
|
|
38
|
+
for p in paths:
|
|
39
|
+
if p.exists() and p.is_file():
|
|
40
|
+
load_dotenv(p, override=False)
|
|
41
|
+
|
|
42
|
+
def get_active_env_path(for_write: bool = False) -> Path:
|
|
43
|
+
paths = get_env_paths()
|
|
44
|
+
for p in paths:
|
|
45
|
+
if p.exists() and p.is_file():
|
|
46
|
+
return p
|
|
47
|
+
if for_write:
|
|
48
|
+
home_env = Path.home() / ".gitcast" / ".env"
|
|
49
|
+
try:
|
|
50
|
+
home_env.parent.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
return home_env
|
|
52
|
+
except Exception:
|
|
53
|
+
pass
|
|
54
|
+
return BASE_DIR / ".env"
|
|
55
|
+
|
|
56
|
+
load_all_dotenvs()
|
|
13
57
|
STORAGE_DIR = BASE_DIR / "storage" / "data"
|
|
14
58
|
POSTHOG_API_KEY = os.getenv("POSTHOG_API_KEY", "")
|
|
15
59
|
SENTRY_DSN = os.getenv("SENTRY_DSN", "")
|
|
@@ -65,8 +109,13 @@ def reload_api_keys() -> None:
|
|
|
65
109
|
global GEMINI_MODEL
|
|
66
110
|
global CEREBRAS_MODEL
|
|
67
111
|
global OPENROUTER_MODEL
|
|
112
|
+
global SUPABASE_URL
|
|
113
|
+
global SUPABASE_SERVICE_KEY
|
|
114
|
+
global SUPABASE_ANON_KEY
|
|
115
|
+
global SUPABASE_JWT_SECRET
|
|
116
|
+
global SUPABASE_JWT_AUDIENCE
|
|
68
117
|
|
|
69
|
-
|
|
118
|
+
load_all_dotenvs(override=True)
|
|
70
119
|
GROQ_API_KEY = os.getenv("GROQ_API_KEY", "")
|
|
71
120
|
MOONSHOT_API_KEY = os.getenv("MOONSHOT_API_KEY", "")
|
|
72
121
|
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY", "")
|
|
@@ -77,6 +126,12 @@ def reload_api_keys() -> None:
|
|
|
77
126
|
GEMINI_MODEL = os.getenv("GEMINI_MODEL", "gemini-2.0-flash")
|
|
78
127
|
CEREBRAS_MODEL = os.getenv("CEREBRAS_MODEL", "gpt-oss-120b")
|
|
79
128
|
OPENROUTER_MODEL = os.getenv("OPENROUTER_MODEL", "qwen/qwen3-coder:free")
|
|
129
|
+
|
|
130
|
+
SUPABASE_URL = os.getenv("SUPABASE_URL", "")
|
|
131
|
+
SUPABASE_SERVICE_KEY = os.getenv("SUPABASE_SERVICE_KEY", "")
|
|
132
|
+
SUPABASE_ANON_KEY = os.getenv("SUPABASE_ANON_KEY", "")
|
|
133
|
+
SUPABASE_JWT_SECRET = os.getenv("SUPABASE_JWT_SECRET", "")
|
|
134
|
+
SUPABASE_JWT_AUDIENCE = os.getenv("SUPABASE_JWT_AUDIENCE", "authenticated")
|
|
80
135
|
|
|
81
136
|
|
|
82
137
|
reload_api_keys()
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -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",
|
|
@@ -1,22 +1,32 @@
|
|
|
1
1
|
import threading
|
|
2
2
|
from supabase import Client, create_client, ClientOptions
|
|
3
|
-
|
|
3
|
+
import config.settings
|
|
4
4
|
|
|
5
5
|
_thread_local = threading.local()
|
|
6
6
|
|
|
7
7
|
def get_client() -> Client:
|
|
8
|
-
|
|
8
|
+
url = getattr(config.settings, "SUPABASE_URL", "")
|
|
9
|
+
key = getattr(config.settings, "SUPABASE_SERVICE_KEY", "")
|
|
10
|
+
if not url or not key:
|
|
9
11
|
raise RuntimeError("SUPABASE_URL and SUPABASE_SERVICE_KEY must be configured")
|
|
10
12
|
|
|
13
|
+
# Check if config changed and recreate if needed
|
|
14
|
+
if hasattr(_thread_local, "client"):
|
|
15
|
+
if getattr(_thread_local, "client_url", None) != url or getattr(_thread_local, "client_key", None) != key:
|
|
16
|
+
if hasattr(_thread_local, "client"):
|
|
17
|
+
delattr(_thread_local, "client")
|
|
18
|
+
|
|
11
19
|
if not hasattr(_thread_local, "client"):
|
|
12
20
|
_thread_local.client = create_client(
|
|
13
|
-
|
|
14
|
-
|
|
21
|
+
url,
|
|
22
|
+
key,
|
|
15
23
|
options=ClientOptions(
|
|
16
24
|
postgrest_client_timeout=10,
|
|
17
25
|
storage_client_timeout=10
|
|
18
26
|
)
|
|
19
27
|
)
|
|
28
|
+
_thread_local.client_url = url
|
|
29
|
+
_thread_local.client_key = key
|
|
20
30
|
return _thread_local.client
|
|
21
31
|
|
|
22
32
|
|
gitcast-1.0.4/api/ratelimit.py
DELETED
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|