pyupdator 0.1.0__py3-none-any.whl

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.
Files changed (42) hide show
  1. examples/basic_usage.py +66 -0
  2. pyupdator/__init__.py +410 -0
  3. pyupdator/core/__init__.py +74 -0
  4. pyupdator/core/backend_manager.py +264 -0
  5. pyupdator/core/downloaders/__init__.py +26 -0
  6. pyupdator/core/downloaders/aria2_dl.py +281 -0
  7. pyupdator/core/downloaders/base.py +108 -0
  8. pyupdator/core/downloaders/http_dl.py +832 -0
  9. pyupdator/core/downloaders/http_zip_delta.py +2070 -0
  10. pyupdator/core/downloaders/libtorrent_dl.py +383 -0
  11. pyupdator/core/downloaders/manual_dl.py +141 -0
  12. pyupdator/core/downloaders/qbittorrent_dl.py +935 -0
  13. pyupdator/core/downloaders/transmission_dl.py +154 -0
  14. pyupdator/core/downloaders/utorrent_dl.py +245 -0
  15. pyupdator/core/torrent.py +1103 -0
  16. pyupdator/core/zip_engine.py +630 -0
  17. pyupdator/utils/__init__.py +38 -0
  18. pyupdator/utils/helpers.py +186 -0
  19. pyupdator-0.1.0.dist-info/METADATA +165 -0
  20. pyupdator-0.1.0.dist-info/RECORD +42 -0
  21. pyupdator-0.1.0.dist-info/WHEEL +5 -0
  22. pyupdator-0.1.0.dist-info/entry_points.txt +2 -0
  23. pyupdator-0.1.0.dist-info/top_level.txt +4 -0
  24. pyupdator_gui/__init__.py +52 -0
  25. pyupdator_gui/main_window.py +1072 -0
  26. tests/cdc_test_server.py +211 -0
  27. tests/test_cdc_delta.py +405 -0
  28. tests/test_comprehensive.py +240 -0
  29. tests/test_final_qbittorrent_fix.py +171 -0
  30. tests/test_html_manifest_fallback.py +474 -0
  31. tests/test_libtorrent_virtual.py +292 -0
  32. tests/test_qbittorrent.py +60 -0
  33. tests/test_qbittorrent_api.py +73 -0
  34. tests/test_qbittorrent_endpoints.py +63 -0
  35. tests/test_qbittorrent_fix.py +184 -0
  36. tests/test_qbittorrent_fix_fixed.py +184 -0
  37. tests/test_qbittorrent_info.py +39 -0
  38. tests/test_qbittorrent_new_logic.py +168 -0
  39. tests/test_qbittorrent_pause.py +48 -0
  40. tests/test_zip_block_delta.py +368 -0
  41. tests/test_zip_delta.py +329 -0
  42. tests/test_zip_root_detect.py +257 -0
