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.
@@ -0,0 +1,9 @@
1
+ Metadata-Version: 2.4
2
+ Name: pymulakat
3
+ Version: 1.0.0
4
+ Summary: Profesyonel Python Mülakat Platformu CLI
5
+ Requires-Python: >=3.8
6
+ Requires-Dist: click>=8.0
7
+ Requires-Dist: rich>=13.0
8
+ Requires-Dist: requests>=2.28
9
+ Requires-Dist: PyJWT>=2.0
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"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
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,9 @@
1
+ Metadata-Version: 2.4
2
+ Name: pymulakat
3
+ Version: 1.0.0
4
+ Summary: Profesyonel Python Mülakat Platformu CLI
5
+ Requires-Python: >=3.8
6
+ Requires-Dist: click>=8.0
7
+ Requires-Dist: rich>=13.0
8
+ Requires-Dist: requests>=2.28
9
+ Requires-Dist: PyJWT>=2.0
@@ -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,2 @@
1
+ [console_scripts]
2
+ pymulakat = pymulakat.main:cli
@@ -0,0 +1,4 @@
1
+ click>=8.0
2
+ rich>=13.0
3
+ requests>=2.28
4
+ PyJWT>=2.0
@@ -0,0 +1 @@
1
+ pymulakat