gog-cli 0.2.1__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.
gog_cli/state.py ADDED
@@ -0,0 +1,193 @@
1
+ """Application state paths and JSON file helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from collections.abc import Mapping
8
+ from contextlib import suppress
9
+ from dataclasses import dataclass
10
+ from datetime import UTC, datetime, timedelta
11
+ from pathlib import Path
12
+ from tempfile import NamedTemporaryFile
13
+ from typing import Any, Literal
14
+
15
+ APP_DIR_NAME = "gog-cli"
16
+
17
+ CacheStatus = Literal["fresh", "stale"]
18
+
19
+
20
+ class StateError(Exception):
21
+ """Base class for application state errors."""
22
+
23
+
24
+ class StateFileMissingError(StateError):
25
+ """Raised when an expected state/cache file does not exist."""
26
+
27
+
28
+ class StateFileCorruptError(StateError):
29
+ """Raised when a JSON state/cache file cannot be decoded."""
30
+
31
+
32
+ class StateFileInvalidError(StateError):
33
+ """Raised when a state/cache file has an unsupported shape."""
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class AppRoots:
38
+ """Root directories owned by the application."""
39
+
40
+ config: Path
41
+ cache: Path
42
+ state: Path
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class AppPaths:
47
+ """Expected application-owned files and directories."""
48
+
49
+ roots: AppRoots
50
+ config_file: Path
51
+ library_cache: Path
52
+ downloads_cache_dir: Path
53
+ session_state: Path
54
+ cookies_file: Path
55
+ schema_file: Path
56
+ locks_dir: Path
57
+
58
+ def download_cache(self, product_id: str) -> Path:
59
+ """Return the per-game download-metadata cache path."""
60
+ if not product_id or "/" in product_id or "\x00" in product_id:
61
+ raise ValueError("product_id must be a non-empty path segment")
62
+ return self.downloads_cache_dir / f"{product_id}.json"
63
+
64
+
65
+ @dataclass(frozen=True)
66
+ class CacheReadResult:
67
+ """Read cache data plus freshness status."""
68
+
69
+ data: Mapping[str, Any]
70
+ status: CacheStatus
71
+
72
+
73
+ def resolve_app_roots(env: Mapping[str, str] | None = None) -> AppRoots:
74
+ """Resolve app config/cache/state roots using Linux/XDG conventions."""
75
+ values = os.environ if env is None else env
76
+ home = Path(values.get("HOME") or str(Path.home())).expanduser()
77
+ config_base = _base_dir(values, "XDG_CONFIG_HOME", home / ".config")
78
+ cache_base = _base_dir(values, "XDG_CACHE_HOME", home / ".cache")
79
+ state_base = _base_dir(values, "XDG_DATA_HOME", home / ".local" / "share")
80
+ return AppRoots(
81
+ config=config_base / APP_DIR_NAME,
82
+ cache=cache_base / APP_DIR_NAME,
83
+ state=state_base / APP_DIR_NAME,
84
+ )
85
+
86
+
87
+ def resolve_app_paths(env: Mapping[str, str] | None = None) -> AppPaths:
88
+ """Resolve expected application-owned paths."""
89
+ roots = resolve_app_roots(env)
90
+ return AppPaths(
91
+ roots=roots,
92
+ config_file=roots.config / "config.toml",
93
+ library_cache=roots.cache / "library.json",
94
+ downloads_cache_dir=roots.cache / "downloads",
95
+ session_state=roots.state / "session.json",
96
+ cookies_file=roots.state / "auth" / "cookies.txt",
97
+ schema_file=roots.state / "schema.json",
98
+ locks_dir=roots.state / "locks",
99
+ )
100
+
101
+
102
+ def read_json_file(path: Path) -> Any:
103
+ """Read a JSON file, raising typed errors for missing or corrupt state."""
104
+ try:
105
+ with path.open("r", encoding="utf-8") as handle:
106
+ return json.load(handle)
107
+ except FileNotFoundError as exc:
108
+ raise StateFileMissingError(f"state file is missing: {path}") from exc
109
+ except json.JSONDecodeError as exc:
110
+ raise StateFileCorruptError(f"state file is corrupt JSON: {path}") from exc
111
+
112
+
113
+ def write_json_file_atomic(path: Path, data: Any) -> None:
114
+ """Write JSON atomically by replacing through a temp file in the same dir."""
115
+ path.parent.mkdir(parents=True, exist_ok=True)
116
+ temp_name = ""
117
+ try:
118
+ with NamedTemporaryFile(
119
+ "w",
120
+ encoding="utf-8",
121
+ dir=path.parent,
122
+ prefix=f".{path.name}.",
123
+ suffix=".tmp",
124
+ delete=False,
125
+ ) as handle:
126
+ temp_name = handle.name
127
+ json.dump(data, handle, indent=2, sort_keys=True)
128
+ handle.write("\n")
129
+ handle.flush()
130
+ os.fsync(handle.fileno())
131
+ os.replace(temp_name, path)
132
+ finally:
133
+ if temp_name:
134
+ with suppress(FileNotFoundError):
135
+ os.unlink(temp_name)
136
+
137
+
138
+ def read_cache_file(
139
+ path: Path,
140
+ *,
141
+ max_age: timedelta | None = None,
142
+ now: datetime | None = None,
143
+ ) -> CacheReadResult:
144
+ """Read a JSON cache file and classify it as fresh or stale."""
145
+ data = read_json_file(path)
146
+ if not isinstance(data, Mapping):
147
+ raise StateFileInvalidError(f"cache file must contain a JSON object: {path}")
148
+ return CacheReadResult(
149
+ data=data,
150
+ status=_cache_status(data, max_age=max_age, now=now),
151
+ )
152
+
153
+
154
+ def utc_timestamp() -> str:
155
+ """Return a UTC timestamp suitable for cache metadata."""
156
+ return datetime.now(UTC).replace(microsecond=0).isoformat().replace("+00:00", "Z")
157
+
158
+
159
+ def _base_dir(env: Mapping[str, str], key: str, fallback: Path) -> Path:
160
+ value = env.get(key)
161
+ if not value:
162
+ return fallback.expanduser()
163
+ return Path(value).expanduser()
164
+
165
+
166
+ def _cache_status(
167
+ data: Mapping[str, Any],
168
+ *,
169
+ max_age: timedelta | None,
170
+ now: datetime | None,
171
+ ) -> CacheStatus:
172
+ if max_age is None:
173
+ return "fresh"
174
+ updated_at = data.get("updated_at")
175
+ if not isinstance(updated_at, str):
176
+ return "stale"
177
+ updated = _parse_timestamp(updated_at)
178
+ if updated is None:
179
+ return "stale"
180
+ reference = now or datetime.now(UTC)
181
+ if reference.tzinfo is None:
182
+ reference = reference.replace(tzinfo=UTC)
183
+ return "stale" if reference - updated > max_age else "fresh"
184
+
185
+
186
+ def _parse_timestamp(value: str) -> datetime | None:
187
+ try:
188
+ parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
189
+ except ValueError:
190
+ return None
191
+ if parsed.tzinfo is None:
192
+ return parsed.replace(tzinfo=UTC)
193
+ return parsed.astimezone(UTC)
gog_cli/sync.py ADDED
@@ -0,0 +1,146 @@
1
+ """Sync planning and stale-backup detection."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Literal
8
+
9
+ from gog_cli.backup import FileSpec, PlannedFile, _game_product_id, _role_dir
10
+ from gog_cli.layout import BackupLayout, sanitize_filename
11
+
12
+ ComparisonStatus = Literal["current", "stale", "missing", "partial", "unverified"]
13
+
14
+
15
+ @dataclass
16
+ class FileComparison:
17
+ source_id: str
18
+ role: str
19
+ platform: str | None
20
+ language: str | None
21
+ status: ComparisonStatus
22
+ stale_reason: str | None = None
23
+
24
+
25
+ @dataclass
26
+ class SyncPlan:
27
+ destination: Path
28
+ comparisons: list[FileComparison]
29
+ to_download: list[PlannedFile]
30
+ to_verify: list[PlannedFile]
31
+ current: list[FileComparison]
32
+ estimated_bytes: int
33
+
34
+
35
+ def compare_file(spec: FileSpec, manifest_record: dict | None) -> FileComparison:
36
+ base = FileComparison(
37
+ source_id=spec.source_id,
38
+ role=spec.role,
39
+ platform=spec.platform,
40
+ language=spec.language,
41
+ status="missing",
42
+ )
43
+
44
+ if manifest_record is None:
45
+ return base
46
+
47
+ rec_status = manifest_record.get("status", "")
48
+ if rec_status == "partial":
49
+ return FileComparison(**{**base.__dict__, "status": "partial"})
50
+ if rec_status == "downloaded":
51
+ return FileComparison(**{**base.__dict__, "status": "unverified"})
52
+
53
+ # Check staleness
54
+ if manifest_record.get("source_id") != spec.source_id:
55
+ return FileComparison(**{**base.__dict__, "status": "stale", "stale_reason": "id_changed"})
56
+ if manifest_record.get("version") != spec.version:
57
+ return FileComparison(
58
+ **{**base.__dict__, "status": "stale", "stale_reason": "version_changed"}
59
+ )
60
+ if manifest_record.get("expected_size") != spec.expected_size:
61
+ return FileComparison(
62
+ **{**base.__dict__, "status": "stale", "stale_reason": "size_changed"}
63
+ )
64
+ record_md5 = manifest_record.get("expected_md5")
65
+ checksum = manifest_record.get("checksum")
66
+ if isinstance(checksum, dict):
67
+ record_md5 = checksum.get("value")
68
+ if record_md5 != spec.expected_md5:
69
+ return FileComparison(
70
+ **{**base.__dict__, "status": "stale", "stale_reason": "checksum_changed"}
71
+ )
72
+
73
+ return FileComparison(**{**base.__dict__, "status": "current"})
74
+
75
+
76
+ def plan_sync(
77
+ destination: Path,
78
+ games: list[dict],
79
+ download_specs: dict[str, list[FileSpec]],
80
+ manifest: dict,
81
+ layout: BackupLayout,
82
+ *,
83
+ platforms: list[str] | None = None,
84
+ languages: list[str] | None = None,
85
+ file_roles: list[str] | None = None,
86
+ ) -> SyncPlan:
87
+ manifest_games: dict[str, dict] = {}
88
+ for g in manifest.get("games", []):
89
+ for f in g.get("files", []):
90
+ key = _file_key(f.get("role"), f.get("platform"), f.get("language"), f.get("source_id"))
91
+ manifest_games.setdefault(str(g.get("product_id", "")), {})[key] = f
92
+
93
+ comparisons: list[FileComparison] = []
94
+ to_download: list[PlannedFile] = []
95
+ to_verify: list[PlannedFile] = []
96
+ current: list[FileComparison] = []
97
+ estimated_bytes = 0
98
+
99
+ for game in games:
100
+ product_id = _game_product_id(game)
101
+ slug = sanitize_filename(game.get("slug") or product_id)
102
+ game_dir = layout.game_dir(slug)
103
+ game_manifest = manifest_games.get(product_id, {})
104
+
105
+ for spec in download_specs.get(product_id, []):
106
+ if platforms and spec.platform and spec.platform not in platforms:
107
+ continue
108
+ if languages and spec.language and spec.language not in languages:
109
+ continue
110
+ if file_roles and spec.role not in file_roles:
111
+ continue
112
+
113
+ key = _file_key(spec.role, spec.platform, spec.language, spec.source_id)
114
+ record = game_manifest.get(key)
115
+ comparison = compare_file(spec, record)
116
+ comparisons.append(comparison)
117
+
118
+ dest_dir = _role_dir(layout, game_dir, spec.role)
119
+ dest = dest_dir / sanitize_filename(spec.filename or spec.source_id)
120
+
121
+ if comparison.status in ("missing", "stale", "partial"):
122
+ to_download.append(PlannedFile(spec=spec, dest=dest, action="download"))
123
+ if spec.expected_size:
124
+ estimated_bytes += spec.expected_size
125
+ elif comparison.status == "unverified":
126
+ to_verify.append(PlannedFile(spec=spec, dest=dest, action="verify"))
127
+ elif comparison.status == "current":
128
+ current.append(comparison)
129
+
130
+ return SyncPlan(
131
+ destination=destination,
132
+ comparisons=comparisons,
133
+ to_download=to_download,
134
+ to_verify=to_verify,
135
+ current=current,
136
+ estimated_bytes=estimated_bytes,
137
+ )
138
+
139
+
140
+ def _file_key(
141
+ role: str | None,
142
+ platform: str | None,
143
+ language: str | None,
144
+ source_id: str | None,
145
+ ) -> str:
146
+ return f"{role}:{platform}:{language}:{source_id}"
@@ -0,0 +1,193 @@
1
+ Metadata-Version: 2.4
2
+ Name: gog-cli
3
+ Version: 0.2.1
4
+ Summary: CLI tool for backing up DRM-free GOG games.
5
+ Author: gog-cli contributors
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 gog-cli contributors
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/aleksandarristic/gog-cli
29
+ Project-URL: Repository, https://github.com/aleksandarristic/gog-cli
30
+ Project-URL: Bug Tracker, https://github.com/aleksandarristic/gog-cli/issues
31
+ Requires-Python: >=3.12
32
+ Description-Content-Type: text/markdown
33
+ License-File: LICENSE
34
+ Requires-Dist: requests>=2.32
35
+ Provides-Extra: dev
36
+ Requires-Dist: pytest>=8; extra == "dev"
37
+ Requires-Dist: responses>=0.25; extra == "dev"
38
+ Requires-Dist: ruff>=0.4; extra == "dev"
39
+ Dynamic: license-file
40
+
41
+ # gog-cli
42
+
43
+ [![CI](https://github.com/aleksandarristic/gog-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/aleksandarristic/gog-cli/actions/workflows/ci.yml)
44
+
45
+ `gog` is a Python CLI for backing up a user's owned DRM-free GOG game library.
46
+
47
+ It is focused on safe, scriptable workflows:
48
+
49
+ - list owned games with filtering and fuzzy search
50
+ - plan and execute backups to a local directory
51
+ - preserve metadata needed to audit and restore backups
52
+ - download installers and related files with resumable behavior
53
+ - verify downloaded files when checksums are available
54
+
55
+ ## Install
56
+
57
+ Requires Python 3.12 or newer.
58
+
59
+ ```sh
60
+ pip install git+https://github.com/aleksandarristic/gog-cli.git
61
+ ```
62
+
63
+ ## Development
64
+
65
+ ```sh
66
+ python3 -m venv .venv
67
+ . .venv/bin/activate
68
+ python -m pip install -e ".[dev]"
69
+ python -m pytest
70
+ ```
71
+
72
+ Run the CLI locally:
73
+
74
+ ```sh
75
+ gog --help
76
+ gog list
77
+ gog plan --destination /path/to/backups --all --summary
78
+ gog backup --destination /path/to/backups --games-from games.txt --dry-run
79
+ ```
80
+
81
+ ## Basic Workflow
82
+
83
+ ```sh
84
+ gog auth login
85
+ gog refresh
86
+ gog list purchased
87
+ gog plan --destination /path/to/backups --all --storage --check-free-space
88
+ gog backup --destination /path/to/backups --all --yes
89
+ gog list backed-up --destination /path/to/backups
90
+ gog sync --destination /path/to/backups --all --yes
91
+ ```
92
+
93
+ `gog refresh` updates the local purchased-library and download-metadata caches.
94
+ It does not download game installers. Run it before browsing or filtering newly
95
+ added library metadata.
96
+
97
+ ## Browsing Purchased Games
98
+
99
+ `gog list purchased` reads the local cache written by `gog refresh`; it does not
100
+ contact GOG. Human output includes ID, title, release year, genre/category, and
101
+ platforms when those fields are available. JSON output also includes scriptable
102
+ metadata such as `owned`, `release_date`, `genres`, and `is_installable`.
103
+
104
+ Examples:
105
+
106
+ ```sh
107
+ gog list purchased
108
+ gog list purchased --format json
109
+ gog list purchased --search witcher
110
+ gog list purchased --search "baldurs gate"
111
+ gog list purchased --platform windows
112
+ gog list purchased --platform linux --search ftl
113
+ gog list purchased --year 1998..2005
114
+ gog list purchased --year 2010..2020 --include-unknown-year
115
+ gog list purchased --genre strategy
116
+ gog list purchased --genre arcade,rts
117
+ gog list purchased --genre strategy --include-unknown-genre
118
+ gog list purchased --search "baldurs gate" --platform linux --format json
119
+ ```
120
+
121
+ Year filters omit games with unknown years by default; use
122
+ `--include-unknown-year` to keep them. Genre filters similarly omit unknown
123
+ genres by default; use `--include-unknown-genre` to keep those rows.
124
+
125
+ ## Planning Backups
126
+
127
+ `gog plan` shows the same dry-run plan as `gog backup --dry-run` without
128
+ downloading files or creating backup directories. Use it before long backup runs
129
+ to estimate size, inspect filters, and check destination free space.
130
+
131
+ Examples:
132
+
133
+ ```sh
134
+ gog plan --destination /path/to/backups --all
135
+ gog plan --destination /path/to/backups --all --summary
136
+ gog plan --destination /path/to/backups --all --storage
137
+ gog plan --destination /path/to/backups --all --check-free-space
138
+ gog plan --destination /path/to/backups --all --format json
139
+ gog plan --destination /path/to/backups cyberpunk_2077
140
+ ```
141
+
142
+ Platform and language filters can reduce backup size:
143
+
144
+ ```sh
145
+ gog plan --destination /path/to/backups --all --platform linux --storage
146
+ gog plan --destination /path/to/backups --all --platform windows --language en --storage
147
+ ```
148
+
149
+ ## Selecting Games
150
+
151
+ Game selectors can be product IDs, slugs, or exact titles. Commands that select
152
+ games accept repeated `--game` flags:
153
+
154
+ ```sh
155
+ gog plan --destination /path/to/backups --game witcher_3 --game cyberpunk_2077
156
+ gog backup --destination /path/to/backups --game 123456789 --yes
157
+ ```
158
+
159
+ For larger curated lists, put selectors in a UTF-8 text file and pass
160
+ `--games-from`. Blank lines and lines whose first non-whitespace character is
161
+ `#` are ignored.
162
+
163
+ Example `games.txt`:
164
+
165
+ ```text
166
+ # first NAS batch
167
+ witcher_3
168
+ cyberpunk_2077
169
+ 123456789
170
+ ```
171
+
172
+ Use the selector file in plan, backup, or sync workflows:
173
+
174
+ ```sh
175
+ gog plan --destination /path/to/backups --games-from games.txt --storage
176
+ gog backup --destination /path/to/backups --games-from games.txt --downloader aria2c --yes
177
+ gog sync --destination /path/to/backups --games-from games.txt --dry-run
178
+ ```
179
+
180
+ `--games-from` is repeatable and combines with repeated `--game` flags. Do not
181
+ combine explicit game selectors with `--all`.
182
+
183
+ ## Downloading
184
+
185
+ `gog backup` defaults to the built-in direct downloader. To use `aria2c`, install
186
+ `aria2c` and pass `--downloader aria2c` on an executing backup run:
187
+
188
+ ```sh
189
+ gog backup --destination /path/to/backups --games-from games.txt --downloader aria2c --yes
190
+ ```
191
+
192
+ Without `--yes`, backup and sync commands print a dry-run plan and exit without
193
+ downloading or modifying backup files.
@@ -0,0 +1,25 @@
1
+ gog_cli/__init__.py,sha256=cVhMzEvfgCoD2nRTTCEETe6ct0TfruTaiJ8lddmXO_c,73
2
+ gog_cli/api.py,sha256=v-iEV7T_seYUWNl2AHHy-qLODewkQh7vrNIjpWf1yt8,5246
3
+ gog_cli/aria2c.py,sha256=DFRL4GUGE-JzSKPuU0BvazKdwCYJNtT43V4RtALRG7U,4181
4
+ gog_cli/auth.py,sha256=6AhtJ3v_2wlAdw0wR8KoKOCz4xaBBoym5K8iv9bfHso,7144
5
+ gog_cli/backup.py,sha256=MsQDM2h8_hFjCCFadMQNH5ZybblrVjvWZSlmTaOXmZU,5971
6
+ gog_cli/cli.py,sha256=77BtIc9rQhLffKfDsetOsKje8Q5JQDMBzy1Ad-HqgEU,18168
7
+ gog_cli/config.py,sha256=4nY62B0ElRV6cW1y_UcXJFNi6RZGI8nA2eBxZlu04hk,4185
8
+ gog_cli/downloader.py,sha256=G0cBQ4CNN4FYNZi-_k1jZGKdgcCaXOnhU6kO8F6eY_g,6919
9
+ gog_cli/errors.py,sha256=iUo3xAQliNJuQUzVQPiIfRxJHtm6bEmwonvrlNChR1I,1185
10
+ gog_cli/execution.py,sha256=WMaQAojT2grafBAzIoPFL_obXUF1snjolxRRJWTMe0o,36571
11
+ gog_cli/layout.py,sha256=Ea1utRw7MjcXTznI5bpi3DgIokZNj-Yv3w0xUcPXcXc,1921
12
+ gog_cli/listing.py,sha256=mZYUmk7geEms6e_DtPKJY-xV45CcUu9H5WWS6rbdM8g,22884
13
+ gog_cli/log.py,sha256=z6sEDbqS7kQYeeD6MCFx3RaFF997lA_Boges-jAHLgI,468
14
+ gog_cli/metadata.py,sha256=5hP16kpxt-N2rzpPWWnIUOnNdG0lkkRTMCQ9AfAcdBs,6961
15
+ gog_cli/output.py,sha256=r4Zou7BpAzMXdVbGtQoT7Qq9lqsdZQIJDdnLUH7gPtw,2561
16
+ gog_cli/prompt.py,sha256=tbWPr_9yd3-SMhQqxnx_hUM6s4pszh_6ph5-KvqPNKQ,1670
17
+ gog_cli/refresh.py,sha256=VTM6Gthi97pvRuZhRlPg32ixUF9xpKOzfZj_2oTDcCo,7277
18
+ gog_cli/state.py,sha256=X4DkVkJ4ZViH1wgLA8xHZgOABqlIwdato5zS6FpZ9VY,5904
19
+ gog_cli/sync.py,sha256=OS0nWNxd4LLIMe2NKDHII8oiQX1DZS72W_dKmgmwilM,4971
20
+ gog_cli-0.2.1.dist-info/licenses/LICENSE,sha256=6gwrSQnIro2IPRBL1bUj7r9cc7oG-WsufbDC8xHv1Lg,1077
21
+ gog_cli-0.2.1.dist-info/METADATA,sha256=aAlbT8u4UuTFf5m1c_5URCsmb0o7oYlhYwEj9nW12jU,6673
22
+ gog_cli-0.2.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
23
+ gog_cli-0.2.1.dist-info/entry_points.txt,sha256=R8ndz-Q46JZ2OBsfqsZN4b5DhHkFvEUwbGkc0P0rqI8,41
24
+ gog_cli-0.2.1.dist-info/top_level.txt,sha256=ghVgOu6AMaHYlBtW37jklNOIdZviBwVAvN23Eq1EOLw,8
25
+ gog_cli-0.2.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ gog = gog_cli.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 gog-cli contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ gog_cli