steam-client 0.0.1.dev1__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.
File without changes
steam_client/app.py ADDED
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+ from abc import ABC, abstractmethod
3
+ from pathlib import Path
4
+ from typing import TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from .steam import Steam
8
+
9
+
10
+ class App(ABC):
11
+ """Abstract base class for all Steam apps."""
12
+
13
+ def __init__(self, steam: Steam):
14
+ self._steam = steam
15
+
16
+ @property
17
+ @abstractmethod
18
+ def appid(self) -> str:
19
+ """Returns the game's appid."""
20
+ pass
21
+
22
+ @property
23
+ @abstractmethod
24
+ def icon(self) -> Path:
25
+ """Returns the path to the icon image."""
26
+ pass
27
+
28
+ @property
29
+ @abstractmethod
30
+ def header(self) -> Path:
31
+ pass
32
+
33
+ @property
34
+ @abstractmethod
35
+ def grid(self) -> Path:
36
+ pass
37
+
38
+ @property
39
+ @abstractmethod
40
+ def hero(self) -> Path:
41
+ pass
42
+
43
+ def run(self):
44
+ """Launches app with the specified app ID in the Steam client."""
45
+ self._steam.commands.run_game_id(self.appid)
@@ -0,0 +1,49 @@
1
+ from typing import Literal
2
+ import os
3
+
4
+
5
+ SteamWindow = Literal[
6
+ 'main',
7
+ 'games',
8
+ 'games/details',
9
+ 'games/grid',
10
+ 'games/list',
11
+ 'friends',
12
+ 'chat',
13
+ 'bigpicture',
14
+ 'news',
15
+ 'settings',
16
+ 'tools',
17
+ 'console'
18
+ ]
19
+
20
+
21
+ class Commands:
22
+
23
+ def run_game_id(self, app_id: str) -> None:
24
+ """Launches game with the specified ID in the Steam client."""
25
+ os.startfile(f'steam://rungameid/{app_id}')
26
+
27
+ def store(self, app_id: str) -> None:
28
+ """Opens the game's store page in the Steam client."""
29
+ os.startfile(f'steam://store/{app_id}')
30
+
31
+ def install(self, app_id: str) -> None:
32
+ """Opens the game's install prompt in the Steam client."""
33
+ os.startfile(f'steam://install/{app_id}')
34
+
35
+ def uninstall(self, app_id: str) -> None:
36
+ """Opens the game's uninstall prompt in the Steam client."""
37
+ os.startfile(f'steam://uninstall/{app_id}')
38
+
39
+ def update_news(self, app_id: str) -> None:
40
+ """Opens the game's update news in the Steam client."""
41
+ os.startfile(f'steam://updatenews/{app_id}')
42
+
43
+ def open(self, window: SteamWindow) -> None:
44
+ """Opens the specified window in the Steam client."""
45
+ os.startfile(f'steam://open/{window}')
46
+
47
+ def open_url(self, url: str) -> None:
48
+ """Opens the specified URL in the Steam client."""
49
+ os.startfile(f'steam://openurl/{url}')
steam_client/game.py ADDED
@@ -0,0 +1,87 @@
1
+ from __future__ import annotations
2
+ import functools
3
+ from pathlib import Path
4
+ from typing import TYPE_CHECKING
5
+
6
+ import vdf
7
+
8
+ if TYPE_CHECKING:
9
+ from .steam import Steam
10
+
11
+ from .app import App
12
+
13
+
14
+ UNKNOWN_GAME_NAME = 'UNKNOWN'
15
+
16
+
17
+ class SteamGame(App):
18
+ """Represents a Steam game."""
19
+
20
+ def __init__(self, steam: Steam, library_path: str, appid: str):
21
+ self.library_path = library_path
22
+ self._appid = appid
23
+ super().__init__(steam)
24
+
25
+ def __repr__(self) -> str:
26
+ return f'Game(steam={self._steam.__repr__()}, library_path={self.library_path.__repr__()}, appid={self.appid.__repr__()})'
27
+
28
+ @property
29
+ def icon(self) -> Path:
30
+ """Returns the path to the icon image."""
31
+ return Path(self._steam.library_cache).joinpath(f'{self.appid}_icon.jpg')
32
+
33
+ @property
34
+ def header(self) -> Path:
35
+ """Returns the path to the header image."""
36
+ return Path(self._steam.library_cache).joinpath(f'{self.appid}_header.jpg')
37
+
38
+ @property
39
+ def grid(self) -> Path:
40
+ """Returns the path to the 600x900 grid image."""
41
+ return Path(self._steam.library_cache).joinpath(f'{self.appid}_library_600x900.jpg')
42
+
43
+ @property
44
+ def hero(self) -> Path:
45
+ """Returns the path to the hero image."""
46
+ return Path(self._steam.library_cache).joinpath(f'{self.appid}_library_hero.jpg')
47
+
48
+ @property
49
+ def hero_blur(self) -> Path:
50
+ """Returns the path to the blurred hero image."""
51
+ return Path(self._steam.library_cache).joinpath(f'{self.appid}_library_hero_blur.jpg')
52
+
53
+ @property
54
+ def manifest_path(self) -> Path:
55
+ """Returns the path to the game's appmanifest."""
56
+ return Path(self.library_path).joinpath('steamapps', f'appmanifest_{self.appid}.acf')
57
+
58
+ @functools.cached_property
59
+ def _manifest(self) -> dict:
60
+ """Returns the data from the game's appmanifest."""
61
+ try:
62
+ with open(self.manifest_path, 'r', encoding='utf-8', errors='ignore') as f:
63
+ manifest = vdf.load(f)
64
+ return manifest
65
+ except FileNotFoundError:
66
+ return {}
67
+
68
+ @functools.cached_property
69
+ def name(self) -> str:
70
+ """Returns the name of the game."""
71
+ try:
72
+ return self._manifest['AppState']['name']
73
+ except KeyError:
74
+ return UNKNOWN_GAME_NAME
75
+
76
+ @property
77
+ def appid(self) -> str:
78
+ """Returns the app ID."""
79
+ return self._appid
80
+
81
+ def open_store_page(self):
82
+ """Opens the game's store page in the Steam client."""
83
+ self._steam.commands.store(self.appid)
84
+
85
+ def uninstall(self):
86
+ """Uninstalls the game."""
87
+ self._steam.commands.uninstall(self.appid)
@@ -0,0 +1,79 @@
1
+ from __future__ import annotations
2
+ from pathlib import Path
3
+ import hashlib
4
+ from typing import TYPE_CHECKING, List, Optional, Union
5
+
6
+ import vdf
7
+
8
+ from steam_client.shortcut import Shortcut
9
+
10
+ if TYPE_CHECKING:
11
+ from .steam import Steam
12
+ from .library_directory import LibraryDirectory
13
+ from .game import SteamGame
14
+
15
+
16
+ class Library:
17
+ """Represents the Steam library."""
18
+
19
+ def __init__(self, steam: 'Steam'):
20
+ self._steam = steam
21
+ self._libraries_hash: Optional[str] = None
22
+ self._libraries: List[LibraryDirectory] = []
23
+
24
+ def _hash_steam_libraries(self) -> str:
25
+ """Returns the MD5 hash of the Steam library folders file."""
26
+ with open(str(self._steam.library_folders), 'rb') as f:
27
+ return hashlib.md5(f.read()).hexdigest()
28
+
29
+ def _is_updated(self) -> bool:
30
+ """Returns whether the Steam library folders have been updated."""
31
+ return self._libraries_hash != self._hash_steam_libraries()
32
+
33
+ def _update_libraries(self):
34
+ """Updates the Steam library folders."""
35
+ with open(self._steam.library_folders, 'r', encoding='utf-8', errors='ignore') as f:
36
+ libraries = vdf.load(f)
37
+ # The is not always formatted the same way, so we grab the first key
38
+ folder_key = list(libraries.keys())[0]
39
+ self._libraries = [LibraryDirectory(self._steam, libraries[folder_key][item]["path"],
40
+ libraries[folder_key][item]["apps"]) for item in libraries[folder_key]]
41
+
42
+ def libraries(self) -> List[LibraryDirectory]:
43
+ """Returns the Steam library folders."""
44
+ if self._is_updated():
45
+ self._libraries_hash = self._hash_steam_libraries()
46
+ self._update_libraries()
47
+ return self._libraries
48
+
49
+ def games(self) -> List[SteamGame]:
50
+ """Returns the games from the Steam library."""
51
+ games = []
52
+ for library in self.libraries():
53
+ games.extend(library.get_games())
54
+ return games
55
+
56
+ def game_by_id(self, appid: str) -> Optional[SteamGame]:
57
+ """Returns the game with the specified ID."""
58
+ for game in self.games():
59
+ if game.appid == appid:
60
+ return game
61
+ return None
62
+
63
+ def game_by_name(self, name: str) -> Optional[SteamGame]:
64
+ """Returns the game with the specified name."""
65
+ for game in self.games():
66
+ if game.name.casefold() == name.casefold():
67
+ return game
68
+ return None
69
+
70
+ def shortcuts(self) -> List[Shortcut]:
71
+ """Returns the Non-Steam shortcuts from the Steam library."""
72
+ shortcuts = []
73
+ for user in self._steam.login_users.users():
74
+ shortcuts.extend(user.shortcuts())
75
+ return shortcuts
76
+
77
+ def all(self) -> List[Union[SteamGame, Shortcut]]:
78
+ """Returns all the games and shortcuts from the Steam library."""
79
+ return self.games() + self.shortcuts()
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+ from dataclasses import dataclass
3
+ from typing import TYPE_CHECKING, List
4
+ from pathlib import Path
5
+
6
+ import vdf
7
+
8
+ if TYPE_CHECKING:
9
+ from .steam import Steam
10
+ from .game import SteamGame
11
+
12
+
13
+ @dataclass
14
+ class LibraryDirectory:
15
+ """Represents a Steam library folder."""
16
+ _steam: Steam
17
+ path: str
18
+ apps: List[str]
19
+
20
+ def get_games(self) -> List[SteamGame]:
21
+ return [SteamGame(self._steam, self.path, appid) for appid in self.apps]
@@ -0,0 +1,100 @@
1
+ from __future__ import annotations
2
+ from dataclasses import dataclass
3
+ from pathlib import Path
4
+ from typing import Dict, List, Optional
5
+ from typing import TYPE_CHECKING
6
+ from typing import TypedDict
7
+
8
+ import vdf
9
+
10
+ if TYPE_CHECKING:
11
+ from steam_client.steam import Steam
12
+
13
+ from .shortcut import Shortcut
14
+
15
+ STEAM64_OFFSET = 76561197960265728
16
+
17
+
18
+ class UserData(TypedDict):
19
+ AccountName: str
20
+ PersonaName: str
21
+ RememberPassword: str
22
+ WantsOfflineMode: str
23
+ SkipOfflineModeWarning: str
24
+ AllowAutoLogin: str
25
+ MostRecent: str
26
+ Timestamp: str
27
+
28
+
29
+ UserLoginsFile = Dict[str, Dict[str, UserData]]
30
+
31
+
32
+ class User:
33
+ """Represents a current or previous logged in Steam user."""
34
+
35
+ def __init__(self, steam: Steam, steam_id64: str, user_data: UserData):
36
+ self._steam = steam
37
+ self.steam_id64 = steam_id64
38
+ self._user_data = user_data
39
+
40
+ def __repr__(self) -> str:
41
+ return f'User(steam={self._steam.__repr__()}, steam_id64={self.steam_id64.__repr__()}, user_data={self._user_data.__repr__()})'
42
+
43
+ @property
44
+ def is_most_recent(self) -> bool:
45
+ """Returns whether the user is the most recent user."""
46
+ return self._user_data['MostRecent'] == '1'
47
+
48
+ @property
49
+ def steam_id3(self) -> int:
50
+ """Returns the last portion of the user's SteamID3."""
51
+ steamidacct = (int(self.steam_id64) - STEAM64_OFFSET)
52
+ return steamidacct
53
+
54
+ @property
55
+ def user_data_dir(self) -> Path:
56
+ """Returns the path to the userdata folder."""
57
+ return Path(self._steam.base_path).joinpath('userdata', str(self.steam_id3))
58
+
59
+ @property
60
+ def config(self) -> Path:
61
+ """Returns the path to the config folder."""
62
+ return Path(self.user_data_dir).joinpath('config')
63
+
64
+ @property
65
+ def shortcuts_file(self) -> Path:
66
+ """Returns the path to the shortcuts.vdf file."""
67
+ return Path(self.config).joinpath('shortcuts.vdf')
68
+
69
+ @property
70
+ def grid_path(self) -> Path:
71
+ """Returns the path to the user's shortcut grid images."""
72
+ return Path(self.user_data_dir).joinpath('config', 'grid')
73
+
74
+ def shortcuts(self) -> List[Shortcut]:
75
+ """Returns the data from the shortcuts.vdf file."""
76
+ with open(self.shortcuts_file, 'rb') as f:
77
+ shortcuts = vdf.binary_load(f)
78
+ return [Shortcut(self._steam, self, shortcuts['shortcuts'][shortcut_idx]) for shortcut_idx in shortcuts['shortcuts']]
79
+
80
+
81
+ class LoginUsers:
82
+ """Represents the loginusers.vdf file."""
83
+
84
+ def __init__(self, steam: Steam):
85
+ self._steam = steam
86
+
87
+ @property
88
+ def _path(self) -> Path:
89
+ """Returns the path to the loginusers.vdf file."""
90
+ return Path(self._steam.base_path).joinpath('config', 'loginusers.vdf')
91
+
92
+ def users(self) -> List[User]:
93
+ """Returns the users from the loginusers.vdf file."""
94
+ with open(self._path, 'r', encoding='utf-8', errors='ignore') as f:
95
+ login_users: UserLoginsFile = vdf.load(f)
96
+ return [User(self._steam, steam_id64, user_data) for steam_id64, user_data in login_users['users'].items()]
97
+
98
+ def most_recent_user(self) -> Optional[User]:
99
+ """Returns the most recent user from the loginusers.vdf file."""
100
+ return next((user for user in self.users() if user.is_most_recent), None)
@@ -0,0 +1,70 @@
1
+ from __future__ import annotations
2
+ from pathlib import Path
3
+ from typing import TYPE_CHECKING, Dict
4
+ from typing import TypedDict
5
+
6
+ import pycrc.algorithms as crc
7
+
8
+ if TYPE_CHECKING:
9
+ from .steam import Steam
10
+ from .login_users import User
11
+
12
+ from .app import App
13
+
14
+
15
+ class ShortcutEntry(TypedDict):
16
+ appid: int
17
+ appname: str
18
+ exe: str
19
+ StartDir: str
20
+ LaunchOptions: str
21
+ icon: str
22
+ tags: Dict[str, str]
23
+
24
+
25
+ class Shortcut(App):
26
+ """Represents a Non-Steam Game shortcut."""
27
+
28
+ def __init__(self, steam: Steam, user: User, data: ShortcutEntry):
29
+ self._data = data
30
+ self._user = user
31
+ super().__init__(steam)
32
+
33
+ def __repr__(self) -> str:
34
+ return f'Shortcut(steam={self._steam.__repr__()}, data={self._data.__repr__()})'
35
+
36
+ @property
37
+ def name(self) -> str:
38
+ return self._data["appname"]
39
+
40
+ @property
41
+ def appid(self) -> str:
42
+ algorithm = crc.Crc(width=32, poly=0x04C11DB7, reflect_in=True,
43
+ xor_in=0xffffffff, reflect_out=True, xor_out=0xffffffff)
44
+ input_string = ''.join([self._data["exe"], self._data["appname"]])
45
+ top_32 = algorithm.bit_by_bit(input_string) | 0x80000000
46
+ full_64 = (top_32 << 32) | 0x02000000
47
+ return str(full_64)
48
+
49
+ def _short_id(self) -> str:
50
+ """
51
+ Return Steam shortened App ID.
52
+ This is primarily used for shortcuts in the grid.
53
+ """
54
+ return str(int(self.appid) >> int(32))
55
+
56
+ @property
57
+ def icon(self) -> str:
58
+ return self._data["icon"] or str(self._user.grid_path.joinpath(f"{self._short_id()}_icon.png"))
59
+
60
+ @property
61
+ def header(self) -> str:
62
+ return str(self._user.grid_path.joinpath(f"{self._short_id()}.png"))
63
+
64
+ @property
65
+ def grid(self) -> str:
66
+ return str(self._user.grid_path.joinpath(f"{self._short_id()}p.png"))
67
+
68
+ @property
69
+ def hero(self) -> str:
70
+ return str(self._user.grid_path.joinpath(f"{self._short_id()}_hero.png"))
steam_client/steam.py ADDED
@@ -0,0 +1,41 @@
1
+ import functools
2
+ from pathlib import Path
3
+
4
+ from .library import Library
5
+ from .commands import Commands
6
+ from .login_users import LoginUsers
7
+
8
+ DEFAULT_WIN_STEAM_PATH = r"c:\Program Files (x86)\Steam"
9
+
10
+
11
+ class Steam:
12
+ """Represents the Steam client."""
13
+
14
+ def __init__(self, base_path: str = DEFAULT_WIN_STEAM_PATH):
15
+ self.base_path: str = base_path
16
+ self.login_users = LoginUsers(self)
17
+ self.commands = Commands()
18
+ self.library = Library(self)
19
+
20
+ def __repr__(self) -> str:
21
+ return f'Steam(base_path={self.base_path.__repr__()})'
22
+
23
+ @property
24
+ def app_cache(self) -> Path:
25
+ """Returns the path to the appcache folder."""
26
+ return Path(self.base_path).joinpath('appcache')
27
+
28
+ @property
29
+ def user_data(self) -> Path:
30
+ """Returns the path to the userdata folder."""
31
+ return Path(self.base_path).joinpath('userdata')
32
+
33
+ @property
34
+ def library_folders(self) -> Path:
35
+ """Returns the path to the libraryfolders.vdf file."""
36
+ return Path(self.base_path).joinpath('config', 'libraryfolders.vdf')
37
+
38
+ @property
39
+ def library_cache(self) -> Path:
40
+ """Returns the path to the librarycache folder."""
41
+ return Path(self.base_path).joinpath('appcache', 'librarycache')
steam_client/utils.py ADDED
@@ -0,0 +1,13 @@
1
+ import winreg as reg
2
+ from winreg import HKEY_LOCAL_MACHINE
3
+
4
+ from .steam import Steam
5
+
6
+
7
+ STEAM_SUB_KEY = r'SOFTWARE\WOW6432Node\Valve\Steam'
8
+
9
+
10
+ def steam_from_registry() -> Steam:
11
+ """Returns a Steam instance from the Windows registry."""
12
+ with reg.OpenKey(HKEY_LOCAL_MACHINE, STEAM_SUB_KEY) as hkey:
13
+ return Steam(reg.QueryValueEx(hkey, "InstallPath")[0])
@@ -0,0 +1,24 @@
1
+ from typing import Any, Dict
2
+
3
+ import requests # type: ignore
4
+
5
+
6
+ BASE_URL = "https://api.steampowered.com"
7
+
8
+
9
+ class WebAPI:
10
+
11
+ def __init__(self):
12
+ self._session = requests.Session()
13
+
14
+ def _request(self, method: str, endpoint: str, base_url: str = BASE_URL, **kwargs) -> requests.Response:
15
+ """Makes a request to the Steam API."""
16
+ url = f'{base_url}/{endpoint}'
17
+ r = self._session.request(method, url, **kwargs)
18
+ r.raise_for_status()
19
+ return r
20
+
21
+ def get_app_list(self) -> Dict[Any, Any]:
22
+ """Returns a list of all Steam apps."""
23
+ r = self._request('GET', 'ISteamApps/GetAppList/v2')
24
+ return r.json()
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Garulf
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,13 @@
1
+ Metadata-Version: 2.1
2
+ Name: steam-client
3
+ Version: 0.0.1.dev1
4
+ Summary: Steam client library for Python
5
+ Author-email: William McAllister <dev.garulf@gmail.com>
6
+ License: MIT
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3 :: Only
10
+ Requires-Python: >=3.8
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+
@@ -0,0 +1,16 @@
1
+ steam_client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ steam_client/app.py,sha256=A93hYLT_sEKpfmScGBliL6tD7REpbEYPeMf61RC7xzM,931
3
+ steam_client/commands.py,sha256=nYYInY3Nxpyc7DGQ1WKDmxRYsWmsODC-pYDOArPski8,1394
4
+ steam_client/game.py,sha256=nww5sGmHJRimCnqZwIcrw-ToU2mB9d5gFTtTEj-htag,2694
5
+ steam_client/library.py,sha256=ruRNC5b-p_H7fEiyZ58O24fOWCHKVl3RAWOQROXO-dY,2916
6
+ steam_client/library_directory.py,sha256=p06iBw9xsuZjCJUTPWmUXhGhDqkx4hUchNGLxU1j5aY,481
7
+ steam_client/login_users.py,sha256=yYbQETSI1gZnwJqJcu5TeREyXY2UKwKiWnNVkajiejw,3288
8
+ steam_client/shortcut.py,sha256=Q07i31KB6u-FSAs0LXxDPnEfHWZUUKEei6Vnsyq3jvU,1976
9
+ steam_client/steam.py,sha256=bXdosPBGpj2ZiIsIk8y142fumh-0LvSnnxBwRbsN0GE,1266
10
+ steam_client/utils.py,sha256=nvRlaYsiUH2VXGXgE-bEvEHWLg7XC2Xopr7KFBYWy4A,367
11
+ steam_client/web_api.py,sha256=7RcVyNGkNqVw5j2Yzu4lO5Pctrd8W00Di_j_h-T4T-k,670
12
+ steam_client-0.0.1.dev1.dist-info/LICENSE,sha256=qIIHsmhBULNOOo_bvd1fFU-vVUDwJ-Dxg0Kl_kwtWnM,1063
13
+ steam_client-0.0.1.dev1.dist-info/METADATA,sha256=De69RGZMH2cYDa_oxCETb98-iupLkCGYGQ8CyWKj88Q,412
14
+ steam_client-0.0.1.dev1.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
15
+ steam_client-0.0.1.dev1.dist-info/top_level.txt,sha256=nyNYK9O4l8s61ogUSz422nEbSumzgI8D_KbXfXGBMgk,13
16
+ steam_client-0.0.1.dev1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.42.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ steam_client