nopoh22 commited on
Commit
7c95450
·
verified ·
1 Parent(s): f7e54f9

Upload 6 files

Browse files
Files changed (6) hide show
  1. Dockerfile +16 -0
  2. README.md +33 -0
  3. app.py +235 -0
  4. keep_alive.py +31 -0
  5. renewer.py +120 -0
  6. requirements.txt +3 -0
Dockerfile ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+
8
+ COPY renewer.py keep_alive.py app.py ./
9
+
10
+ RUN useradd -m -u 1000 appuser && chown -R appuser /app
11
+ USER appuser
12
+
13
+ # 7860 = Gradio UI | 8080 = keep-alive ping endpoint
14
+ EXPOSE 7860 8080
15
+
16
+ CMD ["python", "app.py"]
README.md ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: PythonAnywhere Auto-Renewer
3
+ emoji: 🐍
4
+ colorFrom: green
5
+ colorTo: blue
6
+ sdk: docker
7
+ pinned: false
8
+ ---
9
+
10
+ # 🐍 PythonAnywhere Auto-Renewer
11
+
12
+ أداة لإدارة وتجديد حسابات PythonAnywhere تلقائيًا كل 10 أيام.
13
+
14
+ ## الميزات
15
+ - ✅ دعم حسابات متعددة
16
+ - ✅ تجديد تلقائي كل 10 أيام ("Run until 1 month from today")
17
+ - ✅ دعم الخوادم الأمريكية والأوروبية
18
+ - ✅ واجهة عربية سهلة الاستخدام
19
+ - ✅ سجل عمليات مفصّل
20
+
21
+ ## طريقة الاستخدام
22
+
23
+ 1. اذهب إلى تبويب **إضافة / حذف**
24
+ 2. أدخل اسم المستخدم وAPI Token بتاعك
25
+ 3. اضغط **إضافة الحساب**
26
+ 4. اذهب إلى **التحكم** وشغّل المجدول التلقائي
27
+
28
+ ## كيف تحصل على API Token
29
+ اذهب إلى: `https://www.pythonanywhere.com/account/#api_token`
30
+
31
+ ## ملاحظة
32
+ البيانات تُخزَّن محليًا داخل الـ Space. إذا أعدت تشغيل الـ Space، يجب إعادة إضافة الحسابات.
33
+ للحفاظ على البيانات بشكل دائم، استخدم Hugging Face Secrets أو Dataset.
app.py ADDED
@@ -0,0 +1,235 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from keep_alive import start_ping_server
3
+ import json
4
+ import os
5
+ import threading
6
+ import time
7
+ from datetime import datetime, timedelta
8
+ from keep_alive import start_ping_server
9
+ from renewer import (
10
+ load_accounts, save_accounts,
11
+ get_host, renew_account, run_renewals,
12
+ ACCOUNTS_FILE
13
+ )
14
+
15
+ SCHEDULER_RUNNING = False
16
+ SCHEDULER_THREAD = None
17
+
18
+
19
+ # ─────────────────────────────────────────────
20
+ # Helpers
21
+ # ─────────────────────────────────────────────
22
+
23
+ def accounts_table():
24
+ accounts = load_accounts()
25
+ if not accounts:
26
+ return "📭 لا توجد حسابات مضافة بعد."
27
+ rows = []
28
+ for a in accounts:
29
+ last = a.get("last_renewed", "—")
30
+ nxt = a.get("next_renewal", "—")
31
+ host = a.get("host", "—")
32
+ rows.append(
33
+ f"| **{a['username']}** | {host} | {last[:16] if last != '—' else '—'} | {nxt[:16] if nxt != '—' else '—'} |"
34
+ )
35
+ header = "| المستخدم | الخادم | آخر تجديد | التجديد القادم |\n|---|---|---|---|"
36
+ return header + "\n" + "\n".join(rows)
37
+
38
+
39
+ def log_box(msg, prev=""):
40
+ ts = datetime.utcnow().strftime("%H:%M:%S")
41
+ line = f"[{ts}] {msg}"
42
+ return (prev + "\n" + line).strip()
43
+
44
+
45
+ # ─────────────────────────────────────────────
46
+ # Actions
47
+ # ─────────────────────────────────────────────
48
+
49
+ def add_account(username, token, logs):
50
+ username = username.strip()
51
+ token = token.strip()
52
+ if not username or not token:
53
+ return gr.update(), accounts_table(), log_box("❌ أدخل اسم المستخدم والتوكن.", logs)
54
+
55
+ accounts = load_accounts()
56
+ if any(a["username"] == username for a in accounts):
57
+ return gr.update(), accounts_table(), log_box(f"⚠️ الحساب {username} موجود مسبقًا.", logs)
58
+
59
+ logs = log_box(f"🔍 جاري التحقق من {username}...", logs)
60
+ host = get_host(username, token)
61
+ if not host:
62
+ return gr.update(), accounts_table(), log_box(f"❌ فشل الاتصال بـ {username}. تحقق من البيانات.", logs)
63
+
64
+ accounts.append({
65
+ "username": username,
66
+ "token": token,
67
+ "host": host,
68
+ "last_renewed": None,
69
+ "next_renewal": None,
70
+ "last_results": []
71
+ })
72
+ save_accounts(accounts)
73
+ logs = log_box(f"✅ تمت إضافة {username} على {host}", logs)
74
+ return gr.update(value=""), accounts_table(), logs
75
+
76
+
77
+ def remove_account(username, logs):
78
+ username = username.strip()
79
+ accounts = load_accounts()
80
+ before = len(accounts)
81
+ accounts = [a for a in accounts if a["username"] != username]
82
+ if len(accounts) == before:
83
+ return accounts_table(), log_box(f"⚠️ لم يُعثر على {username}.", logs)
84
+ save_accounts(accounts)
85
+ return accounts_table(), log_box(f"🗑️ تم حذف {username}.", logs)
86
+
87
+
88
+ def renew_now(logs):
89
+ logs = log_box("🚀 بدء التجديد الفوري لجميع الحسابات...", logs)
90
+ accounts = run_renewals()
91
+ if accounts is None:
92
+ return accounts_table(), log_box("📭 لا توجد حسابات.", logs)
93
+ for a in accounts:
94
+ for r in a.get("last_results", []):
95
+ logs = log_box(f" [{a['username']}] {r['domain']} → {r['status']}", logs)
96
+ logs = log_box("✅ انتهى التجديد.", logs)
97
+ return accounts_table(), logs
98
+
99
+
100
+ def renew_single(username, logs):
101
+ username = username.strip()
102
+ accounts = load_accounts()
103
+ account = next((a for a in accounts if a["username"] == username), None)
104
+ if not account:
105
+ return accounts_table(), log_box(f"❌ الحساب {username} غير موجود.", logs)
106
+
107
+ # Force renewal by clearing next_renewal
108
+ account["next_renewal"] = None
109
+ save_accounts(accounts)
110
+
111
+ logs = log_box(f"🔄 تجديد {username}...", logs)
112
+ success, results = renew_account(account)
113
+ # Re-save with updated timestamps
114
+ accounts = [a if a["username"] != username else account for a in accounts]
115
+ save_accounts(accounts)
116
+
117
+ if success:
118
+ for r in results:
119
+ logs = log_box(f" {r['domain']} → {r['status']}", logs)
120
+ logs = log_box(f"✅ تم تجديد {username}.", logs)
121
+ else:
122
+ logs = log_box(f"❌ فشل تجديد {username}: {results}", logs)
123
+ return accounts_table(), logs
124
+
125
+
126
+ # ─────────────────────────────────────────────
127
+ # Auto-scheduler
128
+ # ───────────────────────────────────────────��─
129
+
130
+ def scheduler_loop():
131
+ while SCHEDULER_RUNNING:
132
+ run_renewals()
133
+ time.sleep(3600) # check every hour, renewer handles the 10-day logic
134
+
135
+
136
+ def toggle_scheduler(state, logs):
137
+ global SCHEDULER_RUNNING, SCHEDULER_THREAD
138
+ if not state: # currently OFF → turn ON
139
+ SCHEDULER_RUNNING = True
140
+ SCHEDULER_THREAD = threading.Thread(target=scheduler_loop, daemon=True)
141
+ SCHEDULER_THREAD.start()
142
+ logs = log_box("⏰ المجدول التلقائي شغّال (كل 10 أيام).", logs)
143
+ return True, "🟢 إيقاف المجدول", logs
144
+ else: # currently ON → turn OFF
145
+ SCHEDULER_RUNNING = False
146
+ logs = log_box("⏹️ تم إيقاف المجدول التلقائي.", logs)
147
+ return False, "▶️ تشغيل المجدول التلقائي", logs
148
+
149
+
150
+ # ─────────────────────────────────────────────
151
+ # UI
152
+ # ─────────────────────────────────────────────
153
+
154
+ CSS = """
155
+ body { direction: rtl; }
156
+ .gr-button { font-size: 15px; }
157
+ #log_box textarea { font-family: monospace; font-size: 12px; direction: ltr; }
158
+ """
159
+
160
+ with gr.Blocks(title="PythonAnywhere Manager", css=CSS, theme=gr.themes.Soft()) as demo:
161
+
162
+ scheduler_state = gr.State(False)
163
+
164
+ gr.Markdown("# 🐍 PythonAnywhere Auto-Renewer\nإدارة وتجديد حسابات PythonAnywhere تلقائيًا كل 10 أيام")
165
+
166
+ with gr.Tabs():
167
+
168
+ # ── Tab 1: Accounts ─────────────────────────────
169
+ with gr.TabItem("📋 الحسابات"):
170
+ tbl = gr.Markdown(accounts_table())
171
+ gr.Button("🔄 تحديث القائمة").click(lambda: accounts_table(), outputs=tbl)
172
+
173
+ # ── Tab 2: Add/Remove ───────────────────────────
174
+ with gr.TabItem("➕ إضافة / حذف"):
175
+ with gr.Row():
176
+ inp_user = gr.Textbox(label="اسم المستخدم", placeholder="nopoh55678")
177
+ inp_token = gr.Textbox(label="API Token", placeholder="496b360a...", type="password")
178
+ btn_add = gr.Button("➕ إضافة الحساب", variant="primary")
179
+
180
+ gr.Markdown("---")
181
+ inp_del = gr.Textbox(label="اسم المستخدم للحذف")
182
+ btn_del = gr.Button("🗑️ حذف الحساب", variant="stop")
183
+
184
+ # ── Tab 3: Control ──────────────────────────────
185
+ with gr.TabItem("🎮 التحكم"):
186
+ with gr.Row():
187
+ btn_renew_all = gr.Button("🚀 تجديد جميع الحسابات الآن", variant="primary")
188
+ btn_scheduler = gr.Button("▶️ تشغيل المجدول التلقائي", variant="secondary")
189
+
190
+ with gr.Row():
191
+ inp_single = gr.Textbox(label="تجديد حساب معين (اسم المستخدم)")
192
+ btn_single = gr.Button("🔄 تجديد")
193
+
194
+ # ── Shared log ──────────────────────────────────
195
+ log = gr.Textbox(
196
+ label="📜 السجل",
197
+ lines=12,
198
+ interactive=False,
199
+ elem_id="log_box",
200
+ value="مرحبًا! أضف حساباتك وشغّل المجدول."
201
+ )
202
+
203
+ # ── Wire up events ──────────────────────────────────
204
+ tbl2 = gr.Markdown(accounts_table(), visible=False) # shared table output
205
+
206
+ btn_add.click(
207
+ add_account,
208
+ inputs=[inp_user, inp_token, log],
209
+ outputs=[inp_user, tbl, log]
210
+ )
211
+ btn_del.click(
212
+ remove_account,
213
+ inputs=[inp_del, log],
214
+ outputs=[tbl, log]
215
+ )
216
+ btn_renew_all.click(
217
+ renew_now,
218
+ inputs=[log],
219
+ outputs=[tbl, log]
220
+ )
221
+ btn_single.click(
222
+ renew_single,
223
+ inputs=[inp_single, log],
224
+ outputs=[tbl, log]
225
+ )
226
+ btn_scheduler.click(
227
+ toggle_scheduler,
228
+ inputs=[scheduler_state, log],
229
+ outputs=[scheduler_state, btn_scheduler, log]
230
+ )
231
+
232
+
233
+ if __name__ == "__main__":
234
+ start_ping_server(port=8080)
235
+ demo.launch(server_name="0.0.0.0", server_port=7860)
keep_alive.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ keep_alive.py
3
+ خادم HTTP بسيط على بورت 8080 يُستخدم فقط لإبقاء الـ Space نشطًا.
4
+ يمكن لأي خدمة ping خارجية (UptimeRobot, cron-job.org) أن تطرقه كل 5 دقائق.
5
+ """
6
+
7
+ from http.server import HTTPServer, BaseHTTPRequestHandler
8
+ import threading
9
+ import logging
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class PingHandler(BaseHTTPRequestHandler):
15
+ def do_GET(self):
16
+ self.send_response(200)
17
+ self.send_header("Content-Type", "text/plain; charset=utf-8")
18
+ self.end_headers()
19
+ self.wfile.write("alive\n".encode())
20
+
21
+ # Suppress default request logs to keep console clean
22
+ def log_message(self, format, *args):
23
+ pass
24
+
25
+
26
+ def start_ping_server(port: int = 8080):
27
+ server = HTTPServer(("0.0.0.0", port), PingHandler)
28
+ t = threading.Thread(target=server.serve_forever, daemon=True)
29
+ t.start()
30
+ logger.info(f"✅ Keep-alive server running on port {port} → GET / → 200 alive")
31
+ return server
renewer.py ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ import json
3
+ import os
4
+ import logging
5
+ from datetime import datetime, timedelta
6
+
7
+ logging.basicConfig(
8
+ level=logging.INFO,
9
+ format='%(asctime)s [%(levelname)s] %(message)s'
10
+ )
11
+ logger = logging.getLogger(__name__)
12
+
13
+ ACCOUNTS_FILE = "accounts.json"
14
+
15
+
16
+ def load_accounts():
17
+ if os.path.exists(ACCOUNTS_FILE):
18
+ with open(ACCOUNTS_FILE, "r") as f:
19
+ return json.load(f)
20
+ return []
21
+
22
+
23
+ def save_accounts(accounts):
24
+ with open(ACCOUNTS_FILE, "w") as f:
25
+ json.dump(accounts, f, indent=2)
26
+
27
+
28
+ def get_host(username, token):
29
+ """Detect if account is on US or EU server."""
30
+ for host in ["www.pythonanywhere.com", "eu.pythonanywhere.com"]:
31
+ try:
32
+ r = requests.get(
33
+ f"https://{host}/api/v0/user/{username}/cpu/",
34
+ headers={"Authorization": f"Token {token}"},
35
+ timeout=10
36
+ )
37
+ if r.status_code == 200:
38
+ return host
39
+ except Exception:
40
+ continue
41
+ return None
42
+
43
+
44
+ def extend_webapp(username, token, host):
45
+ """Extend all webapps by 3 months (Run until 1 month from today)."""
46
+ results = []
47
+ try:
48
+ r = requests.get(
49
+ f"https://{host}/api/v0/user/{username}/webapps/",
50
+ headers={"Authorization": f"Token {token}"},
51
+ timeout=10
52
+ )
53
+ if r.status_code != 200:
54
+ logger.warning(f"[{username}] Failed to list webapps: {r.status_code}")
55
+ return results
56
+
57
+ webapps = r.json()
58
+ for app in webapps:
59
+ domain = app.get("domain_name")
60
+ # Reload = "Run until 1 month from today" for free accounts
61
+ reload_r = requests.post(
62
+ f"https://{host}/api/v0/user/{username}/webapps/{domain}/reload/",
63
+ headers={"Authorization": f"Token {token}"},
64
+ timeout=10
65
+ )
66
+ status = "✅ OK" if reload_r.status_code == 200 else f"❌ {reload_r.status_code}"
67
+ logger.info(f"[{username}] Reload {domain}: {status}")
68
+ results.append({"domain": domain, "status": status})
69
+ except Exception as e:
70
+ logger.error(f"[{username}] Error: {e}")
71
+ return results
72
+
73
+
74
+ def renew_account(account):
75
+ username = account["username"]
76
+ token = account["token"]
77
+ host = account.get("host") or get_host(username, token)
78
+
79
+ if not host:
80
+ logger.error(f"[{username}] Cannot connect - invalid credentials?")
81
+ return False, "Cannot connect"
82
+
83
+ account["host"] = host
84
+ logger.info(f"[{username}] Connected to {host}")
85
+
86
+ results = extend_webapp(username, token, host)
87
+ account["last_renewed"] = datetime.utcnow().isoformat()
88
+ account["next_renewal"] = (datetime.utcnow() + timedelta(days=10)).isoformat()
89
+ account["last_results"] = results
90
+ return True, results
91
+
92
+
93
+ def run_renewals():
94
+ accounts = load_accounts()
95
+ if not accounts:
96
+ logger.info("No accounts configured yet.")
97
+ return
98
+
99
+ now = datetime.utcnow()
100
+ renewed_count = 0
101
+
102
+ for account in accounts:
103
+ username = account["username"]
104
+ next_renewal_str = account.get("next_renewal")
105
+
106
+ if next_renewal_str:
107
+ next_renewal = datetime.fromisoformat(next_renewal_str)
108
+ if now < next_renewal:
109
+ remaining = (next_renewal - now).days
110
+ logger.info(f"[{username}] Skipping - next renewal in {remaining} days")
111
+ continue
112
+
113
+ logger.info(f"[{username}] Starting renewal...")
114
+ success, results = renew_account(account)
115
+ if success:
116
+ renewed_count += 1
117
+
118
+ save_accounts(accounts)
119
+ logger.info(f"Done. Renewed {renewed_count}/{len(accounts)} accounts.")
120
+ return accounts
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ requests==2.31.0
2
+ gradio==3.50.2
3
+ huggingface_hub==0.19.4