@@ -0,0 +1,66 @@
1
+ """
2
+ examples/basic_usage.py — GUI-free example showing how to use pyupdator.
3
+
4
+ This is the canonical example for a game launcher or other host project.
5
+
6
+ pip install pyupdator
7
+
8
+ Usage:
9
+ python examples/basic_usage.py
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ import sys
16
+ from pathlib import Path
17
+
18
+ # Ensure the local pyupdator package is importable when running from the repo.
19
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
20
+
21
+ from pyupdator import Updater, UpdaterError, human_bytes
22
+
23
+
24
+ def progress_callback(done: int, total: int, message: str = "") -> None:
25
+ """Host-provided progress callback."""
26
+ if total:
27
+ pct = done / total * 100
28
+ print(f"[{pct:6.2f}%] {message} ({human_bytes(done)} / {human_bytes(total)})")
29
+ else:
30
+ print(f"{message} ({human_bytes(done)})")
31
+
32
+
33
+ def main() -> int:
34
+ logging.basicConfig(
35
+ level=logging.INFO,
36
+ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
37
+ )
38
+
39
+ # The host launcher decides install_dir and source.
40
+ install_dir = Path("C:/Oyun") # <-- change to your game folder
41
+ source = "oyun.torrent" # <-- change to your .torrent or HTTP URL
42
+
43
+ try:
44
+ updater = Updater(install_dir=str(install_dir), source=source)
45
+
46
+ # 1) Analyze what needs to be downloaded / verified.
47
+ plan = updater.analyze()
48
+ print("Analiz tamamlandı. Plan:", plan)
49
+
50
+ # 2) Apply the update, reporting progress back to the host.
51
+ updater.update(progress_callback=progress_callback)
52
+ print("Güncelleme tamamlandı.")
53
+
54
+ # 3) (Optional) verify the installed files against the torrent.
55
+ result = updater.verify()
56
+ print("Doğrulama sonucu:", result)
57
+
58
+ except UpdaterError as exc:
59
+ print(f"Hata: {exc}", file=sys.stderr)
60
+ return 1
61
+
62
+ return 0
63
+
64
+
65
+ if __name__ == "__main__":
66
+ raise SystemExit(main())
pyupdator/__init__.py ADDED
@@ -0,0 +1,410 @@
1
+ """
2
+ pyupdator — Delta updater for games (Torrent and HTTP Zip support).
3
+
4
+ Public API:
5
+
6
+ from pyupdator import Updater
7
+
8
+ updater = Updater(install_dir="C:/Oyun", source="oyun.torrent")
9
+ plan = updater.analyze()
10
+ updater.update(progress_callback=my_func)
11
+
12
+ The ``pyupdator`` package contains NO tkinter / customtkinter / gui imports.
13
+ GUI code lives in the separate ``pyupdator_gui`` package.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import logging
19
+ from pathlib import Path
20
+ from typing import Any, Callable, Dict, Optional, Union
21
+
22
+ from .core import (
23
+ TorrentParser,
24
+ PieceMap,
25
+ DeltaPlan,
26
+ DeltaAnalyzer,
27
+ DeltaApplier,
28
+ Verifier,
29
+ TorrentError,
30
+ NeedDownloadError,
31
+ ZipDeltaEngine,
32
+ ZipEntry,
33
+ ZipIndex,
34
+ ZipDeltaError,
35
+ HttpRangeClient,
36
+ chunk_file,
37
+ generate_manifest,
38
+ manifest_stats,
39
+ BackendManager,
40
+ BackendInfo,
41
+ BackendScanResult,
42
+ BackendError,
43
+ PIECE_LEVEL,
44
+ FILE_LEVEL,
45
+ CHUNK_LEVEL,
46
+ AUTO_ORDER,
47
+ scan_backends,
48
+ get_backend_class,
49
+ )
50
+ from .utils import (
51
+ human_bytes,
52
+ sha1_hex,
53
+ sha256_file,
54
+ safe_join,
55
+ list_files,
56
+ read_json,
57
+ write_json,
58
+ load_config,
59
+ save_config,
60
+ UpdaterError,
61
+ DEFAULT_CONFIG,
62
+ CONFIG_PATH,
63
+ )
64
+
65
+ __version__ = "0.1.0"
66
+
67
+ __all__ = [
68
+ "Updater",
69
+ "UpdaterError",
70
+ "TorrentError",
71
+ "NeedDownloadError",
72
+ "ZipDeltaError",
73
+ "BackendError",
74
+ # core classes
75
+ "TorrentParser",
76
+ "PieceMap",
77
+ "DeltaPlan",
78
+ "DeltaAnalyzer",
79
+ "DeltaApplier",
80
+ "Verifier",
81
+ "ZipDeltaEngine",
82
+ "ZipEntry",
83
+ "ZipIndex",
84
+ "HttpRangeClient",
85
+ "BackendManager",
86
+ "BackendInfo",
87
+ "BackendScanResult",
88
+ # constants
89
+ "PIECE_LEVEL",
90
+ "FILE_LEVEL",
91
+ "CHUNK_LEVEL",
92
+ "AUTO_ORDER",
93
+ # functions
94
+ "chunk_file",
95
+ "generate_manifest",
96
+ "manifest_stats",
97
+ "scan_backends",
98
+ "get_backend_class",
99
+ "human_bytes",
100
+ "sha1_hex",
101
+ "sha256_file",
102
+ "safe_join",
103
+ "list_files",
104
+ "read_json",
105
+ "write_json",
106
+ "load_config",
107
+ "save_config",
108
+ # version
109
+ "__version__",
110
+ ]
111
+
112
+ # ---------------------------------------------------------------------------
113
+ # Paket düzeyinde logger; host launcher kendi handler'ını ekleyebilir.
114
+ # ---------------------------------------------------------------------------
115
+
116
+ logger = logging.getLogger("pyupdator")
117
+ _logger_initialized = False
118
+
119
+
120
+ def _ensure_logging() -> None:
121
+ """Varsayılan logging yapılandırmasını tek seferlik kurar."""
122
+ global _logger_initialized
123
+ if _logger_initialized:
124
+ return
125
+ _logger_initialized = True
126
+ logging.basicConfig(
127
+ level=logging.INFO,
128
+ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
129
+ )
130
+
131
+
132
+ def set_log_level(level: Union[int, str]) -> None:
133
+ """Kütüphane genel log seviyesini ayarlar."""
134
+ _ensure_logging()
135
+ logger.setLevel(level)
136
+
137
+
138
+ def add_log_handler(handler: logging.Handler) -> None:
139
+ """Host launcher'ın kendi log handler'ını ekler (ör. GUI konsolu)."""
140
+ _ensure_logging()
141
+ logger.addHandler(handler)
142
+
143
+
144
+ def remove_log_handler(handler: logging.Handler) -> None:
145
+ """Önceden eklenen handler'ı çıkarır."""
146
+ logger.removeHandler(handler)
147
+
148
+
149
+ # ---------------------------------------------------------------------------
150
+ # Updater — Ana API sınıfı
151
+ # ---------------------------------------------------------------------------
152
+
153
+
154
+ class Updater:
155
+ """
156
+ Ana güncelleme motoru.
157
+
158
+ Sözleşme:
159
+
160
+ from pyupdator import Updater
161
+ updater = Updater(install_dir="C:/Oyun", source="oyun.torrent")
162
+ plan = updater.analyze()
163
+ updater.update(progress_callback=my_func)
164
+
165
+ ``source`` hem .torrent dosya yolu hem de http/https URL'i olabilir.
166
+ """
167
+
168
+ def __init__(
169
+ self,
170
+ install_dir: Union[str, Path],
171
+ source: Union[str, Path],
172
+ torrent_root: Optional[Union[str, Path]] = None,
173
+ source_dir: Optional[Union[str, Path]] = None,
174
+ allow_delete: bool = False,
175
+ do_backup: bool = True,
176
+ logger_cb: Optional[Callable[[str], None]] = None,
177
+ config: Optional[Dict[str, Any]] = None,
178
+ ):
179
+ self.install_dir = Path(install_dir).resolve()
180
+ self.source = str(source)
181
+ self.torrent_root = Path(torrent_root).resolve() if torrent_root else None
182
+ self.source_dir = Path(source_dir).resolve() if source_dir else None
183
+ self.allow_delete = allow_delete
184
+ self.do_backup = do_backup
185
+ self.logger_cb = logger_cb or (lambda msg: logger.info(msg))
186
+
187
+ self.config = config or load_config()
188
+
189
+ # Durum
190
+ self._plan: Optional[DeltaPlan] = None
191
+ _ensure_logging()
192
+
193
+ # Kaynak türünü belirle (http/https ise ZIP delta / CDC, değilse torrent)
194
+ self._is_http = self.source.startswith(("http://", "https://"))
195
+
196
+ # ------------------------------------------------------------------
197
+ # Günlükleme
198
+ # ------------------------------------------------------------------
199
+
200
+ def _log(self, msg: str) -> None:
201
+ self.logger_cb(msg)
202
+ logger.info(msg)
203
+
204
+ # ------------------------------------------------------------------
205
+ # Analyze
206
+ # ------------------------------------------------------------------
207
+
208
+ def analyze(self) -> Dict[str, Any]:
209
+ """
210
+ Kurulu klasörü kaynakla karşılaştırır ve güncelleme planı üretir.
211
+
212
+ Döner: plan sözlüğü (DeltaPlan.to_dict()).
213
+ """
214
+ if self._is_http:
215
+ return self._analyze_http()
216
+ return self._analyze_torrent()
217
+
218
+ def _analyze_torrent(self) -> Dict[str, Any]:
219
+ analyzer = DeltaAnalyzer(
220
+ installed_dir=self.install_dir,
221
+ torrent_path=self.source,
222
+ torrent_root=self.torrent_root,
223
+ logger=self._log,
224
+ )
225
+ plan = analyzer.analyze()
226
+ self._plan = plan
227
+ return plan.to_dict()
228
+
229
+ def _analyze_http(self) -> Dict[str, Any]:
230
+ """
231
+ HTTP kaynağı (ZIP) için analiz.
232
+
233
+ ZIP delta motoru kullanılarak uzak ZIP'in Central Directory'si
234
+ okunur ve yerel klasörle karşılaştırılır.
235
+ """
236
+ engine = ZipDeltaEngine(
237
+ url=self.source,
238
+ local_dir=self.install_dir,
239
+ logger_cb=self._log,
240
+ )
241
+ index = engine.fetch_index()
242
+ changed = engine.compare(index)
243
+
244
+ plan: Dict[str, Any] = {
245
+ "mode": "ZIP_DELTA",
246
+ "url": self.source,
247
+ "install_dir": str(self.install_dir),
248
+ "total_entries": len(index.entries),
249
+ "to_download": [e.to_dict() for e in changed],
250
+ "unchanged": [
251
+ e.to_dict() for e in index.entries
252
+ if not e.is_dir and e not in changed
253
+ ],
254
+ "stats": {
255
+ "total_compressed": index.total_compressed,
256
+ "changed_count": len(changed),
257
+ },
258
+ }
259
+ self._plan = plan # type: ignore[assignment]
260
+ self._log(
261
+ f"HTTP ZIP analiz tamam: {len(changed)} dosya değişecek / "
262
+ f"{index.total_entries} toplam giriş"
263
+ )
264
+ return plan
265
+
266
+ # ------------------------------------------------------------------
267
+ # Update
268
+ # ------------------------------------------------------------------
269
+
270
+ def update(
271
+ self,
272
+ progress_callback: Optional[Callable[[int, int, str], None]] = None,
273
+ allow_delete: Optional[bool] = None,
274
+ do_backup: Optional[bool] = None,
275
+ dry_run: bool = False,
276
+ ) -> Dict[str, Any]:
277
+ """
278
+ Mevcut planı uygular.
279
+
280
+ Eğer ``analyze()`` henüz çağrılmadıysa otomatik olarak çağrılır.
281
+
282
+ Döner: uygulama özeti sözlüğü.
283
+ """
284
+ if self._plan is None:
285
+ self.analyze()
286
+
287
+ if self._is_http:
288
+ return self._update_http(progress_callback=progress_callback)
289
+
290
+ return self._update_torrent(
291
+ progress_callback=progress_callback,
292
+ allow_delete=allow_delete if allow_delete is not None else self.allow_delete,
293
+ do_backup=do_backup if do_backup is not None else self.do_backup,
294
+ dry_run=dry_run,
295
+ )
296
+
297
+ def _update_torrent(
298
+ self,
299
+ progress_callback: Optional[Callable[[int, int, str], None]] = None,
300
+ allow_delete: bool = False,
301
+ do_backup: bool = True,
302
+ dry_run: bool = False,
303
+ ) -> Dict[str, Any]:
304
+ plan = self._plan
305
+ assert plan is not None
306
+
307
+ def _cb(done: int, total: int, msg: str) -> None:
308
+ if progress_callback:
309
+ progress_callback(done, total, msg)
310
+ self._log(f"{msg} ({done}/{total})")
311
+
312
+ applier = DeltaApplier(
313
+ installed_dir=self.install_dir,
314
+ torrent_path=self.source,
315
+ source_dir=self.source_dir,
316
+ logger=self._log,
317
+ )
318
+ result = applier.apply_plan(
319
+ plan=plan,
320
+ allow_delete=allow_delete,
321
+ do_backup=do_backup,
322
+ dry_run=dry_run,
323
+ )
324
+ _cb(1, 1, "Güncelleme tamamlandı")
325
+ return result
326
+
327
+ def _update_http(
328
+ self,
329
+ progress_callback: Optional[Callable[[int, int, str], None]] = None,
330
+ ) -> Dict[str, Any]:
331
+ plan = self._plan
332
+ assert plan is not None and plan.get("mode") == "ZIP_DELTA"
333
+
334
+ engine = ZipDeltaEngine(
335
+ url=self.source,
336
+ local_dir=self.install_dir,
337
+ logger_cb=self._log,
338
+ )
339
+ index = engine.fetch_index()
340
+ changed_entries = [
341
+ ZipEntry(
342
+ name=e["name"],
343
+ compress_size=e.get("compress_size", 0),
344
+ file_size=e.get("file_size", 0),
345
+ crc32=int(e.get("crc32", 0), 16) if isinstance(e.get("crc32"), str) else int(e.get("crc32", 0)),
346
+ compress_type=e.get("compress_type", 0),
347
+ flag_bits=e.get("flag_bits", 0),
348
+ local_header_offset=e.get("local_header_offset", 0),
349
+ )
350
+ for e in plan.get("to_download", [])
351
+ ]
352
+
353
+ def _cb(done: int, total: int, msg: str) -> None:
354
+ if progress_callback:
355
+ progress_callback(done, total, msg)
356
+ self._log(f"{msg} ({done}/{total})")
357
+
358
+ result = engine.apply(
359
+ zip_index=index,
360
+ changed_entries=changed_entries,
361
+ progress_cb=_cb,
362
+ )
363
+ _cb(1, 1, "HTTP ZIP güncelleme tamamlandı")
364
+ return result
365
+
366
+ # ------------------------------------------------------------------
367
+ # Verify
368
+ # ------------------------------------------------------------------
369
+
370
+ def verify(
371
+ self,
372
+ against_dir: Optional[Union[str, Path]] = None,
373
+ backup_manifest: Optional[Dict[str, Any]] = None,
374
+ root_override: Optional[Union[str, Path]] = None,
375
+ ) -> Dict[str, Any]:
376
+ """
377
+ Kurulu klasörü .torrent ile doğrular.
378
+
379
+ Döner: {"ok": bool, "errors": [...], ...}
380
+ """
381
+ if self._is_http:
382
+ raise UpdaterError("Doğrulama sadece .torrent kaynağı için desteklenir.")
383
+
384
+ verifier = Verifier(
385
+ installed_dir=self.install_dir,
386
+ torrent_path=self.source,
387
+ logger=self._log,
388
+ )
389
+ return verifier.verify(
390
+ against_dir=against_dir,
391
+ backup_manifest=backup_manifest,
392
+ root_override=root_override,
393
+ )
394
+
395
+ # ------------------------------------------------------------------
396
+ # Plan accessors
397
+ # ------------------------------------------------------------------
398
+
399
+ @property
400
+ def plan(self) -> Optional[Dict[str, Any]]:
401
+ """Son analyze() sonucu (dict) veya None."""
402
+ if self._plan is None:
403
+ return None
404
+ if isinstance(self._plan, DeltaPlan):
405
+ return self._plan.to_dict()
406
+ return self._plan
407
+
408
+ @property
409
+ def is_http(self) -> bool:
410
+ return self._is_http
@@ -0,0 +1,74 @@
1
+ """
2
+ pyupdator.core — Güncelleme motoru.
3
+
4
+ Paket içerisinde:
5
+ torrent.py : .torrent parse, recheck, delta planı, in-place uygulama,
6
+ doğrulama
7
+ zip_engine.py : HTTP ZIP delta + CDC chunker
8
+ backend_manager.py: indirme backend tespiti ve seçimi
9
+ """
10
+
11
+ from .torrent import (
12
+ TorrentParser,
13
+ PieceMap,
14
+ DeltaPlan,
15
+ DeltaAnalyzer,
16
+ DeltaApplier,
17
+ Verifier,
18
+ TorrentError,
19
+ NeedDownloadError,
20
+ )
21
+ from .zip_engine import (
22
+ ZipDeltaEngine,
23
+ ZipEntry,
24
+ ZipIndex,
25
+ ZipDeltaError,
26
+ HttpRangeClient,
27
+ chunk_file,
28
+ generate_manifest,
29
+ manifest_stats,
30
+ )
31
+ from .backend_manager import (
32
+ BackendManager,
33
+ BackendInfo,
34
+ BackendScanResult,
35
+ BackendError,
36
+ PIECE_LEVEL,
37
+ FILE_LEVEL,
38
+ CHUNK_LEVEL,
39
+ AUTO_ORDER,
40
+ scan_backends,
41
+ get_backend_class,
42
+ )
43
+
44
+ __all__ = [
45
+ # torrent
46
+ "TorrentParser",
47
+ "PieceMap",
48
+ "DeltaPlan",
49
+ "DeltaAnalyzer",
50
+ "DeltaApplier",
51
+ "Verifier",
52
+ "TorrentError",
53
+ "NeedDownloadError",
54
+ # zip_engine
55
+ "ZipDeltaEngine",
56
+ "ZipEntry",
57
+ "ZipIndex",
58
+ "ZipDeltaError",
59
+ "HttpRangeClient",
60
+ "chunk_file",
61
+ "generate_manifest",
62
+ "manifest_stats",
63
+ # backend_manager
64
+ "BackendManager",
65
+ "BackendInfo",
66
+ "BackendScanResult",
67
+ "BackendError",
68
+ "PIECE_LEVEL",
69
+ "FILE_LEVEL",
70
+ "CHUNK_LEVEL",
71
+ "AUTO_ORDER",
72
+ "scan_backends",
73
+ "get_backend_class",
74
+ ]