gitcast 1.0.0__tar.gz → 1.0.1__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.0 → gitcast-1.0.1}/MANIFEST.in +1 -0
- {gitcast-1.0.0/gitcast.egg-info → gitcast-1.0.1}/PKG-INFO +1 -1
- gitcast-1.0.1/assets/__init__.py +1 -0
- gitcast-1.0.1/cli/.env.example +26 -0
- gitcast-1.0.1/cli/gitcast.py +106 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/core/capture.py +7 -3
- {gitcast-1.0.0 → gitcast-1.0.1}/core/screenshot_session.py +6 -3
- {gitcast-1.0.0 → gitcast-1.0.1/gitcast.egg-info}/PKG-INFO +1 -1
- {gitcast-1.0.0 → gitcast-1.0.1}/gitcast.egg-info/SOURCES.txt +3 -11
- {gitcast-1.0.0 → gitcast-1.0.1}/gitcast.egg-info/top_level.txt +2 -1
- {gitcast-1.0.0 → gitcast-1.0.1}/setup.py +1 -1
- gitcast-1.0.1/web/__init__.py +1 -0
- gitcast-1.0.0/cli/gitcast.py +0 -56
- gitcast-1.0.0/storage/cleanup.py +0 -60
- gitcast-1.0.0/storage/engagement.py +0 -114
- gitcast-1.0.0/storage/insights.py +0 -203
- gitcast-1.0.0/storage/key_manager.py +0 -45
- gitcast-1.0.0/storage/logger.py +0 -208
- gitcast-1.0.0/storage/metrics.py +0 -119
- gitcast-1.0.0/storage/sprint.py +0 -40
- gitcast-1.0.0/storage/streak.py +0 -0
- gitcast-1.0.0/storage/supabase_client.py +0 -25
- gitcast-1.0.0/storage/tone_memory.py +0 -139
- gitcast-1.0.0/ui/__init__.py +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/.env.example +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/LICENSE +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/README.md +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/ai/__init__.py +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/ai/formatter.py +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/ai/generator.py +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/ai/prompts.py +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/ai/viral_patterns.py +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/api/__init__.py +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/api/analytics.py +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/api/auth.py +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/api/auth_middleware.py +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/api/auth_routes.py +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/api/monitoring.py +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/api/payload.py +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/api/ratelimit.py +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/api/routes.py +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/api/server.py +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/api/validators.py +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/assets/favicon-16x16.png +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/assets/favicon-32x32.png +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/assets/favicon-64x64.png +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/assets/favicon.ico +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/assets/icon.png +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/cli/__init__.py +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/config/__init__.py +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/config/settings.py +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/core/__init__.py +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/core/codebase_reader.py +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/core/framing.py +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/core/hotkey.py +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/core/log_stream.py +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/core/ocr.py +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/core/security.py +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/core/tray.py +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/docs/api-keys.md +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/docs/article-generator.md +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/docs/capture-flow.md +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/docs/changelog.md +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/docs/contributing.md +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/docs/dashboard.md +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/docs/insights.md +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/docs/introduction.md +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/docs/post-formats.md +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/docs/providers.md +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/docs/quickstart.md +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/docs/sprint-mode.md +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/gitcast.egg-info/dependency_links.txt +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/gitcast.egg-info/entry_points.txt +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/gitcast.egg-info/requires.txt +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/publisher/__init__.py +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/publisher/clipboard.py +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/publisher/twitter.py +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/pyproject.toml +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/requirements.txt +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/setup.cfg +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/tests/test_keys.py +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/tests/test_ocr_fix.py +0 -0
- {gitcast-1.0.0/storage → gitcast-1.0.1/ui}/__init__.py +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/web/index.html +0 -0
- {gitcast-1.0.0 → gitcast-1.0.1}/web/landing.html +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Make assets directory a Python package so it gets packaged and installed to site-packages.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Groq — primary AI inference
|
|
2
|
+
GROQ_API_KEY=your_groq_api_key_here
|
|
3
|
+
|
|
4
|
+
# DeepSeek & Moonshot — specialized/free-tier fallbacks
|
|
5
|
+
DEEPSEEK_API_KEY=your_deepseek_api_key_here
|
|
6
|
+
MOONSHOT_API_KEY=your_moonshot_api_key_here
|
|
7
|
+
CEREBRAS_API_KEY=your_cerebras_api_key_here
|
|
8
|
+
OPENROUTER_API_KEY=your_openrouter_api_key_here
|
|
9
|
+
|
|
10
|
+
# Gemini — vision fallback (when OCR confidence is too low)
|
|
11
|
+
GEMINI_API_KEY=your_gemini_api_key_here
|
|
12
|
+
|
|
13
|
+
# Optional model overrides
|
|
14
|
+
GROQ_MODEL=llama-3.3-70b-versatile
|
|
15
|
+
DEEPSEEK_MODEL=deepseek-chat
|
|
16
|
+
MOONSHOT_MODEL=moonshot-v1-8k
|
|
17
|
+
GEMINI_MODEL=gemini-2.0-flash
|
|
18
|
+
CEREBRAS_MODEL=llama-3.3-70b
|
|
19
|
+
OPENROUTER_MODEL=meta-llama/llama-3.3-70b-instruct:free
|
|
20
|
+
|
|
21
|
+
# X (Twitter) API v2 — all five are required for posting
|
|
22
|
+
TWITTER_API_KEY=your_twitter_api_key_here
|
|
23
|
+
TWITTER_API_SECRET=your_twitter_api_secret_here
|
|
24
|
+
TWITTER_ACCESS_TOKEN=your_twitter_access_token_here
|
|
25
|
+
TWITTER_ACCESS_SECRET=your_twitter_access_secret_here
|
|
26
|
+
TWITTER_BEARER_TOKEN=your_twitter_bearer_token_here
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
import httpx
|
|
6
|
+
import webbrowser
|
|
7
|
+
|
|
8
|
+
def main():
|
|
9
|
+
if "--setup" in sys.argv:
|
|
10
|
+
env_path = os.path.join(
|
|
11
|
+
os.path.dirname(__file__), '..', '.env')
|
|
12
|
+
example_path = os.path.join(
|
|
13
|
+
os.path.dirname(__file__), '..', '.env.example')
|
|
14
|
+
if not os.path.exists(example_path):
|
|
15
|
+
example_path = os.path.join(
|
|
16
|
+
os.path.dirname(__file__), '.env.example')
|
|
17
|
+
|
|
18
|
+
if not os.path.exists(env_path):
|
|
19
|
+
if os.path.exists(example_path):
|
|
20
|
+
shutil.copy(example_path, env_path)
|
|
21
|
+
else:
|
|
22
|
+
# If neither is found, create an empty or minimal env file
|
|
23
|
+
with open(env_path, 'w') as f:
|
|
24
|
+
f.write("# Gitcast Environment Variables\nGROQ_API_KEY=\nGEMINI_API_KEY=\n")
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
if sys.platform == "win32":
|
|
28
|
+
subprocess.run(["notepad", env_path])
|
|
29
|
+
elif sys.platform == "darwin":
|
|
30
|
+
subprocess.run(["open", "-t", env_path])
|
|
31
|
+
else:
|
|
32
|
+
try:
|
|
33
|
+
subprocess.run(["xdg-open", env_path])
|
|
34
|
+
except Exception:
|
|
35
|
+
print(f"[Gitcast] Created/configured .env at: {env_path}")
|
|
36
|
+
print("Please open it in an editor to add your API keys.")
|
|
37
|
+
except Exception as e:
|
|
38
|
+
print(f"[Gitcast] Created/configured .env at: {env_path}")
|
|
39
|
+
print(f"Could not automatically open editor: {e}")
|
|
40
|
+
sys.exit(0)
|
|
41
|
+
|
|
42
|
+
if len(sys.argv) < 2:
|
|
43
|
+
# start FastAPI server in background thread
|
|
44
|
+
import threading
|
|
45
|
+
import webbrowser
|
|
46
|
+
import time
|
|
47
|
+
import urllib.request
|
|
48
|
+
|
|
49
|
+
def start_server():
|
|
50
|
+
from api.server import start_server as run
|
|
51
|
+
run()
|
|
52
|
+
|
|
53
|
+
server_thread = threading.Thread(
|
|
54
|
+
target=start_server, daemon=True)
|
|
55
|
+
server_thread.start()
|
|
56
|
+
|
|
57
|
+
# wait for server to be ready (max 5 seconds)
|
|
58
|
+
for _ in range(10):
|
|
59
|
+
try:
|
|
60
|
+
urllib.request.urlopen(
|
|
61
|
+
"http://127.0.0.1:8000/health",
|
|
62
|
+
timeout=1)
|
|
63
|
+
break
|
|
64
|
+
except Exception:
|
|
65
|
+
time.sleep(0.5)
|
|
66
|
+
|
|
67
|
+
# open browser
|
|
68
|
+
webbrowser.open("http://127.0.0.1:8000")
|
|
69
|
+
|
|
70
|
+
# start tray and hotkey
|
|
71
|
+
from core.tray import run_tray
|
|
72
|
+
from main import on_trigger
|
|
73
|
+
run_tray(trigger_callback=on_trigger)
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
command = sys.argv[1]
|
|
77
|
+
|
|
78
|
+
if command == "capture":
|
|
79
|
+
from core.screenshot_session import ScreenshotSession
|
|
80
|
+
session = ScreenshotSession()
|
|
81
|
+
session.run()
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
thought = " ".join(sys.argv[1:])
|
|
85
|
+
print(f"[Gitcast] Initializing capture with thought: '{thought}'")
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
# Call the internal trigger endpoint
|
|
89
|
+
response = httpx.post(
|
|
90
|
+
"http://127.0.0.1:8000/api/cli/trigger",
|
|
91
|
+
json={"thought": thought},
|
|
92
|
+
timeout=30
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
if response.status_code == 200:
|
|
96
|
+
print("[Gitcast] Context captured. Opening Draft Room...")
|
|
97
|
+
webbrowser.open("http://127.0.0.1:8000")
|
|
98
|
+
else:
|
|
99
|
+
print(f"[Gitcast Error] Failed to trigger: {response.text}")
|
|
100
|
+
|
|
101
|
+
except Exception as e:
|
|
102
|
+
print(f"[Gitcast Error] Connection failed: {e}")
|
|
103
|
+
print("Is Gitcast running? (python -m gitcast or python main.py)")
|
|
104
|
+
|
|
105
|
+
if __name__ == "__main__":
|
|
106
|
+
main()
|
|
@@ -7,6 +7,8 @@ from config.settings import STORAGE_DIR
|
|
|
7
7
|
from core.log_stream import stream_log
|
|
8
8
|
from api.analytics import track
|
|
9
9
|
BASE_DIR = STORAGE_DIR.parent.parent
|
|
10
|
+
screenshot_dir = STORAGE_DIR / "screenshots"
|
|
11
|
+
screenshot_dir.mkdir(parents=True, exist_ok=True)
|
|
10
12
|
from .framing import add_programming_frame
|
|
11
13
|
|
|
12
14
|
|
|
@@ -218,11 +220,13 @@ def run_capture(delay: float = 5.0) -> dict:
|
|
|
218
220
|
# Apply programming frame if capture succeeded
|
|
219
221
|
if screenshot.get("success") and screenshot.get("path"):
|
|
220
222
|
try:
|
|
221
|
-
|
|
222
|
-
|
|
223
|
+
abs_path = str(screenshot_dir / Path(screenshot["path"]).name)
|
|
224
|
+
framed_path = add_programming_frame(abs_path)
|
|
225
|
+
rel_framed_path = str(Path(framed_path).relative_to(BASE_DIR)).replace("\\", "/")
|
|
226
|
+
screenshot["framed_path"] = rel_framed_path
|
|
223
227
|
# By default, use the framed path for downstream preview/publishing
|
|
224
228
|
screenshot["raw_path"] = screenshot["path"]
|
|
225
|
-
screenshot["path"] =
|
|
229
|
+
screenshot["path"] = rel_framed_path
|
|
226
230
|
except Exception as e:
|
|
227
231
|
stream_log("Capture", "WARN", f"framing failed: {e}")
|
|
228
232
|
screenshot["framed_path"] = ""
|
|
@@ -17,6 +17,8 @@ from core.ocr import run_ocr
|
|
|
17
17
|
from core.security import scan_for_secrets, delete_capture
|
|
18
18
|
from config.settings import STORAGE_DIR, CURRENT_DRAFT, get_project_narrative
|
|
19
19
|
|
|
20
|
+
screenshot_dir = STORAGE_DIR / "screenshots"
|
|
21
|
+
|
|
20
22
|
console = Console()
|
|
21
23
|
|
|
22
24
|
class ScreenshotSession:
|
|
@@ -127,12 +129,13 @@ class ScreenshotSession:
|
|
|
127
129
|
return
|
|
128
130
|
|
|
129
131
|
# OCR and Security (Silent progress)
|
|
130
|
-
|
|
132
|
+
abs_shot_path = str(screenshot_dir / Path(shot["path"]).name)
|
|
133
|
+
ocr = run_ocr(abs_shot_path)
|
|
131
134
|
security = scan_for_secrets(ocr["text"] or ocr["raw_text"])
|
|
132
135
|
|
|
133
136
|
if not security["clean"]:
|
|
134
137
|
console.print(f"\n[red]⚠️ Shot {len(self.screenshots)+1} blocked (sensitive content).[/red]")
|
|
135
|
-
delete_capture(
|
|
138
|
+
delete_capture(abs_shot_path)
|
|
136
139
|
return
|
|
137
140
|
|
|
138
141
|
# Add to list with default purpose
|
|
@@ -212,7 +215,7 @@ class ScreenshotSession:
|
|
|
212
215
|
if keep:
|
|
213
216
|
final_shots.append(s)
|
|
214
217
|
else:
|
|
215
|
-
delete_capture(s["path"])
|
|
218
|
+
delete_capture(str(screenshot_dir / Path(s["path"]).name))
|
|
216
219
|
|
|
217
220
|
self.screenshots = final_shots
|
|
218
221
|
|
|
@@ -21,11 +21,13 @@ api/ratelimit.py
|
|
|
21
21
|
api/routes.py
|
|
22
22
|
api/server.py
|
|
23
23
|
api/validators.py
|
|
24
|
+
assets/__init__.py
|
|
24
25
|
assets/favicon-16x16.png
|
|
25
26
|
assets/favicon-32x32.png
|
|
26
27
|
assets/favicon-64x64.png
|
|
27
28
|
assets/favicon.ico
|
|
28
29
|
assets/icon.png
|
|
30
|
+
cli/.env.example
|
|
29
31
|
cli/__init__.py
|
|
30
32
|
cli/gitcast.py
|
|
31
33
|
config/__init__.py
|
|
@@ -61,19 +63,9 @@ gitcast.egg-info/top_level.txt
|
|
|
61
63
|
publisher/__init__.py
|
|
62
64
|
publisher/clipboard.py
|
|
63
65
|
publisher/twitter.py
|
|
64
|
-
storage/__init__.py
|
|
65
|
-
storage/cleanup.py
|
|
66
|
-
storage/engagement.py
|
|
67
|
-
storage/insights.py
|
|
68
|
-
storage/key_manager.py
|
|
69
|
-
storage/logger.py
|
|
70
|
-
storage/metrics.py
|
|
71
|
-
storage/sprint.py
|
|
72
|
-
storage/streak.py
|
|
73
|
-
storage/supabase_client.py
|
|
74
|
-
storage/tone_memory.py
|
|
75
66
|
tests/test_keys.py
|
|
76
67
|
tests/test_ocr_fix.py
|
|
77
68
|
ui/__init__.py
|
|
69
|
+
web/__init__.py
|
|
78
70
|
web/index.html
|
|
79
71
|
web/landing.html
|
|
@@ -19,7 +19,7 @@ if os.path.exists(requirements_path):
|
|
|
19
19
|
|
|
20
20
|
setup(
|
|
21
21
|
name="gitcast",
|
|
22
|
-
version="1.0.
|
|
22
|
+
version="1.0.1",
|
|
23
23
|
description="git diff → published post. under 60 seconds.",
|
|
24
24
|
long_description=long_description,
|
|
25
25
|
long_description_content_type="text/markdown",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Make web directory a Python package so it gets packaged and installed to site-packages.
|
gitcast-1.0.0/cli/gitcast.py
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import sys
|
|
2
|
-
import os
|
|
3
|
-
import shutil
|
|
4
|
-
import subprocess
|
|
5
|
-
import httpx
|
|
6
|
-
import webbrowser
|
|
7
|
-
|
|
8
|
-
def main():
|
|
9
|
-
if "--setup" in sys.argv:
|
|
10
|
-
env_path = os.path.join(
|
|
11
|
-
os.path.dirname(__file__), '..', '.env')
|
|
12
|
-
example_path = os.path.join(
|
|
13
|
-
os.path.dirname(__file__), '..', '.env.example')
|
|
14
|
-
if not os.path.exists(env_path):
|
|
15
|
-
shutil.copy(example_path, env_path)
|
|
16
|
-
subprocess.run(["notepad", env_path])
|
|
17
|
-
sys.exit(0)
|
|
18
|
-
|
|
19
|
-
if len(sys.argv) < 2:
|
|
20
|
-
print("Usage:")
|
|
21
|
-
print(" gitcast \"your thought here\" -> Quick single capture")
|
|
22
|
-
print(" gitcast capture -> Start interactive multi-shot session")
|
|
23
|
-
print(" gitcast --setup -> Setup environment variables")
|
|
24
|
-
sys.exit(1)
|
|
25
|
-
|
|
26
|
-
command = sys.argv[1]
|
|
27
|
-
|
|
28
|
-
if command == "capture":
|
|
29
|
-
from core.screenshot_session import ScreenshotSession
|
|
30
|
-
session = ScreenshotSession()
|
|
31
|
-
session.run()
|
|
32
|
-
return
|
|
33
|
-
|
|
34
|
-
thought = " ".join(sys.argv[1:])
|
|
35
|
-
print(f"[Gitcast] Initializing capture with thought: '{thought}'")
|
|
36
|
-
|
|
37
|
-
try:
|
|
38
|
-
# Call the internal trigger endpoint
|
|
39
|
-
response = httpx.post(
|
|
40
|
-
"http://127.0.0.1:8000/api/cli/trigger",
|
|
41
|
-
json={"thought": thought},
|
|
42
|
-
timeout=30
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
if response.status_code == 200:
|
|
46
|
-
print("[Gitcast] Context captured. Opening Draft Room...")
|
|
47
|
-
webbrowser.open("http://127.0.0.1:8000")
|
|
48
|
-
else:
|
|
49
|
-
print(f"[Gitcast Error] Failed to trigger: {response.text}")
|
|
50
|
-
|
|
51
|
-
except Exception as e:
|
|
52
|
-
print(f"[Gitcast Error] Connection failed: {e}")
|
|
53
|
-
print("Is Gitcast running? (python -m gitcast or python main.py)")
|
|
54
|
-
|
|
55
|
-
if __name__ == "__main__":
|
|
56
|
-
main()
|
gitcast-1.0.0/storage/cleanup.py
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import time
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
from config.settings import STORAGE_DIR, screenshot_retention_hours
|
|
5
|
-
|
|
6
|
-
# [Cleanup] module for managing screenshot retention policy
|
|
7
|
-
|
|
8
|
-
def run_cleanup():
|
|
9
|
-
"""Deletes screenshots older than screenshot_retention_hours."""
|
|
10
|
-
screenshots_dir = STORAGE_DIR / "screenshots"
|
|
11
|
-
if not screenshots_dir.exists():
|
|
12
|
-
print("[Cleanup] No screenshots directory found.")
|
|
13
|
-
return
|
|
14
|
-
|
|
15
|
-
now = time.time()
|
|
16
|
-
retention_sec = screenshot_retention_hours * 3600
|
|
17
|
-
deleted_count = 0
|
|
18
|
-
|
|
19
|
-
for file_path in screenshots_dir.glob("*.png"):
|
|
20
|
-
file_age = now - file_path.stat().st_mtime
|
|
21
|
-
if file_age > retention_sec:
|
|
22
|
-
try:
|
|
23
|
-
# Use secure delete if possible, but for cleanup standard unlink is usually fine
|
|
24
|
-
# unless the user specifically wants secure delete for all old files.
|
|
25
|
-
# Given core/security.py exists, we could use it, but it might be overkill for bulk cleanup.
|
|
26
|
-
file_path.unlink()
|
|
27
|
-
deleted_count += 1
|
|
28
|
-
except Exception as e:
|
|
29
|
-
print(f"[Cleanup] Error deleting {file_path.name}: {e}")
|
|
30
|
-
|
|
31
|
-
if deleted_count > 0:
|
|
32
|
-
print(f"[Cleanup] Deleted {deleted_count} old screenshots.")
|
|
33
|
-
else:
|
|
34
|
-
print("[Cleanup] No old screenshots to delete.")
|
|
35
|
-
|
|
36
|
-
def get_storage_stats():
|
|
37
|
-
"""Returns count of screenshots, total size in MB, and oldest file date."""
|
|
38
|
-
screenshots_dir = STORAGE_DIR / "screenshots"
|
|
39
|
-
if not screenshots_dir.exists():
|
|
40
|
-
return {"count": 0, "total_size_mb": 0.0, "oldest_file": "N/A"}
|
|
41
|
-
|
|
42
|
-
files = list(screenshots_dir.glob("*.png"))
|
|
43
|
-
if not files:
|
|
44
|
-
return {"count": 0, "total_size_mb": 0.0, "oldest_file": "N/A"}
|
|
45
|
-
|
|
46
|
-
total_size = sum(f.stat().st_size for f in files)
|
|
47
|
-
oldest_time = min(f.stat().st_mtime for f in files)
|
|
48
|
-
oldest_date = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(oldest_time))
|
|
49
|
-
|
|
50
|
-
return {
|
|
51
|
-
"count": len(files),
|
|
52
|
-
"total_size_mb": round(total_size / (1024 * 1024), 2),
|
|
53
|
-
"oldest_file": oldest_date
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if __name__ == "__main__":
|
|
57
|
-
print("=== STORAGE CLEANUP TEST ===")
|
|
58
|
-
stats = get_storage_stats()
|
|
59
|
-
print(f"Current stats: {stats}")
|
|
60
|
-
run_cleanup()
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import time
|
|
3
|
-
import threading
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
import tweepy
|
|
6
|
-
from config.settings import ENGAGEMENT_LOG, TWITTER_BEARER_TOKEN, POST_LOG
|
|
7
|
-
from storage.tone_memory import update_engagement
|
|
8
|
-
|
|
9
|
-
# [Engagement] module for tracking X metrics 24h after publish
|
|
10
|
-
|
|
11
|
-
def log_pending_fetch(tweet_id: str, post_text: str):
|
|
12
|
-
"""Saves a tweet ID to ENGAGEMENT_LOG for background processing."""
|
|
13
|
-
entry = {
|
|
14
|
-
"tweet_id": tweet_id,
|
|
15
|
-
"post_text": post_text,
|
|
16
|
-
"publish_timestamp": time.time(),
|
|
17
|
-
"fetched": False
|
|
18
|
-
}
|
|
19
|
-
try:
|
|
20
|
-
with open(ENGAGEMENT_LOG, "a", encoding="utf-8") as f:
|
|
21
|
-
f.write(json.dumps(entry) + "\n")
|
|
22
|
-
print(f"[Engagement] Logged pending fetch for tweet {tweet_id}")
|
|
23
|
-
except Exception as e:
|
|
24
|
-
print(f"[Engagement] Error logging pending fetch: {e}")
|
|
25
|
-
|
|
26
|
-
def get_pending_fetches():
|
|
27
|
-
"""Returns list of entries due for metrics fetch (24h+ old, not fetched)."""
|
|
28
|
-
if not ENGAGEMENT_LOG.exists():
|
|
29
|
-
return []
|
|
30
|
-
|
|
31
|
-
pending = []
|
|
32
|
-
now = time.time()
|
|
33
|
-
try:
|
|
34
|
-
with open(ENGAGEMENT_LOG, "r", encoding="utf-8") as f:
|
|
35
|
-
for line in f:
|
|
36
|
-
if not line.strip(): continue
|
|
37
|
-
entry = json.loads(line)
|
|
38
|
-
# 24 hours = 86400 seconds
|
|
39
|
-
if not entry.get("fetched") and (now - entry["publish_timestamp"] > 86400):
|
|
40
|
-
pending.append(entry)
|
|
41
|
-
except Exception as e:
|
|
42
|
-
print(f"[Engagement] Error reading pending fetches: {e}")
|
|
43
|
-
|
|
44
|
-
return pending
|
|
45
|
-
|
|
46
|
-
def fetch_and_store_metrics(tweet_id: str, post_text: str):
|
|
47
|
-
"""Calls X API v2 to get metrics and updates tone_memory."""
|
|
48
|
-
if not TWITTER_BEARER_TOKEN:
|
|
49
|
-
print("[Engagement] No Twitter Bearer Token found, skipping fetch.")
|
|
50
|
-
return False
|
|
51
|
-
|
|
52
|
-
try:
|
|
53
|
-
client = tweepy.Client(bearer_token=TWITTER_BEARER_TOKEN)
|
|
54
|
-
response = client.get_tweet(tweet_id, tweet_fields=["public_metrics"])
|
|
55
|
-
|
|
56
|
-
if response.data:
|
|
57
|
-
metrics = response.data.public_metrics
|
|
58
|
-
likes = metrics.get("like_count", 0)
|
|
59
|
-
retweets = metrics.get("retweet_count", 0)
|
|
60
|
-
replies = metrics.get("reply_count", 0)
|
|
61
|
-
|
|
62
|
-
update_engagement(post_text, likes, retweets, replies)
|
|
63
|
-
return True
|
|
64
|
-
else:
|
|
65
|
-
print(f"[Engagement] Could not find tweet {tweet_id}")
|
|
66
|
-
return False
|
|
67
|
-
except Exception as e:
|
|
68
|
-
print(f"[Engagement] X API error for {tweet_id}: {e}")
|
|
69
|
-
return False
|
|
70
|
-
|
|
71
|
-
def mark_as_fetched(tweet_id: str):
|
|
72
|
-
"""Updates ENGAGEMENT_LOG to mark a tweet as fetched."""
|
|
73
|
-
if not ENGAGEMENT_LOG.exists():
|
|
74
|
-
return
|
|
75
|
-
|
|
76
|
-
entries = []
|
|
77
|
-
try:
|
|
78
|
-
with open(ENGAGEMENT_LOG, "r", encoding="utf-8") as f:
|
|
79
|
-
for line in f:
|
|
80
|
-
if not line.strip(): continue
|
|
81
|
-
entry = json.loads(line)
|
|
82
|
-
if entry["tweet_id"] == tweet_id:
|
|
83
|
-
entry["fetched"] = True
|
|
84
|
-
entries.append(entry)
|
|
85
|
-
|
|
86
|
-
with open(ENGAGEMENT_LOG, "w", encoding="utf-8") as f:
|
|
87
|
-
for e in entries:
|
|
88
|
-
f.write(json.dumps(e) + "\n")
|
|
89
|
-
except Exception as e:
|
|
90
|
-
print(f"[Engagement] Error marking as fetched: {e}")
|
|
91
|
-
|
|
92
|
-
def run_engagement_worker():
|
|
93
|
-
"""Background thread that runs hourly and processes pending fetches."""
|
|
94
|
-
def worker():
|
|
95
|
-
while True:
|
|
96
|
-
print("[Engagement] Checking for pending metrics fetches...")
|
|
97
|
-
pending = get_pending_fetches()
|
|
98
|
-
for entry in pending:
|
|
99
|
-
success = fetch_and_store_metrics(entry["tweet_id"], entry["post_text"])
|
|
100
|
-
if success:
|
|
101
|
-
mark_as_fetched(entry["tweet_id"])
|
|
102
|
-
|
|
103
|
-
# Sleep for 1 hour
|
|
104
|
-
time.sleep(3600)
|
|
105
|
-
|
|
106
|
-
thread = threading.Thread(target=worker, daemon=True)
|
|
107
|
-
thread.start()
|
|
108
|
-
print("[Engagement] Background worker started.")
|
|
109
|
-
|
|
110
|
-
if __name__ == "__main__":
|
|
111
|
-
print("=== ENGAGEMENT TRACKING TEST ===")
|
|
112
|
-
log_pending_fetch("123456789", "Test post text")
|
|
113
|
-
print(f"Pending: {get_pending_fetches()}")
|
|
114
|
-
# Note: X API call will fail without real token in test
|