pymulakat 1.0.0__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.
- pymulakat-1.0.0/PKG-INFO +9 -0
- pymulakat-1.0.0/README.md +0 -0
- pymulakat-1.0.0/pyproject.toml +24 -0
- pymulakat-1.0.0/setup.cfg +4 -0
- pymulakat-1.0.0/src/pymulakat/__init__.py +0 -0
- pymulakat-1.0.0/src/pymulakat/main.py +901 -0
- pymulakat-1.0.0/src/pymulakat.egg-info/PKG-INFO +9 -0
- pymulakat-1.0.0/src/pymulakat.egg-info/SOURCES.txt +10 -0
- pymulakat-1.0.0/src/pymulakat.egg-info/dependency_links.txt +1 -0
- pymulakat-1.0.0/src/pymulakat.egg-info/entry_points.txt +2 -0
- pymulakat-1.0.0/src/pymulakat.egg-info/requires.txt +4 -0
- pymulakat-1.0.0/src/pymulakat.egg-info/top_level.txt +1 -0
pymulakat-1.0.0/PKG-INFO
ADDED
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=42", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pymulakat"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Profesyonel Python Mülakat Platformu CLI"
|
|
9
|
+
requires-python = ">=3.8"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"click>=8.0",
|
|
12
|
+
"rich>=13.0",
|
|
13
|
+
"requests>=2.28",
|
|
14
|
+
"PyJWT>=2.0",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.scripts]
|
|
18
|
+
pymulakat = "pymulakat.main:cli"
|
|
19
|
+
|
|
20
|
+
[tool.setuptools.packages.find]
|
|
21
|
+
where = ["src"]
|
|
22
|
+
|
|
23
|
+
[tool.setuptools.package-dir]
|
|
24
|
+
"" = "src"
|
|
File without changes
|
|
@@ -0,0 +1,901 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
PyMulakat CLI - Modern ve Profesyonel Mülakat Platformu Arayüzü
|
|
4
|
+
"""
|
|
5
|
+
import http.server
|
|
6
|
+
import socketserver
|
|
7
|
+
import urllib.parse
|
|
8
|
+
import secrets
|
|
9
|
+
from threading import Thread
|
|
10
|
+
import time
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
import stat
|
|
14
|
+
import errno
|
|
15
|
+
import json
|
|
16
|
+
import base64
|
|
17
|
+
import logging
|
|
18
|
+
import subprocess
|
|
19
|
+
import tempfile
|
|
20
|
+
import re
|
|
21
|
+
import requests
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from datetime import datetime
|
|
24
|
+
from typing import Optional
|
|
25
|
+
|
|
26
|
+
import click
|
|
27
|
+
from rich.console import Console
|
|
28
|
+
from rich.panel import Panel
|
|
29
|
+
from rich.table import Table
|
|
30
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
31
|
+
from rich.prompt import Prompt, Confirm
|
|
32
|
+
from rich.theme import Theme
|
|
33
|
+
from rich.markdown import Markdown
|
|
34
|
+
from rich.syntax import Syntax
|
|
35
|
+
from rich.live import Live
|
|
36
|
+
from rich.layout import Layout
|
|
37
|
+
from rich.box import ROUNDED
|
|
38
|
+
|
|
39
|
+
# ─────────────────────────────────────────────
|
|
40
|
+
# 🎨 Tema & Console
|
|
41
|
+
# ─────────────────────────────────────────────
|
|
42
|
+
custom_theme = Theme({
|
|
43
|
+
"info": "cyan",
|
|
44
|
+
"warning": "yellow",
|
|
45
|
+
"error": "bold red",
|
|
46
|
+
"success": "bold green",
|
|
47
|
+
"primary": "bold blue",
|
|
48
|
+
"secondary": "dim white",
|
|
49
|
+
"accent": "magenta",
|
|
50
|
+
})
|
|
51
|
+
console = Console(theme=custom_theme)
|
|
52
|
+
|
|
53
|
+
# ─────────────────────────────────────────────
|
|
54
|
+
# 🔧 Sabitler
|
|
55
|
+
# ─────────────────────────────────────────────
|
|
56
|
+
API_BASE = os.getenv("API_URL", "https://datascience-tutor-backend-production.up.railway.app")
|
|
57
|
+
PYMULAKAT_DIR = Path.home() / ".pymulakat"
|
|
58
|
+
TOKEN_FILE = PYMULAKAT_DIR / "token"
|
|
59
|
+
CONFIG_FILE = PYMULAKAT_DIR / "config.json"
|
|
60
|
+
META_FILE = Path(".interview_meta.json")
|
|
61
|
+
MIN_PYTHON_VERSION = (3, 8)
|
|
62
|
+
|
|
63
|
+
logger = logging.getLogger("pymulakat")
|
|
64
|
+
|
|
65
|
+
# ═══════════════════════════════════════════════════════════
|
|
66
|
+
# ⚙️ KULLANICI İZİN KONFİGÜRASYONU
|
|
67
|
+
# ═══════════════════════════════════════════════════════════
|
|
68
|
+
|
|
69
|
+
# Varsayılan izinler (octal)
|
|
70
|
+
DEFAULT_PERMISSIONS = {
|
|
71
|
+
"token_file": 0o600, # Sadece owner r/w
|
|
72
|
+
"config_file": 0o600, # Sadece owner r/w
|
|
73
|
+
"solution_file": 0o644, # Owner r/w, diğerleri r
|
|
74
|
+
"test_file": 0o600, # Gizli: sadece owner r/w
|
|
75
|
+
"meta_file": 0o644, # Owner r/w, diğerleri r
|
|
76
|
+
"pymulakat_dir": 0o700, # Sadece owner erişebilir
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# İzin seti şablonları (kullanıcı seçer)
|
|
80
|
+
PERMISSION_PRESETS = {
|
|
81
|
+
"private": {"solution_file": 0o600, "meta_file": 0o600, "pymulakat_dir": 0o700},
|
|
82
|
+
"standard": {"solution_file": 0o644, "meta_file": 0o644, "pymulakat_dir": 0o700},
|
|
83
|
+
"shared": {"solution_file": 0o664, "meta_file": 0o664, "pymulakat_dir": 0o755},
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def load_permission_config() -> dict:
|
|
88
|
+
"""
|
|
89
|
+
İzin konfigürasyonunu ~/.pymulakat/config.json'dan yükler.
|
|
90
|
+
Dosya yoksa varsayılanları döner.
|
|
91
|
+
"""
|
|
92
|
+
if CONFIG_FILE.exists():
|
|
93
|
+
try:
|
|
94
|
+
raw = json.loads(CONFIG_FILE.read_text(encoding="utf-8"))
|
|
95
|
+
perms = DEFAULT_PERMISSIONS.copy()
|
|
96
|
+
for key, val in raw.get("permissions", {}).items():
|
|
97
|
+
if key in perms:
|
|
98
|
+
# JSON'da int veya "0o644" string olabilir
|
|
99
|
+
perms[key] = int(str(val), 8) if isinstance(val, str) else val
|
|
100
|
+
return perms
|
|
101
|
+
except Exception as e:
|
|
102
|
+
logger.warning(f"Config okunamadı, varsayılanlar kullanılıyor: {e}")
|
|
103
|
+
return DEFAULT_PERMISSIONS.copy()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def save_permission_config(perms: dict):
|
|
107
|
+
"""İzin ayarlarını config.json'a kaydeder."""
|
|
108
|
+
_ensure_pymulakat_dir()
|
|
109
|
+
existing = {}
|
|
110
|
+
if CONFIG_FILE.exists():
|
|
111
|
+
try:
|
|
112
|
+
existing = json.loads(CONFIG_FILE.read_text(encoding="utf-8"))
|
|
113
|
+
except Exception:
|
|
114
|
+
pass
|
|
115
|
+
existing["permissions"] = {k: oct(v) for k, v in perms.items()}
|
|
116
|
+
_atomic_write(CONFIG_FILE, json.dumps(existing, indent=2, ensure_ascii=False))
|
|
117
|
+
_set_mode(CONFIG_FILE, perms.get("config_file", 0o600))
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ═══════════════════════════════════════════════════════════
|
|
121
|
+
# 🗂️ DÜŞÜK SEVİYE DOSYA YARDIMCILARI
|
|
122
|
+
# ═══════════════════════════════════════════════════════════
|
|
123
|
+
|
|
124
|
+
def _ensure_pymulakat_dir():
|
|
125
|
+
"""~/.pymulakat dizinini güvenli şekilde oluşturur."""
|
|
126
|
+
try:
|
|
127
|
+
PYMULAKAT_DIR.mkdir(parents=True, exist_ok=True)
|
|
128
|
+
_set_mode(PYMULAKAT_DIR, load_permission_config().get("pymulakat_dir", 0o700))
|
|
129
|
+
except PermissionError as e:
|
|
130
|
+
raise PermissionError(
|
|
131
|
+
f"❌ '{PYMULAKAT_DIR}' dizini oluşturulamadı: {e}\n"
|
|
132
|
+
f" Çözüm: chmod 700 {PYMULAKAT_DIR.parent}"
|
|
133
|
+
) from e
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _set_mode(path: Path, mode: int):
|
|
137
|
+
"""Dosya/dizin iznini ayarlar; hata varsa loglar ama patlamaz."""
|
|
138
|
+
try:
|
|
139
|
+
path.chmod(mode)
|
|
140
|
+
except PermissionError:
|
|
141
|
+
logger.warning(f"⚠️ İzin ayarlanamadı: {path} → {oct(mode)}")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _atomic_write(path: Path, content: str, encoding: str = "utf-8"):
|
|
145
|
+
"""
|
|
146
|
+
İçeriği geçici dosyaya yazar, sonra atomik olarak rename eder.
|
|
147
|
+
Yarım yazma / bozuk dosya riskini ortadan kaldırır.
|
|
148
|
+
"""
|
|
149
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
150
|
+
tmp_path = path.with_suffix(path.suffix + ".tmp")
|
|
151
|
+
try:
|
|
152
|
+
tmp_path.write_text(content, encoding=encoding)
|
|
153
|
+
tmp_path.replace(path) # atomic on POSIX; near-atomic on Windows
|
|
154
|
+
except OSError as e:
|
|
155
|
+
tmp_path.unlink(missing_ok=True)
|
|
156
|
+
_raise_os_error(e, path)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _raise_os_error(e: OSError, path: Path):
|
|
160
|
+
"""OSError'u anlaşılır mesaja çevirir."""
|
|
161
|
+
if e.errno == errno.EACCES:
|
|
162
|
+
raise PermissionError(f"❌ Erişim reddedildi: '{path}'. Çözüm: chmod 644 {path}") from e
|
|
163
|
+
if e.errno == errno.ENOSPC:
|
|
164
|
+
raise OSError(f"❌ Disk dolu, '{path}' yazılamadı.") from e
|
|
165
|
+
if e.errno == errno.EROFS:
|
|
166
|
+
raise OSError(f"❌ Salt-okunur dosya sistemi: '{path}'.") from e
|
|
167
|
+
raise
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def get_permissions_info(path: Path) -> dict:
|
|
171
|
+
"""Dosya/dizin izin bilgilerini döner."""
|
|
172
|
+
if not path.exists():
|
|
173
|
+
return {"path": str(path), "exists": False}
|
|
174
|
+
st = path.stat()
|
|
175
|
+
return {
|
|
176
|
+
"path": str(path),
|
|
177
|
+
"exists": True,
|
|
178
|
+
"mode": stat.filemode(st.st_mode),
|
|
179
|
+
"octal": oct(stat.S_IMODE(st.st_mode)),
|
|
180
|
+
"readable": os.access(path, os.R_OK),
|
|
181
|
+
"writable": os.access(path, os.W_OK),
|
|
182
|
+
"owner_uid": st.st_uid,
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# ═══════════════════════════════════════════════════════════
|
|
187
|
+
# 🔐 TOKEN YÖNETİMİ
|
|
188
|
+
# ═══════════════════════════════════════════════════════════
|
|
189
|
+
|
|
190
|
+
def get_token() -> Optional[str]:
|
|
191
|
+
if TOKEN_FILE.exists():
|
|
192
|
+
try:
|
|
193
|
+
return TOKEN_FILE.read_text(encoding="utf-8").strip()
|
|
194
|
+
except PermissionError:
|
|
195
|
+
console.print("[error]❌ Token dosyası okunamadı — izin hatası.[/]")
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def save_token(token: str):
|
|
200
|
+
perms = load_permission_config()
|
|
201
|
+
_ensure_pymulakat_dir()
|
|
202
|
+
_atomic_write(TOKEN_FILE, token)
|
|
203
|
+
_set_mode(TOKEN_FILE, perms["token_file"])
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def get_auth_headers() -> dict:
|
|
207
|
+
token = get_token()
|
|
208
|
+
return {"Authorization": f"Bearer {token}"} if token else {}
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# ═══════════════════════════════════════════════════════════
|
|
212
|
+
# 📄 META DOSYASI
|
|
213
|
+
# ═══════════════════════════════════════════════════════════
|
|
214
|
+
|
|
215
|
+
def save_interview_meta(question_id: str, lib: str, topic: str, solution_file: str):
|
|
216
|
+
perms = load_permission_config()
|
|
217
|
+
meta: dict = {}
|
|
218
|
+
if META_FILE.exists():
|
|
219
|
+
try:
|
|
220
|
+
meta = json.loads(META_FILE.read_text(encoding="utf-8"))
|
|
221
|
+
except Exception:
|
|
222
|
+
meta = {}
|
|
223
|
+
meta[question_id] = {"lib": lib, "topic": topic, "file": solution_file}
|
|
224
|
+
_atomic_write(META_FILE, json.dumps(meta, ensure_ascii=False, indent=2))
|
|
225
|
+
_set_mode(META_FILE, perms["meta_file"])
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def load_interview_meta(question_id: str) -> Optional[dict]:
|
|
229
|
+
if not META_FILE.exists():
|
|
230
|
+
return None
|
|
231
|
+
try:
|
|
232
|
+
meta = json.loads(META_FILE.read_text(encoding="utf-8"))
|
|
233
|
+
return meta.get(question_id)
|
|
234
|
+
except Exception:
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
# ═══════════════════════════════════════════════════════════
|
|
239
|
+
# 🔧 Genel Yardımcılar
|
|
240
|
+
# ═══════════════════════════════════════════════════════════
|
|
241
|
+
|
|
242
|
+
def get_next_solution_filename() -> str:
|
|
243
|
+
pattern = re.compile(r"solution(\d+)\.py")
|
|
244
|
+
existing_nums = [
|
|
245
|
+
int(m.group(1))
|
|
246
|
+
for f in Path.cwd().glob("solution*.py")
|
|
247
|
+
if (m := pattern.match(f.name))
|
|
248
|
+
]
|
|
249
|
+
return f"solution{max(existing_nums) + 1 if existing_nums else 1}.py"
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def get_latest_solution_file() -> Optional[str]:
|
|
253
|
+
solutions = sorted(Path.cwd().glob("solution*.py"), key=lambda p: p.stat().st_mtime, reverse=True)
|
|
254
|
+
return solutions[0].name if solutions else None
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def check_python_version():
|
|
258
|
+
current = sys.version_info[:2]
|
|
259
|
+
if current < MIN_PYTHON_VERSION:
|
|
260
|
+
console.print(Panel(
|
|
261
|
+
f"[error]❌ Python {MIN_PYTHON_VERSION[0]}.{MIN_PYTHON_VERSION[1]}+ gerekiyor!\n"
|
|
262
|
+
f" Mevcut: {sys.version.split()[0]}[/]",
|
|
263
|
+
title="Versiyon Hatası", border_style="red"
|
|
264
|
+
))
|
|
265
|
+
sys.exit(1)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def check_interpreter(lib: str = "python") -> bool:
|
|
269
|
+
try:
|
|
270
|
+
result = subprocess.run([lib, "--version"], capture_output=True, text=True, timeout=5)
|
|
271
|
+
if result.returncode == 0:
|
|
272
|
+
console.print(f"[success]✓[/] {lib} {result.stdout.strip().split()[-1]} bulundu")
|
|
273
|
+
return True
|
|
274
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
275
|
+
pass
|
|
276
|
+
console.print(f"[error]✗[/] {lib} bulunamadı!")
|
|
277
|
+
return False
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def decode_base64_content(encoded: str) -> str:
|
|
281
|
+
return base64.b64decode(encoded).decode("utf-8")
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def api_request(method: str, endpoint: str, **kwargs) -> dict:
|
|
285
|
+
url = f"{API_BASE}{endpoint}"
|
|
286
|
+
headers = kwargs.pop("headers", {})
|
|
287
|
+
headers.update(get_auth_headers())
|
|
288
|
+
try:
|
|
289
|
+
response = requests.request(method, url, headers=headers, timeout=30, **kwargs)
|
|
290
|
+
response.raise_for_status()
|
|
291
|
+
return response.json() if response.content else {}
|
|
292
|
+
except requests.exceptions.HTTPError as e:
|
|
293
|
+
detail = e.response.json().get("detail", str(e)) if e.response and e.response.content else str(e)
|
|
294
|
+
console.print(Panel(f"[error]{detail}[/]", title="API Hatası", border_style="red"))
|
|
295
|
+
sys.exit(1)
|
|
296
|
+
except requests.exceptions.ConnectionError:
|
|
297
|
+
console.print(Panel("[error]Sunucuya bağlanılamadı.[/]", title="Bağlantı Hatası", border_style="red"))
|
|
298
|
+
sys.exit(1)
|
|
299
|
+
except requests.exceptions.Timeout:
|
|
300
|
+
console.print(Panel("[error]İstek zaman aşımına uğradı.[/]", title="Timeout", border_style="red"))
|
|
301
|
+
sys.exit(1)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
# ═══════════════════════════════════════════════════════════
|
|
305
|
+
# 🎨 UI Yardımcıları
|
|
306
|
+
# ═══════════════════════════════════════════════════════════
|
|
307
|
+
|
|
308
|
+
def show_header(title: str, subtitle: str = ""):
|
|
309
|
+
console.print()
|
|
310
|
+
content = f"[bold blue]{title}[/]"
|
|
311
|
+
if subtitle:
|
|
312
|
+
content += f"\n[dim white]{subtitle}[/]"
|
|
313
|
+
console.print(Panel(content, box=ROUNDED, border_style="blue", padding=(1, 2)))
|
|
314
|
+
console.print()
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def show_success(message: str, title: str = "Başarılı"):
|
|
318
|
+
console.print(Panel(f"[success]{message}[/]", title=title, border_style="green"))
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def show_error(message: str, title: str = "Hata"):
|
|
322
|
+
console.print(Panel(f"[error]{message}[/]", title=title, border_style="red"))
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
# ═══════════════════════════════════════════════════════════
|
|
326
|
+
# 🔒 LOGIN KORUMA DECORATOR'I
|
|
327
|
+
# ═══════════════════════════════════════════════════════════
|
|
328
|
+
|
|
329
|
+
def require_login(func):
|
|
330
|
+
"""
|
|
331
|
+
Komutu çalıştırmadan önce geçerli bir token olup olmadığını kontrol eder.
|
|
332
|
+
Token yoksa kullanıcıya giriş yapmasını söyler ve komutu iptal eder.
|
|
333
|
+
"""
|
|
334
|
+
import functools
|
|
335
|
+
|
|
336
|
+
@functools.wraps(func)
|
|
337
|
+
def wrapper(*args, **kwargs):
|
|
338
|
+
token = get_token()
|
|
339
|
+
if not token:
|
|
340
|
+
console.print()
|
|
341
|
+
console.print(Panel(
|
|
342
|
+
"[error]Bu işlemi gerçekleştirmek için giriş yapmanız gerekiyor.[/]\n\n"
|
|
343
|
+
"[info]👉 Giriş yapmak için:[/] [cyan]pymulakat login[/]",
|
|
344
|
+
title="🔒 Oturum Gerekli",
|
|
345
|
+
border_style="red",
|
|
346
|
+
))
|
|
347
|
+
console.print()
|
|
348
|
+
sys.exit(1)
|
|
349
|
+
return func(*args, **kwargs)
|
|
350
|
+
|
|
351
|
+
return wrapper
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def create_progress_table(data: list, lib_filter: Optional[str] = None):
|
|
355
|
+
table = Table(box=ROUNDED, header_style="bold blue", show_lines=True)
|
|
356
|
+
table.add_column("ID", style="cyan", width=8)
|
|
357
|
+
table.add_column("Kütüphane", style="magenta", width=12)
|
|
358
|
+
table.add_column("Konu", style="white", width=30)
|
|
359
|
+
table.add_column("Zorluk", style="yellow", width=12)
|
|
360
|
+
for item in data:
|
|
361
|
+
difficulty_badge = {
|
|
362
|
+
"Beginner": "[green]🟢 Beginner[/]",
|
|
363
|
+
"Junior-Mid": "[yellow]🟡 Junior-Mid[/]",
|
|
364
|
+
"Mid-Senior": "[red]🔴 Mid-Senior[/]",
|
|
365
|
+
}.get(item.get("difficulty", ""), "⚪ Belirtilmemiş")
|
|
366
|
+
table.add_row(
|
|
367
|
+
f"[cyan]#{item['id']}[/]",
|
|
368
|
+
f"[magenta]{item.get('lib', 'unknown')}[/]",
|
|
369
|
+
item.get("topic", item.get("title", "Başlıksız")),
|
|
370
|
+
difficulty_badge,
|
|
371
|
+
)
|
|
372
|
+
return table
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
# ═══════════════════════════════════════════════════════════
|
|
376
|
+
# 🔐 AUTH KOMUTLARI
|
|
377
|
+
# ═══════════════════════════════════════════════════════════
|
|
378
|
+
|
|
379
|
+
@click.group()
|
|
380
|
+
@click.version_option(version="1.0.0", prog_name="pymulakat")
|
|
381
|
+
@click.pass_context
|
|
382
|
+
def cli(ctx):
|
|
383
|
+
"""🎓 PyMulakat CLI - Profesyonel Python Mülakat Platformu"""
|
|
384
|
+
check_python_version()
|
|
385
|
+
ctx.ensure_object(dict)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
@cli.command()
|
|
389
|
+
@click.option("-e", "--email", prompt="E-posta", help="Kullanıcı e-posta adresi")
|
|
390
|
+
@click.option("-p", "--password", prompt="Şifre", hide_input=True, help="Kullanıcı şifresi")
|
|
391
|
+
def login(email: str, password: str):
|
|
392
|
+
"""🔐 CLI üzerinden giriş yap (OAuth flow)"""
|
|
393
|
+
show_header("🔐 Giriş", "Hesabınıza bağlanılıyor...")
|
|
394
|
+
|
|
395
|
+
with Progress(SpinnerColumn(), TextColumn("[primary]Doğrulanıyor...[/]"), console=console) as progress:
|
|
396
|
+
progress.add_task("", total=None)
|
|
397
|
+
try:
|
|
398
|
+
data = api_request("POST", "/auth/cli/init", json={"email": email, "password": password})
|
|
399
|
+
session_id = data.get("session_id")
|
|
400
|
+
if not session_id:
|
|
401
|
+
show_error("session_id alınamadı")
|
|
402
|
+
return
|
|
403
|
+
|
|
404
|
+
state = secrets.token_urlsafe(16)
|
|
405
|
+
port = 3000 + secrets.randbelow(1000)
|
|
406
|
+
redirect_uri = f"http://localhost:{port}/callback"
|
|
407
|
+
auth_url = (
|
|
408
|
+
f"http://www.pythonmulakat.com/authorize"
|
|
409
|
+
f"?session={urllib.parse.quote(session_id)}"
|
|
410
|
+
f"&redirect_uri={urllib.parse.quote(redirect_uri)}"
|
|
411
|
+
f"&state={state}"
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
console.print(Panel(
|
|
415
|
+
f"[info]Lütfen aşağıdaki linki tarayıcınızda açın:[/]\n\n"
|
|
416
|
+
f"[link={auth_url}]{auth_url}[/]\n\n"
|
|
417
|
+
f"[secondary]Giriş yapıp onay verdikten sonra token otomatik alınacaktır.[/]",
|
|
418
|
+
title="🌐 Tarayıcı Onayı", border_style="cyan"
|
|
419
|
+
))
|
|
420
|
+
|
|
421
|
+
token_received = None
|
|
422
|
+
|
|
423
|
+
class CallbackHandler(http.server.BaseHTTPRequestHandler):
|
|
424
|
+
def do_GET(self):
|
|
425
|
+
nonlocal token_received
|
|
426
|
+
parsed = urllib.parse.urlparse(self.path)
|
|
427
|
+
if parsed.path == "/callback":
|
|
428
|
+
params = urllib.parse.parse_qs(parsed.query)
|
|
429
|
+
returned_token = params.get("token", [None])[0]
|
|
430
|
+
returned_state = params.get("state", [None])[0]
|
|
431
|
+
if returned_token and returned_state == state:
|
|
432
|
+
token_received = returned_token
|
|
433
|
+
self.send_response(200)
|
|
434
|
+
self.send_header("Content-type", "text/html")
|
|
435
|
+
self.end_headers()
|
|
436
|
+
self.wfile.write(b"Login successful!")
|
|
437
|
+
else:
|
|
438
|
+
self.send_response(400)
|
|
439
|
+
self.end_headers()
|
|
440
|
+
self.wfile.write("Onay alınamadı.".encode("utf-8"))
|
|
441
|
+
else:
|
|
442
|
+
self.send_response(404)
|
|
443
|
+
self.end_headers()
|
|
444
|
+
|
|
445
|
+
def log_message(self, format, *args):
|
|
446
|
+
pass
|
|
447
|
+
|
|
448
|
+
def run_server():
|
|
449
|
+
with socketserver.TCPServer(("localhost", port), CallbackHandler) as httpd:
|
|
450
|
+
httpd.handle_request()
|
|
451
|
+
|
|
452
|
+
Thread(target=run_server, daemon=True).start()
|
|
453
|
+
|
|
454
|
+
try:
|
|
455
|
+
import webbrowser
|
|
456
|
+
webbrowser.open(auth_url)
|
|
457
|
+
except Exception:
|
|
458
|
+
pass
|
|
459
|
+
|
|
460
|
+
except Exception as e:
|
|
461
|
+
show_error(str(e))
|
|
462
|
+
return
|
|
463
|
+
|
|
464
|
+
with Progress(SpinnerColumn(), TextColumn("[primary]Onay bekleniyor...[/]"), console=console) as progress:
|
|
465
|
+
progress.add_task("", total=None)
|
|
466
|
+
for _ in range(60):
|
|
467
|
+
if token_received:
|
|
468
|
+
break
|
|
469
|
+
time.sleep(1)
|
|
470
|
+
|
|
471
|
+
if token_received:
|
|
472
|
+
try:
|
|
473
|
+
save_token(token_received)
|
|
474
|
+
show_success("Giriş başarılı! Token kaydedildi.", "✓ Oturum Açıldı")
|
|
475
|
+
except (PermissionError, OSError) as e:
|
|
476
|
+
show_error(str(e), "Token Kaydedilemedi")
|
|
477
|
+
else:
|
|
478
|
+
show_error("Onay süresi doldu. Lütfen tekrar deneyin.")
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
@cli.command()
|
|
482
|
+
def logout():
|
|
483
|
+
"""🚪 Çıkış yap ve token'ı sil"""
|
|
484
|
+
if TOKEN_FILE.exists():
|
|
485
|
+
try:
|
|
486
|
+
TOKEN_FILE.unlink()
|
|
487
|
+
show_success("Çıkış yapıldı. Token silindi.", "✓ Oturum Kapatıldı")
|
|
488
|
+
except PermissionError as e:
|
|
489
|
+
show_error(f"Token silinemedi: {e}")
|
|
490
|
+
else:
|
|
491
|
+
console.print("[warning]⚠ Zaten giriş yapılmamış.[/]")
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
# ═══════════════════════════════════════════════════════════
|
|
495
|
+
# ⚙️ CONFIG KOMUTU — kullanıcıdan izin al
|
|
496
|
+
# ═══════════════════════════════════════════════════════════
|
|
497
|
+
|
|
498
|
+
@cli.command("config")
|
|
499
|
+
@click.option("--preset", type=click.Choice(["private", "standard", "shared"]),
|
|
500
|
+
default=None, help="Hazır izin şablonu seç")
|
|
501
|
+
@click.option("--show", is_flag=True, help="Mevcut ayarları göster")
|
|
502
|
+
def config_cmd(preset: Optional[str], show: bool):
|
|
503
|
+
"""⚙️ Dosya izinlerini yapılandır (kullanıcı tarafından)"""
|
|
504
|
+
show_header("⚙️ İzin Yapılandırması", "Dosya erişim izinlerini özelleştirin")
|
|
505
|
+
|
|
506
|
+
perms = load_permission_config()
|
|
507
|
+
|
|
508
|
+
if show:
|
|
509
|
+
table = Table(box=ROUNDED, title="Mevcut İzin Ayarları", header_style="bold blue")
|
|
510
|
+
table.add_column("Dosya Türü", style="cyan")
|
|
511
|
+
table.add_column("Octal İzin", style="yellow")
|
|
512
|
+
table.add_column("Açıklama", style="dim")
|
|
513
|
+
descriptions = {
|
|
514
|
+
"token_file": "JWT token (~/.pymulakat/token)",
|
|
515
|
+
"config_file": "Bu config dosyası",
|
|
516
|
+
"solution_file": "İndirilen çözüm dosyaları",
|
|
517
|
+
"test_file": "Gizli test dosyaları",
|
|
518
|
+
"meta_file": "Oturum meta verisi",
|
|
519
|
+
"pymulakat_dir": "~/.pymulakat dizini",
|
|
520
|
+
}
|
|
521
|
+
for key, val in perms.items():
|
|
522
|
+
table.add_row(key, oct(val), descriptions.get(key, ""))
|
|
523
|
+
console.print(table)
|
|
524
|
+
return
|
|
525
|
+
|
|
526
|
+
if preset:
|
|
527
|
+
perms.update(PERMISSION_PRESETS[preset])
|
|
528
|
+
console.print(f"[success]✓[/] '{preset}' şablonu uygulandı.")
|
|
529
|
+
else:
|
|
530
|
+
# İnteraktif mod: her izni kullanıcıdan al
|
|
531
|
+
console.print("[info]Her dosya türü için izin girin (örn: 0o644) veya boş bırakıp varsayılanı kullanın.[/]\n")
|
|
532
|
+
for key, current in perms.items():
|
|
533
|
+
raw = Prompt.ask(
|
|
534
|
+
f" [cyan]{key}[/]",
|
|
535
|
+
default=oct(current),
|
|
536
|
+
show_default=True,
|
|
537
|
+
).strip()
|
|
538
|
+
try:
|
|
539
|
+
perms[key] = int(raw, 8)
|
|
540
|
+
except ValueError:
|
|
541
|
+
console.print(f" [warning]Geçersiz değer, {oct(current)} korundu.[/]")
|
|
542
|
+
|
|
543
|
+
save_permission_config(perms)
|
|
544
|
+
show_success(f"İzin ayarları kaydedildi → {CONFIG_FILE}", "✓ Kaydedildi")
|
|
545
|
+
|
|
546
|
+
# Var olan dosyalara hemen uygula
|
|
547
|
+
for path, key in [
|
|
548
|
+
(TOKEN_FILE, "token_file"),
|
|
549
|
+
(CONFIG_FILE, "config_file"),
|
|
550
|
+
(META_FILE, "meta_file"),
|
|
551
|
+
(PYMULAKAT_DIR, "pymulakat_dir"),
|
|
552
|
+
]:
|
|
553
|
+
if Path(path).exists():
|
|
554
|
+
_set_mode(Path(path), perms[key])
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
# ═══════════════════════════════════════════════════════════
|
|
558
|
+
# 📋 SORU YÖNETİMİ KOMUTLARI
|
|
559
|
+
# ═══════════════════════════════════════════════════════════
|
|
560
|
+
|
|
561
|
+
@cli.command("test")
|
|
562
|
+
@click.argument("question_id")
|
|
563
|
+
@click.option("-l", "--lib", default=None, help="Kütüphane adı (verilmezse meta'dan okunur)")
|
|
564
|
+
@click.option("-f", "--file", default=None, help="Test edilecek dosya (verilmezse en son solution*.py)")
|
|
565
|
+
@require_login
|
|
566
|
+
def test_solution(question_id: str, lib: Optional[str], file: Optional[str]):
|
|
567
|
+
"""🧪 Çözümü yerel olarak test et"""
|
|
568
|
+
show_header("🧪 Yerel Test", f"Soru #{question_id}")
|
|
569
|
+
|
|
570
|
+
if not lib:
|
|
571
|
+
meta = load_interview_meta(question_id)
|
|
572
|
+
if meta:
|
|
573
|
+
lib = meta["lib"]
|
|
574
|
+
console.print(f"[dim]📌 Meta'dan kütüphane okundu: [cyan]{lib}[/][/]")
|
|
575
|
+
else:
|
|
576
|
+
show_error(
|
|
577
|
+
f"Soru #{question_id} için kütüphane bilgisi bulunamadı.\n"
|
|
578
|
+
" → `pymulakat download` ile soruyu indirin veya -l/--lib ile belirtin."
|
|
579
|
+
)
|
|
580
|
+
return
|
|
581
|
+
|
|
582
|
+
if not file:
|
|
583
|
+
file = get_latest_solution_file()
|
|
584
|
+
if not file:
|
|
585
|
+
show_error("Dizinde solution*.py bulunamadı.")
|
|
586
|
+
console.print("[info]💡 `pymulakat download` ile önce kodları indirin.[/]")
|
|
587
|
+
return
|
|
588
|
+
console.print(f"[info]📄 Otomatik seçilen dosya: [cyan]{file}[/]")
|
|
589
|
+
|
|
590
|
+
solution_path = Path(file)
|
|
591
|
+
if not solution_path.exists():
|
|
592
|
+
show_error(f"Dosya bulunamadı: {file}")
|
|
593
|
+
return
|
|
594
|
+
|
|
595
|
+
try:
|
|
596
|
+
solution_code = solution_path.read_text(encoding="utf-8")
|
|
597
|
+
except PermissionError as e:
|
|
598
|
+
show_error(f"Dosya okunamadı (izin hatası): {e}")
|
|
599
|
+
return
|
|
600
|
+
except OSError as e:
|
|
601
|
+
show_error(f"Dosya okunamadı: {e}")
|
|
602
|
+
return
|
|
603
|
+
|
|
604
|
+
console.print("[info]📥 Gizli test scripti sunucudan alınıyor...[/]")
|
|
605
|
+
try:
|
|
606
|
+
data = api_request("GET", f"/interviews/download/{lib}/{question_id}")
|
|
607
|
+
test_code_b64 = data.get("test_encoded")
|
|
608
|
+
if not test_code_b64:
|
|
609
|
+
show_error("Bu soru için test kodu bulunamadı.")
|
|
610
|
+
return
|
|
611
|
+
test_code = decode_base64_content(test_code_b64)
|
|
612
|
+
except SystemExit:
|
|
613
|
+
return
|
|
614
|
+
except Exception as e:
|
|
615
|
+
show_error(f"Test kodları alınamadı: {e}")
|
|
616
|
+
return
|
|
617
|
+
|
|
618
|
+
import base64 as _b64
|
|
619
|
+
sol_b64 = _b64.b64encode(solution_code.encode("utf-8")).decode("ascii")
|
|
620
|
+
test_b64 = _b64.b64encode(test_code.encode("utf-8")).decode("ascii")
|
|
621
|
+
|
|
622
|
+
runner_content = f"""# -*- coding: utf-8 -*-
|
|
623
|
+
import sys, json, base64
|
|
624
|
+
_sol_code = base64.b64decode("{sol_b64}").decode("utf-8")
|
|
625
|
+
_test_code = base64.b64decode("{test_b64}").decode("utf-8")
|
|
626
|
+
_ns = {{}}
|
|
627
|
+
try:
|
|
628
|
+
exec(compile(_sol_code, "<solution>", "exec"), _ns)
|
|
629
|
+
except SyntaxError as e:
|
|
630
|
+
print(f"SYNTAX_ERROR: {{e}}"); sys.exit(1)
|
|
631
|
+
except Exception as e:
|
|
632
|
+
print(f"RUNTIME_ERROR: {{e}}"); sys.exit(1)
|
|
633
|
+
try:
|
|
634
|
+
exec(compile(_test_code, "<tests>", "exec"), _ns)
|
|
635
|
+
except Exception as e:
|
|
636
|
+
print(f"TEST_LOAD_ERROR: {{e}}"); sys.exit(1)
|
|
637
|
+
passed, failed, errors = 0, 0, []
|
|
638
|
+
for name, func in list(_ns.items()):
|
|
639
|
+
if name.startswith("test_") and callable(func):
|
|
640
|
+
try:
|
|
641
|
+
func(); passed += 1
|
|
642
|
+
except AssertionError as e:
|
|
643
|
+
failed += 1; errors.append(f"{{name}}: {{str(e)[:120]}}")
|
|
644
|
+
except Exception as e:
|
|
645
|
+
failed += 1; errors.append(f"{{name}}: {{type(e).__name__}}: {{str(e)[:120]}}")
|
|
646
|
+
print(f"RESULT_JSON: {{json.dumps({{'passed': passed, 'failed': failed, 'errors': errors}})}}")
|
|
647
|
+
"""
|
|
648
|
+
|
|
649
|
+
runner_file: Optional[str] = None
|
|
650
|
+
try:
|
|
651
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False, encoding="utf-8") as f:
|
|
652
|
+
f.write(runner_content)
|
|
653
|
+
runner_file = f.name
|
|
654
|
+
|
|
655
|
+
# Runner dosyasını sadece owner çalıştırabilir
|
|
656
|
+
_set_mode(Path(runner_file), 0o600)
|
|
657
|
+
|
|
658
|
+
console.print("[info]⏳ Testler çalıştırılıyor...[/]")
|
|
659
|
+
env = os.environ.copy()
|
|
660
|
+
env["PYTHONIOENCODING"] = "utf-8"
|
|
661
|
+
env["PYTHONUTF8"] = "1"
|
|
662
|
+
|
|
663
|
+
result = subprocess.run(
|
|
664
|
+
["python", runner_file],
|
|
665
|
+
capture_output=True, text=True, timeout=15,
|
|
666
|
+
encoding="utf-8", errors="replace", env=env,
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
output = result.stdout.strip()
|
|
670
|
+
json_line = [l for l in output.split("\n") if l.startswith("RESULT_JSON:")]
|
|
671
|
+
|
|
672
|
+
if json_line:
|
|
673
|
+
stats = json.loads(json_line[0].replace("RESULT_JSON: ", ""))
|
|
674
|
+
table = Table(box=ROUNDED, title="🧪 Test Sonuçları")
|
|
675
|
+
table.add_column("Durum", style="bold")
|
|
676
|
+
table.add_column("Sayı", justify="right")
|
|
677
|
+
table.add_row("[green]✅ Başarılı[/]", f"[green]{stats['passed']}[/]")
|
|
678
|
+
table.add_row("[red]❌ Başarısız[/]", f"[red]{stats['failed']}[/]")
|
|
679
|
+
console.print(table)
|
|
680
|
+
|
|
681
|
+
if stats["errors"]:
|
|
682
|
+
console.print("\n[error]Detaylar:[/]")
|
|
683
|
+
for err in stats["errors"]:
|
|
684
|
+
console.print(f" [dim]• {err}[/]")
|
|
685
|
+
|
|
686
|
+
if stats["failed"] == 0 and stats["passed"] > 0:
|
|
687
|
+
show_success("Tüm testler geçti! 🎉")
|
|
688
|
+
console.print(f"[info]Devam: `pymulakat submit {question_id}`[/]")
|
|
689
|
+
else:
|
|
690
|
+
console.print("[warning]⚠️ Bazı testler başarısız oldu.[/]")
|
|
691
|
+
else:
|
|
692
|
+
console.print("[red]❌ Test çıktısı alınamadı.[/]")
|
|
693
|
+
if result.stderr:
|
|
694
|
+
console.print(f"[dim]{result.stderr[:200]}[/]")
|
|
695
|
+
|
|
696
|
+
except subprocess.TimeoutExpired:
|
|
697
|
+
show_error("Testler zaman aşımına uğradı (15sn). Sonsuz döngü var mı?")
|
|
698
|
+
except Exception as e:
|
|
699
|
+
show_error(f"Test çalıştırma hatası: {e}")
|
|
700
|
+
finally:
|
|
701
|
+
if runner_file and Path(runner_file).exists():
|
|
702
|
+
try:
|
|
703
|
+
Path(runner_file).unlink()
|
|
704
|
+
except OSError:
|
|
705
|
+
pass
|
|
706
|
+
console.print("[dim]🧹 Geçici dosyalar temizlendi.[/]")
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
@cli.command("get")
|
|
710
|
+
@click.argument("question_id")
|
|
711
|
+
@click.option("-l", "--lib", required=True, help="Kütüphane adı (zorunlu)")
|
|
712
|
+
@require_login
|
|
713
|
+
def get_question(question_id: str, lib: str):
|
|
714
|
+
"""🔍 Belirli bir sorunun detaylarını görüntüle"""
|
|
715
|
+
show_header(f"📝 Soru #{question_id}", f"Kategori: {lib}")
|
|
716
|
+
data = api_request("GET", f"/interviews/{lib}/{question_id}")
|
|
717
|
+
console.print(Panel(
|
|
718
|
+
Markdown(data.get("question", data.get("description", "Açıklama yok"))),
|
|
719
|
+
title="🎯 Görev", border_style="blue"
|
|
720
|
+
))
|
|
721
|
+
meta_table = Table(show_header=False, box=None, padding=(0, 1))
|
|
722
|
+
meta_table.add_row("[accent]Zorluk:[/]", data.get("difficulty", "Belirtilmemiş"))
|
|
723
|
+
meta_table.add_row("[accent]Konu:[/]", data.get("topic", "Belirtilmemiş"))
|
|
724
|
+
console.print(meta_table)
|
|
725
|
+
if data.get("hints"):
|
|
726
|
+
console.print("\n[info]💡 İpuçları:[/]")
|
|
727
|
+
for i, hint in enumerate(data["hints"], 1):
|
|
728
|
+
console.print(f" [secondary]{i}.[/] {hint}")
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
@cli.command("download")
|
|
732
|
+
@click.option("-l", "--lib", default=None, help="Kütüphane filtresi")
|
|
733
|
+
@require_login
|
|
734
|
+
def download_interactive(lib: Optional[str]):
|
|
735
|
+
"""📥 Soru listesinden seçim yap ve indir"""
|
|
736
|
+
show_header("📥 Soru Seçimi ve İndirme", "Listeden seçim yapın")
|
|
737
|
+
|
|
738
|
+
params = {"lib": lib} if lib else {}
|
|
739
|
+
try:
|
|
740
|
+
data = api_request("GET", "/interviews/", params=params)
|
|
741
|
+
except SystemExit:
|
|
742
|
+
return
|
|
743
|
+
|
|
744
|
+
if not data:
|
|
745
|
+
show_error("Bu kriterlerde soru bulunamadı.")
|
|
746
|
+
return
|
|
747
|
+
|
|
748
|
+
console.print("[info]📋 Mevcut Sorular:[/]")
|
|
749
|
+
table = Table(box=ROUNDED, show_header=True, header_style="bold blue", show_lines=True)
|
|
750
|
+
table.add_column("#", style="cyan", width=4)
|
|
751
|
+
table.add_column("ID", style="dim", width=6)
|
|
752
|
+
table.add_column("Konu", style="white")
|
|
753
|
+
table.add_column("Kütüphane", style="magenta")
|
|
754
|
+
table.add_column("Zorluk", style="yellow")
|
|
755
|
+
for i, item in enumerate(data, 1):
|
|
756
|
+
table.add_row(
|
|
757
|
+
str(i), item["id"],
|
|
758
|
+
item.get("topic", "Başlıksız"),
|
|
759
|
+
item.get("lib", "unknown"),
|
|
760
|
+
item.get("level", item.get("difficulty", "-")),
|
|
761
|
+
)
|
|
762
|
+
console.print(table)
|
|
763
|
+
|
|
764
|
+
max_idx = len(data)
|
|
765
|
+
choice = click.prompt(f"\n📌 İndirmek istediğiniz sorunun numarası (1-{max_idx})", type=int)
|
|
766
|
+
if not (1 <= choice <= max_idx):
|
|
767
|
+
show_error("Geçersiz seçim!")
|
|
768
|
+
return
|
|
769
|
+
|
|
770
|
+
selected = data[choice - 1]
|
|
771
|
+
q_id = selected["id"]
|
|
772
|
+
q_lib = selected["lib"]
|
|
773
|
+
output_file = get_next_solution_filename()
|
|
774
|
+
|
|
775
|
+
console.print(f"\n[info]⬇️ İndiriliyor: {q_lib.upper()} / #{q_id}[/]")
|
|
776
|
+
with Progress(SpinnerColumn(), TextColumn("[primary]Kodlar hazırlanıyor...[/]"), console=console) as p:
|
|
777
|
+
p.add_task("", total=None)
|
|
778
|
+
resp = api_request("GET", f"/interviews/download/{q_lib}/{q_id}")
|
|
779
|
+
|
|
780
|
+
perms = load_permission_config()
|
|
781
|
+
|
|
782
|
+
# Starter code
|
|
783
|
+
if resp.get("starter_encoded"):
|
|
784
|
+
starter = decode_base64_content(resp["starter_encoded"])
|
|
785
|
+
try:
|
|
786
|
+
_atomic_write(Path(output_file), starter)
|
|
787
|
+
_set_mode(Path(output_file), perms["solution_file"])
|
|
788
|
+
console.print(f"[success]✓[/] Çözüm dosyası: [cyan]{Path(output_file).resolve()}[/]")
|
|
789
|
+
except (PermissionError, OSError) as e:
|
|
790
|
+
show_error(str(e), "Dosya Yazılamadı")
|
|
791
|
+
return
|
|
792
|
+
|
|
793
|
+
# Gizli test dosyası
|
|
794
|
+
if resp.get("test_encoded"):
|
|
795
|
+
tests = decode_base64_content(resp["test_encoded"])
|
|
796
|
+
test_path = Path(f".test_{q_id}.py")
|
|
797
|
+
try:
|
|
798
|
+
_atomic_write(test_path, tests)
|
|
799
|
+
_set_mode(test_path, perms["test_file"])
|
|
800
|
+
except (PermissionError, OSError) as e:
|
|
801
|
+
show_error(str(e), "Test Dosyası Yazılamadı")
|
|
802
|
+
return
|
|
803
|
+
|
|
804
|
+
try:
|
|
805
|
+
save_interview_meta(q_id, q_lib, selected.get("topic", ""), output_file)
|
|
806
|
+
console.print("[dim]📌 Meta verisi kaydedildi.[/]")
|
|
807
|
+
except (PermissionError, OSError) as e:
|
|
808
|
+
console.print(f"[warning]⚠️ Meta kaydedilemedi: {e}[/]")
|
|
809
|
+
|
|
810
|
+
try:
|
|
811
|
+
subprocess.Popen(["code", output_file], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
812
|
+
console.print(f"[success]✓[/] VSCode açılıyor: [cyan]{output_file}[/]")
|
|
813
|
+
except FileNotFoundError:
|
|
814
|
+
console.print("[dim]💡 VSCode bulunamadı, dosyayı manuel açabilirsiniz.[/]")
|
|
815
|
+
|
|
816
|
+
console.print(Panel(
|
|
817
|
+
"[info]📌 Sonraki Adımlar:[/]\n"
|
|
818
|
+
f" 1. [cyan]{output_file}[/] dosyasını düzenle\n"
|
|
819
|
+
f" 2. Lokal test: [cyan]pymulakat test {q_id}[/]\n"
|
|
820
|
+
f" 3. Sunucuya gönder: [cyan]pymulakat submit {q_id}[/]",
|
|
821
|
+
title="🚀 Devam Et", border_style="green"
|
|
822
|
+
))
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
# ═══════════════════════════════════════════════════════════
|
|
826
|
+
# 📈 PROGRESS KOMUTU
|
|
827
|
+
# ═══════════════════════════════════════════════════════════
|
|
828
|
+
|
|
829
|
+
@cli.command("progress")
|
|
830
|
+
@click.option("-l", "--lib", required=True, help="Kütüphane adı")
|
|
831
|
+
@click.option("-j", "--json", "as_json", is_flag=True, help="JSON formatında çıktı")
|
|
832
|
+
@require_login
|
|
833
|
+
def show_progress(lib: str, as_json: bool):
|
|
834
|
+
"""📈 Kullanıcı ilerleme durumunu görüntüle"""
|
|
835
|
+
show_header("📈 İlerleme Raporu", f"Kategori: {lib.upper()}")
|
|
836
|
+
data = api_request("GET", "/auth/my-progress", params={"lib": lib})
|
|
837
|
+
|
|
838
|
+
if as_json:
|
|
839
|
+
console.print_json(json.dumps(data, indent=2, ensure_ascii=False))
|
|
840
|
+
return
|
|
841
|
+
|
|
842
|
+
summary = Table(box=ROUNDED, show_header=False)
|
|
843
|
+
summary.add_row("[accent]📦 Toplam Soru:[/]", f"[bold]{data.get('total', 0)}[/]")
|
|
844
|
+
completed = data.get("completed", [])
|
|
845
|
+
summary.add_row("[success]✅ Çözülen:[/]", f"[bold green]{len(completed)}[/]")
|
|
846
|
+
if completed:
|
|
847
|
+
accuracy = len(completed) / data.get("total", 1) * 100
|
|
848
|
+
summary.add_row("[info]🎯 Başarı Oranı:[/]", f"[bold cyan]{accuracy:.1f}%[/]")
|
|
849
|
+
console.print(summary)
|
|
850
|
+
|
|
851
|
+
if completed:
|
|
852
|
+
console.print("\n[info]🔹 Tamamlanan Sorular:[/]")
|
|
853
|
+
for item in completed:
|
|
854
|
+
attempt = item.get("attempt", {})
|
|
855
|
+
time_str = _format_duration(attempt.get("time_taken", 0))
|
|
856
|
+
points = attempt.get("points_gained", 0)
|
|
857
|
+
console.print(
|
|
858
|
+
f" [success]✓[/] [cyan]#{item['id']}[/] {item.get('topic')} "
|
|
859
|
+
f"[secondary]→[/] ⏱️ {time_str} | 🏆 +{points} puan"
|
|
860
|
+
)
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
# ═══════════════════════════════════════════════════════════
|
|
864
|
+
# 🔧 HELPER FONKSIYONLAR
|
|
865
|
+
# ═══════════════════════════════════════════════════════════
|
|
866
|
+
|
|
867
|
+
def _get_user_id_from_token() -> Optional[str]:
|
|
868
|
+
import jwt
|
|
869
|
+
token = get_token()
|
|
870
|
+
if not token:
|
|
871
|
+
return None
|
|
872
|
+
try:
|
|
873
|
+
payload = jwt.decode(token, options={"verify_signature": False})
|
|
874
|
+
return payload.get("sub")
|
|
875
|
+
except Exception:
|
|
876
|
+
return None
|
|
877
|
+
|
|
878
|
+
|
|
879
|
+
def _format_duration(seconds: float) -> str:
|
|
880
|
+
s = int(seconds or 0)
|
|
881
|
+
h, remainder = divmod(s, 3600)
|
|
882
|
+
m, sec = divmod(remainder, 60)
|
|
883
|
+
if h: return f"{h}sa {m}dk"
|
|
884
|
+
if m: return f"{m}dk {sec}sn"
|
|
885
|
+
return f"{sec}sn"
|
|
886
|
+
|
|
887
|
+
|
|
888
|
+
# ═══════════════════════════════════════════════════════════
|
|
889
|
+
# 🚀 ENTRY POINT
|
|
890
|
+
# ═══════════════════════════════════════════════════════════
|
|
891
|
+
|
|
892
|
+
if __name__ == "__main__":
|
|
893
|
+
console.print(Panel.fit(
|
|
894
|
+
"[bold primary]🎓 PyMulakat CLI[/] [secondary]v1.0.0[/]\n"
|
|
895
|
+
"[dim]Profesyonel Python Mülakat Platformu[/]",
|
|
896
|
+
box=ROUNDED, border_style="blue"
|
|
897
|
+
))
|
|
898
|
+
if not check_interpreter("python"):
|
|
899
|
+
console.print("[warning]⚠ Bazı komutlar çalışmayabilir.[/]")
|
|
900
|
+
console.print()
|
|
901
|
+
cli(obj={})
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/pymulakat/__init__.py
|
|
4
|
+
src/pymulakat/main.py
|
|
5
|
+
src/pymulakat.egg-info/PKG-INFO
|
|
6
|
+
src/pymulakat.egg-info/SOURCES.txt
|
|
7
|
+
src/pymulakat.egg-info/dependency_links.txt
|
|
8
|
+
src/pymulakat.egg-info/entry_points.txt
|
|
9
|
+
src/pymulakat.egg-info/requires.txt
|
|
10
|
+
src/pymulakat.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pymulakat
|