pax_global_header00006660000000000000000000000064145060241150014510gustar00rootroot0000000000000052 comment=932987b58e66ffd96ebe3ee2f68afeb88ab98009 Lagg-steamodd-932987b/000077500000000000000000000000001450602411500144515ustar00rootroot00000000000000Lagg-steamodd-932987b/.gitignore000066400000000000000000000001311450602411500164340ustar00rootroot00000000000000*.pyc build testscript.py .idea/* .DS_Store dist steamodd.egg-info # Sphinx docs/_build Lagg-steamodd-932987b/.readthedocs.yaml000066400000000000000000000002451450602411500177010ustar00rootroot00000000000000version: 2 build: os: ubuntu-22.04 tools: python: "3.10" sphinx: configuration: docs/conf.py python: install: - requirements: docs/requirements.txt Lagg-steamodd-932987b/LICENSE000066400000000000000000000013601450602411500154560ustar00rootroot00000000000000Copyright (c) 2010+, Anthony Garcia Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. Lagg-steamodd-932987b/README.md000066400000000000000000000016021450602411500157270ustar00rootroot00000000000000# Steamodd # Steam odds and ends ## Overview ## Steamodd implements a set of tools for working with the Steam API and related data: * Steam API interface wrappers * Steam inventory manager (SIM) * VDF serializer ## Requirements ## Python 3+ ## Installation ## From command line: $ pip install steamodd If you wish to install it manually, Steamodd uses the standard setuptools module. To install it run: $ python setup.py install ## Documentation ## Full documentation is available at http://steamodd.readthedocs.org/en/latest/. ## Testing ## To launch the test suite run `python setup.py run_tests -k `. ## Contributing ## If you would like to contribute please send a pull request. ## Bugs and feature requests ## Feel free to open an [issue](https://github.com/Lagg/steamodd/issues) if you spot a bug or have an idea you would like to see go into steamodd. Lagg-steamodd-932987b/docs/000077500000000000000000000000001450602411500154015ustar00rootroot00000000000000Lagg-steamodd-932987b/docs/Makefile000066400000000000000000000163711450602411500170510ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " applehelp to make an Apple Help Book" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " coverage to run coverage check of the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Steamodd.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Steamodd.qhc" applehelp: $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp @echo @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." @echo "N.B. You won't be able to view it unless you put it in" \ "~/Library/Documentation/Help or install it in your application" \ "bundle." devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/Steamodd" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Steamodd" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." coverage: $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage @echo "Testing of coverage in the sources finished, look at the " \ "results in $(BUILDDIR)/coverage/python.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." Lagg-steamodd-932987b/docs/api.rst000066400000000000000000000326621450602411500167150ustar00rootroot00000000000000================== Steam API wrappers ================== Low level methods ================= You can call `any method from any of Steam API interfaces`_ using :code:`steam.api.interface` class. Let's start with a quick example where we fetch user's game library. Start by importing :code:`interface` class: >>> from steam.api import interface Call method :code:`GetOwnedGames` of interface :code:`IPlayerService`. We are going to fetch games of user with id :code:`76561198017493014` and include all application information: >>> games = interface('IPlayerService').GetOwnedGames(steamid=76561198017493014, include_appinfo=1) Since all method calls are lazy by default, this doesn't do anything at all. We'll need to either iterate over :code:`games`, :code:`print` it or access any of its dictionary keys: >>> print(games['response']['game_count']) # Fetches resource 249 Don't worry, resource isn't fetched each time you access results. >>> print(games) # Uses cached resource {'response': {'games': [{'name': 'Counter-Strike', 'playtime_forever': 1570,... You can disable laziness of :code:`interface` by passing :code:`aggressive=True` to its method: >>> games = interface('IPlayerService').GetOwnedGames(steamid=76561198017493014, include_appinfo=1, aggressive=True) You can also pass :code:`since` (which translates to HTTP header :code:`If-Modified-Since`) and :code:`timeout` to method. By default, :code:`version` is set to :code:`1`. :code:`data` can be passed to send POST data with requests. By default no data is assumed and request types are GET. Any number of additional keyword arguments are supported depending on the given method (see `documentation`_). .. _any method from any of Steam API interfaces: https://wiki.teamfortress.com/wiki/WebAPI#Methods .. _documentation: https://wiki.teamfortress.com/wiki/WebAPI#Methods High level methods ================== Following classes are convenience wrappers around `Low level methods`_. :code:`kwargs` are always passed to appropriate interface methods, so you can use all arguments from previous section. Apps ---- .. autoclass:: steam.apps.app_list >>> from steam.apps import app_list >>> app_list = app_list() >>> 'Dota 2' in app_list True >>> 'Half-Life 3' in app_list False >>> len(app_list) 16762 >>> app_list['Counter-Strike'] (10, u'Counter-Strike') Items ----- .. autoclass:: steam.items.schema Fetching schema of Team Fortress 2 (id ``440``) would look like: >>> schema = steam.items.schema(440) >>> schema[340].name u'Defiant Spartan' Schema class is an iterator of :meth:`steam.items.item` objects. There are also other properties available: .. autoattribute:: steam.items.schema.client_url .. autoattribute:: steam.items.schema.language .. autoattribute:: steam.items.schema.attributes .. autoattribute:: steam.items.schema.origins .. autoattribute:: steam.items.schema.qualities .. autoattribute:: steam.items.schema.particle_systems .. autoattribute:: steam.items.schema.kill_ranks .. autoattribute:: steam.items.schema.kill_types .. autoclass:: steam.items.item This is a simple wrapper around JSON representation of both schema and inventory items. It is composed mostly from item properties: >>> item = schema[340] >>> item.name u'Defiant Spartan' >>> item.type u'Hat' >>> item.attributes [, ] As convenience, ``item`` acts also as iterator of its attributes: >>> for attribute in item.attributes: ... attribute.name ... u'kill eater score type' u'kill eater kill type' Following properties are available: .. autoattribute:: steam.items.item.attributes .. autoattribute:: steam.items.item.quality .. autoattribute:: steam.items.item.inventory_token .. autoattribute:: steam.items.item.position .. autoattribute:: steam.items.item.equipped .. autoattribute:: steam.items.item.equipable_classes .. autoattribute:: steam.items.item.schema_id .. autoattribute:: steam.items.item.name .. autoattribute:: steam.items.item.type .. autoattribute:: steam.items.item.icon .. autoattribute:: steam.items.item.image .. autoattribute:: steam.items.item.id .. autoattribute:: steam.items.item.original_id .. autoattribute:: steam.items.item.level .. autoattribute:: steam.items.item.slot_name .. autoattribute:: steam.items.item.cvar_class .. autoattribute:: steam.items.item.craft_class .. autoattribute:: steam.items.item.craft_material_type .. autoattribute:: steam.items.item.custom_name .. autoattribute:: steam.items.item.custom_description .. autoattribute:: steam.items.item.quantity .. autoattribute:: steam.items.item.description .. autoattribute:: steam.items.item.min_level .. autoattribute:: steam.items.item.contents .. autoattribute:: steam.items.item.tradable .. autoattribute:: steam.items.item.craftable .. autoattribute:: steam.items.item.full_name .. autoattribute:: steam.items.item.kill_eaters .. autoattribute:: steam.items.item.rank .. autoattribute:: steam.items.item.available_styles .. autoattribute:: steam.items.item.style .. autoattribute:: steam.items.item.capabilities .. autoattribute:: steam.items.item.tool_metadata .. autoattribute:: steam.items.item.origin .. autoclass:: steam.items.item_attribute >>> for attribute in item.attributes: ... print('%s: %s' % (attribute.name, attribute.formatted_value)) ... kill eater score type: 64.0 kill eater kill type: 64.0 Following properties are available: .. autoattribute:: steam.items.item_attribute.formatted_value .. autoattribute:: steam.items.item_attribute.formatted_description .. autoattribute:: steam.items.item_attribute.name .. autoattribute:: steam.items.item_attribute.cvar_class .. autoattribute:: steam.items.item_attribute.id .. autoattribute:: steam.items.item_attribute.type .. autoattribute:: steam.items.item_attribute.value .. autoattribute:: steam.items.item_attribute.value_int .. autoattribute:: steam.items.item_attribute.value_float .. autoattribute:: steam.items.item_attribute.description .. autoattribute:: steam.items.item_attribute.value_type .. autoattribute:: steam.items.item_attribute.hidden .. autoattribute:: steam.items.item_attribute.account_info .. autoclass:: steam.items.inventory Fetches inventory of ``player`` for given ``app`` id: >>> inventory = steam.items.inventory(76561198017493014, 570) >>> for item in inventory: ... item.name ... '226749283' '226749284' Since inventory endpoint returns just very basic structure, we have to provide also ``schema`` if we want to work with fully populated :meth:`steam.items.item` objects: >>> schema = steam.items.schema(440) >>> inventory = steam.items.inventory(76561198017493014, 440, schema) >>> for item in inventory: ... item.name ... u'Mercenary' u'Noise Maker - Winter Holiday' There is also single property: .. autoattribute:: steam.items.inventory.cells_total .. autoclass:: steam.items.assets Fetches store assets for ``app`` id. Assets class acts as an iterator of :meth:`steam.items.asset_item` objects. >>> assets = steam.items.assets(440) >>> for asset in assets: ... asset.price ... {u'MXN': 74.0, u'EUR': 4.59, u'VND': 109000.0, u'AUD': 6.5, ...} {u'MXN': 112.0, u'EUR': 6.99, u'VND': 159000.0, u'AUD': 9.8, ...} If you care only about single currency, ``currency`` keyword argument in `ISO 4217`_ format is also accepted. >>> assets = steam.items.assets(440, currency="RUB") >>> for asset in assets: ... asset.price ... {u'RUB': 290.0} {u'RUB': 435.0} All available tags of assets are available in following property: .. autoattribute:: steam.items.assets.tags .. autoclass:: steam.items.asset_item .. autoattribute:: steam.items.asset_item.tags .. autoattribute:: steam.items.asset_item.base_price .. autoattribute:: steam.items.asset_item.price .. autoattribute:: steam.items.asset_item.name .. _ISO 4217: http://en.wikipedia.org/wiki/ISO_4217 Localization ------------ .. autoclass:: steam.loc.language >>> language = steam.loc.language('nl_NL') >>> language.name 'Dutch' >>> language.code 'nl_NL' If language is not specified, it defaults to English: >>> language = steam.loc.language() >>> language.name 'English' >>> language.code 'en_US' If language isn't supported, ``__init__`` raises :meth:`steam.loc.LanguageUnsupportedError` >>> language = steam.loc.language('sk_SK') Traceback (most recent call last): File "", line 1, in File "steam/loc.py", line 68, in __init__ raise LanguageUnsupportedError(code) steam.loc.LanguageUnsupportedError: sk_sk Properties: .. autoattribute:: steam.loc.language.code .. autoattribute:: steam.loc.language.name .. autoclass:: steam.loc.LanguageUnsupportedError Remote storage -------------- Tools for probing Steam's UGC file storage system. UGC itself means User Generated Content but in this context assume that such terms as "UGC ID" are specific to Valve's system. UGC IDs are found in various places in the API and Steam including decal attributes on TF2 items. Practically speaking the purpose of `ugc_file` is similar to that of :class:`steam.user.vanity_url`. Namely to convert an arbitrary ID into something useful like a direct URL. .. autoclass:: steam.remote_storage.ugc_file Fetches UGC file metadata for the given UGC and app ID. >>> ugc = steam.remote_storage.ugc_file(440, 650994986817657344) >>> ugc.url u'http://images.akamai.steamusercontent.com/ugc/650994986817657344/D2ADAD7F19BFA9A99BD2B8850CC317DC6BA01BA9/' Properties: .. autoattribute:: steam.remote_storage.ugc_file.size .. autoattribute:: steam.remote_storage.ugc_file.filename .. autoattribute:: steam.remote_storage.ugc_file.url .. autoclass:: steam.remote_storage.FileNotFoundError User ---- .. autoclass:: steam.user.vanity_url >>> vanity_url = steam.user.vanity_url('http://steamcommunity.com/id/ondrowan') >>> vanity_url.id64 76561198017493014 .. autoclass:: steam.user.profile >>> profile = steam.user.profile('76561198017493014') >>> profile.persona u'Lich Buchannon' >>> profile.level 37 .. autoattribute:: steam.user.profile.id64 .. autoattribute:: steam.user.profile.id32 .. autoattribute:: steam.user.profile.persona .. autoattribute:: steam.user.profile.profile_url .. autoattribute:: steam.user.profile.vanity .. autoattribute:: steam.user.profile.avatar_small .. autoattribute:: steam.user.profile.avatar_medium .. autoattribute:: steam.user.profile.avatar_large .. autoattribute:: steam.user.profile.status .. autoattribute:: steam.user.profile.visibility .. autoattribute:: steam.user.profile.configured .. autoattribute:: steam.user.profile.last_online .. autoattribute:: steam.user.profile.comments_enabled .. autoattribute:: steam.user.profile.real_name .. autoattribute:: steam.user.profile.primary_group .. autoattribute:: steam.user.profile.creation_date .. autoattribute:: steam.user.profile.current_game .. autoattribute:: steam.user.profile.location .. autoattribute:: steam.user.profile.lobbysteamid .. autoattribute:: steam.user.profile.level .. automethod:: steam.user.profile.from_def .. autoattribute:: steam.user.profile.current_game .. autoclass:: steam.user.profile_batch >>> profiles = steam.user.profile_batch(['76561198811195748', '76561198017493014']) >>> for profile in profiles: ... profile.persona ... u'Bot.Lagg.Me Space Cadet 01' u'Lich Buchannon' .. autoclass:: steam.user.bans >>> bans = steam.user.bans('76561197962899758') >>> bans.vac True >>> bans.vac_count 1 >>> bans.days_unbanned 2708 .. autoattribute:: steam.user.bans.id64 .. autoattribute:: steam.user.bans.community .. autoattribute:: steam.user.bans.vac .. autoattribute:: steam.user.bans.vac_count .. autoattribute:: steam.user.bans.days_unbanned .. autoattribute:: steam.user.bans.economy .. autoattribute:: steam.user.bans.game_count .. automethod:: steam.user.bans.from_def .. autoclass:: steam.user.bans_batch >>> bans_batch = steam.user.bans_batch(['76561197962899758', '76561198017493014']) >>> for bans in bans_batch: ... '%s: %s' % (bans.id64, bans.vac) ... '76561197962899758: True' '76561198017493014: False' .. autoclass:: steam.user.friend .. autoattribute:: steam.user.friend.steamid .. autoattribute:: steam.user.friend.relationship .. autoattribute:: steam.user.friend.since .. autoclass:: steam.user.friend_list >>> friend_list = steam.user.friend_list('76561198811195748') >>> friend_list.count 146 >>> for friend in friend_list: ... friend.steamid ... 76561197960299337 76561197960339433 (... and 144 more) .. autoattribute:: steam.user.friend_list.count Lagg-steamodd-932987b/docs/conf.py000066400000000000000000000221721450602411500167040ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # Steamodd documentation build configuration file, created by # sphinx-quickstart on Thu Apr 16 15:14:25 2015. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys import os import shlex # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) cwd = os.getcwd() parent = os.path.dirname(cwd) sys.path.append(parent) import steam # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'Steamodd' copyright = u'2015, Anthony Garcia & Ondrej Slinták (initial docs)' author = u'Anthony Garcia' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = steam.__version__ # The full version, including alpha/beta/rc tags. release = steam.__version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all # documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. #keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. #html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' #html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # Now only 'ja' uses this config value #html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. #html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = 'Steamodddoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', # Latex figure (float) alignment #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'Steamodd.tex', u'Steamodd Documentation', u'Anthony Garcia', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'steamodd', u'Steamodd Documentation', [author], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'Steamodd', u'Steamodd Documentation', author, 'Steamodd', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False Lagg-steamodd-932987b/docs/history.rst000066400000000000000000000027241450602411500176410ustar00rootroot00000000000000======= History ======= Origin ------ Steamodd originated with an early version of OPTF2 which itself grew out of a 200 line script I wrote in the early days of the Steam API to find things I could complain about. Since then it has grown into a more and more capable and fully featured module with every version. It is still a work in progress and the API is subject to change in breaking ways, however as of the 3.0 release I have began using a simple and meaningful versioning system that should make moving to new versions much easier. Major version numbers are incremented when the release makes breaking changes, minor version numbers are incremented when they are not. Meaning that it is safe to upgrade without having to change existing code. The name -------- If there's one thing I've learned over the years and most recently from OPTF2 it's a good idea to record the meaning behind your project names if they aren't explicitly indicative of function or you *will* forget. Steamodd quite simply stands for "Steam odds and ends". Even though it's starting to become more of a robust module it started out as a small and probably not very well designed script meant to be run as a tool instead of a reusable lib. That's not to say that the name doesn't fit, since in addition to the strong implementation of the API it has the recent `VDF`_ support and the SIM layer to boast as useful but not exactly unrelated utilities. .. _VDF: http://wiki.teamfortress.com/wiki/WebAPI/VDF Lagg-steamodd-932987b/docs/index.rst000066400000000000000000000002631450602411500172430ustar00rootroot00000000000000Welcome to Steamodd's documentation! ==================================== Contents: .. toctree:: :maxdepth: 3 history installation quick-start api sim vdf Lagg-steamodd-932987b/docs/installation.rst000066400000000000000000000005471450602411500206420ustar00rootroot00000000000000============ Installation ============ From command line: .. code-block:: bash $ pip install steamodd If you wish to install it manually, Steamodd uses the standard distutils module. To install it run: .. code-block:: bash $ python setup.py install For further instructions and commands run: .. code-block:: bash $ python setup.py --help Lagg-steamodd-932987b/docs/make.bat000066400000000000000000000161201450602411500170060ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. xml to make Docutils-native XML files echo. pseudoxml to make pseudoxml-XML files for display purposes echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled echo. coverage to run coverage check of the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) REM Check if sphinx-build is available and fallback to Python version if any %SPHINXBUILD% 2> nul if errorlevel 9009 goto sphinx_python goto sphinx_ok :sphinx_python set SPHINXBUILD=python -m sphinx.__init__ %SPHINXBUILD% 2> nul if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) :sphinx_ok if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Steamodd.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Steamodd.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdf" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf cd %~dp0 echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdfja" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf-ja cd %~dp0 echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) if "%1" == "coverage" ( %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage if errorlevel 1 exit /b 1 echo. echo.Testing of coverage in the sources finished, look at the ^ results in %BUILDDIR%/coverage/python.txt. goto end ) if "%1" == "xml" ( %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml if errorlevel 1 exit /b 1 echo. echo.Build finished. The XML files are in %BUILDDIR%/xml. goto end ) if "%1" == "pseudoxml" ( %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml if errorlevel 1 exit /b 1 echo. echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. goto end ) :end Lagg-steamodd-932987b/docs/quick-start.rst000066400000000000000000000011611450602411500204010ustar00rootroot00000000000000=========== Quick start =========== Steam API key ------------- If you are going to use Steam API, you'll need to set Steam API key either from code: >>> import steam >>> steam.api.key.set(API_KEY) Or set environmental variable: .. code-block:: bash $ export STEAMODD_API_KEY="your_key" Most methods will not complete successfully without it. If you don't have an API key you can register for one on `Steam`_. .. _Steam: http://steamcommunity.com/dev/apikey Components ---------- This library consists of three major components, which are documented separately: * :doc:`api` * :doc:`sim` * :doc:`vdf` Lagg-steamodd-932987b/docs/requirements.txt000066400000000000000000000000141450602411500206600ustar00rootroot00000000000000Sphinx>=7.2 Lagg-steamodd-932987b/docs/sim.rst000066400000000000000000000071201450602411500167230ustar00rootroot00000000000000======================= Steam Inventory Manager ======================= High level item manager which scrapes data from http://steamcommunity.com instead of Steam API. .. autoclass:: steam.sim.inventory_context Fetches metadata of inventories for different games of given user: >>> inventory_context = steam.sim.inventory_context('76561198017493014') >>> inventory_context.apps [u'570', u'753', u'251970', u'440', u'620'] >>> inventory_context.get(570) {u'name': u'Dota 2', u'trade_permissions': u'FULL', u'rgContexts': ...} This class also acts as an iterator of inventories: >>> for game_inventory_ctx in inventory_context: ... game_inventory_ctx['name'] ... u'Team Fortress 2' u'Dota 2' u'Portal 2' u'Steam' u'Sins of a Dark Age' Properties: .. autoattribute:: steam.sim.inventory_context.ctx .. autoattribute:: steam.sim.inventory_context.apps .. automethod:: steam.sim.inventory_context.get .. autoclass:: steam.sim.inventory Takes a user ID, app ID and inventory section ID. Returns given inventory using the JSON/AJAX feed: >>> inventory = steam.sim.inventory('76561198017493014', 570, 2) >>> inventory.cells_total 650 This class also acts as an iterator yielding :class:`steam.sim.item` objects: >>> for item in inventory: ... item.full_name ... u'Rattlebite' u'Heavenly Guardian Skirt' u'Gloried Horn of Druud' ... An optional last_assetid and page size can be passed for pagination. Properties: .. autoattribute:: steam.sim.inventory.cells_total .. autoattribute:: steam.sim.inventory.page_end .. autoattribute:: steam.sim.inventory.pages_continue .. autoclass:: steam.sim.item Subclass of :class:`steam.items.item`. It is used as output from :class:`steam.sim.inventory`. On top of properties inherited from :class:`steam.items.item`, these are available: .. autoattribute:: steam.items.item.attributes .. autoattribute:: steam.sim.item.background_color .. autoattribute:: steam.sim.item.name .. autoattribute:: steam.sim.item.custom_name .. autoattribute:: steam.sim.item.name_color .. autoattribute:: steam.sim.item.full_name .. autoattribute:: steam.sim.item.hash_name .. autoattribute:: steam.sim.item.tool_metadata .. autoattribute:: steam.sim.item.tags .. autoattribute:: steam.sim.item.tradable .. autoattribute:: steam.sim.item.craftable .. autoattribute:: steam.sim.item.quality .. autoattribute:: steam.sim.item.quantity .. autoattribute:: steam.sim.item.attributes .. autoattribute:: steam.sim.item.position .. autoattribute:: steam.sim.item.schema_id .. autoattribute:: steam.sim.item.type .. autoattribute:: steam.sim.item.icon .. autoattribute:: steam.sim.item.image .. autoattribute:: steam.sim.item.id .. autoattribute:: steam.sim.item.slot_name .. autoattribute:: steam.sim.item.appid .. autoclass:: steam.sim.item_attribute Subclass of :class:`steam.items.item_attribute`. It is used as output from :meth:`steam.sim.item.attributes`. On top of properties inherited from :meth:`steam.items.item_attribute`, these are available: .. autoattribute:: steam.sim.item_attribute.value_type .. autoattribute:: steam.sim.item_attribute.description .. autoattribute:: steam.sim.item_attribute.description_color .. autoattribute:: steam.sim.item_attribute.type .. autoattribute:: steam.sim.item_attribute.value Lagg-steamodd-932987b/docs/vdf.rst000066400000000000000000000022631450602411500167150ustar00rootroot00000000000000============== VDF serializer ============== |VDF|_ is format similar to JSON or YAML, used by Valve to store data. This module mimics built-in :code:`json` module and provides functions for serialization and deserialization of :code:`VDF` files. .. autofunction:: steam.vdf.dump .. code:: python >>> with open('dump.vdf', 'w') as file: ... vdf.dump({u"key": u"value", u"list": [1, 2, 3]}, file) → cat dump.vdf "list" { "1" "1" "2" "1" "3" "1" } "key" "value" .. autofunction:: steam.vdf.dumps .. code:: python >>> vdf_obj = vdf.dumps({"key": "value", "list": [1, 2, 3]}) >>> vdf_obj.decode('utf-16') u'\n "list"\n {\n "1" "1"\n "2" "1"\n "3" "1"\n }\n\n "key" "value"\n' .. autofunction:: steam.vdf.load .. code:: python >>> with open('dump.vdf', 'r') as file: ... vdf.load(file) ... {u'list': {u'1': u'1', u'3': u'1', u'2': u'1'}, u'key': u'value'} .. autofunction:: steam.vdf.loads .. code:: python >>> vdf.loads('"list" { "a" "1" "b" "2" "c" "3" }') {u'list': {u'a': u'1', u'c': u'3', u'b': u'2'}} .. |VDF| replace:: :code:`VDF` .. _VDF: https://wiki.teamfortress.com/wiki/WebAPI/VDF Lagg-steamodd-932987b/setup.py000066400000000000000000000033611450602411500161660ustar00rootroot00000000000000""" Copyright (c) 2010+, Anthony Garcia Distributed under the ISC License (see LICENSE) """ from setuptools import setup, Command from setuptools.errors import OptionError from unittest import TestLoader, TextTestRunner import sys import steam class run_tests(Command): description = "Run the steamodd unit tests" user_options = [ ("key=", 'k', "Your API key") ] def initialize_options(self): try: self.key = steam.api.key.get() except steam.api.APIKeyMissingError: self.key = None def finalize_options(self): if not self.key: raise OptionError("API key is required") else: steam.api.key.set(self.key) # Generous timeout so slow server days don't cause failed builds steam.api.socket_timeout.set(20) def run(self): tests = TestLoader().discover("tests") results = TextTestRunner(verbosity = 2).run(tests) sys.exit(int(not results.wasSuccessful())) setup(name = "steamodd", version = steam.__version__, description = "High level Steam API implementation with low level reusable core", long_description = "Please see the `README `_ for a full description.", packages = ["steam"], author = steam.__author__, author_email = steam.__contact__, url = "https://github.com/Lagg/steamodd", classifiers = [ "License :: OSI Approved :: ISC License (ISCL)", "Intended Audience :: Developers", "Operating System :: OS Independent", "Programming Language :: Python" ], license = steam.__license__, cmdclass = {"run_tests": run_tests}) Lagg-steamodd-932987b/steam/000077500000000000000000000000001450602411500155625ustar00rootroot00000000000000Lagg-steamodd-932987b/steam/__init__.py000066400000000000000000000006751450602411500177030ustar00rootroot00000000000000""" High level Steam API implementation with low level reusable core Copyright (c) 2010+, Anthony Garcia Distributed under the ISC License (see LICENSE) """ __version__ = "5.0" __author__ = "Anthony Garcia" __contact__ = "anthony@lagg.me" __license__ = "ISC" __copyright__ = "Copyright (c) 2010+, " + __author__ __all__ = [ "api", "apps", "items", "loc", "remote_storage", "sim", "user", "vdf" ] from . import * Lagg-steamodd-932987b/steam/api.py000066400000000000000000000166411450602411500167150ustar00rootroot00000000000000""" Core API code Copyright (c) 2010+, Anthony Garcia Distributed under the ISC License (see LICENSE) """ import os import json import socket import sys # Python 2 <-> 3 glue try: from urllib.request import urlopen from urllib.request import Request as urlrequest from urllib.parse import urlencode from urllib import error as urlerror except ImportError: from urllib2 import urlopen from urllib2 import Request as urlrequest from urllib import urlencode import urllib2 as urlerror class SteamError(Exception): """ For future expansion, considering that steamodd is already no longer *just* an API implementation """ pass class APIError(SteamError): """ Base API exception class """ pass class APIKeyMissingError(APIError): pass class HTTPError(APIError): """ Raised for other HTTP codes or results """ pass class HTTPStale(HTTPError): """ Raised for HTTP code 304 """ pass class HTTPTimeoutError(HTTPError): """ Raised for timeouts (not necessarily from the http lib itself but the socket layer, but the effect and recovery is the same, this just makes it more convenient """ pass class HTTPFileNotFoundError(HTTPError): """ Raised for HTTP code 404 """ pass class HTTPInternalServerError(HTTPError): """ Raised for HTTP code 500 """ pass class key(object): __api_key = None __api_key_env_var = os.environ.get("STEAMODD_API_KEY") @classmethod def set(cls, value): """ Set the current API key, overrides env var. """ cls.__api_key = str(value) @classmethod def get(cls): """Get the current API key. if one has not been given via 'set' the env var STEAMODD_API_KEY will be checked instead. """ apikey = cls.__api_key or cls.__api_key_env_var if apikey: return apikey else: raise APIKeyMissingError("API key not set") class socket_timeout(object): """ Global timeout, can be overridden by timeouts passed to ctor """ __timeout = 5 @classmethod def set(cls, value): cls.__timeout = value @classmethod def get(cls): return cls.__timeout class _interface_method(object): def __init__(self, iface, name): self._iface = iface self._name = name def __call__(self, version=1, timeout=None, since=None, aggressive=False, data={}, **kwargs): kwargs.setdefault("format", "json") kwargs.setdefault("key", key.get()) url = "https://api.steampowered.com/{0}/{1}/v{2}?{3}".format(self._iface, self._name, version, urlencode(kwargs)) return method_result(url, last_modified=since, timeout=timeout, aggressive=aggressive, data=data) class interface(object): def __init__(self, iface): self._iface = iface def __getattr__(self, name): return _interface_method(self._iface, name) class http_downloader(object): def __init__(self, url, last_modified=None, timeout=None, data={}): self._user_agent = "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; Valve Steam Client/1366845241; ) AppleWebKit/535.15 (KHTML, like Gecko) Chrome/18.0.989.0 Safari/535.11" self._url = url self._timeout = timeout or socket_timeout.get() self._last_modified = last_modified self._data = None if data: self._data = data def _build_headers(self): head = {} if self._last_modified: head["If-Modified-Since"] = str(self._last_modified) if self._user_agent: head["User-Agent"] = str(self._user_agent) return head def download(self): head = self._build_headers() status_code = -1 body = '' try: if self._data: url_req = urlrequest(self._url, headers=head, data=urlencode(self._data)) else: url_req = urlrequest(self._url, headers=head); req = urlopen(url_req, timeout=self._timeout) status_code = req.code body = req.read() except urlerror.HTTPError as E: code = E.getcode() # More portability hax (no reason property in 2.6?) try: reason = E.reason except AttributeError: reason = "Connection error" if code == 404: raise HTTPFileNotFoundError("File not found") elif code == 304: raise HTTPStale(str(self._last_modified)) elif code == 500: raise HTTPInternalServerError("Internal Server Error") else: raise HTTPError("Server connection failed: {0} ({1})".format(reason, code)) except (socket.timeout, urlerror.URLError): raise HTTPTimeoutError("Server took too long to respond") except socket.error as E: raise HTTPError("Server read error: {0}".format(E)) lm = req.headers.get("last-modified") self._last_modified = lm return body @property def last_modified(self): return self._last_modified @property def url(self): return self._url class method_result(dict): """ Holds a deserialized JSON object obtained from fetching the given URL. If aggressive is True then the data will be fetched when the method is called instead of only when the object is actually accessed. """ def __handle_accessor(self, method, *args, **kwargs): try: if not self._fetched: self.call() except AttributeError: self._fetched = True return getattr(super(method_result, self), method)(*args, **kwargs) def __init__(self, *args, **kwargs): super(method_result, self).__init__() self._fetched = False aggressive = kwargs.get("aggressive") if "aggressive" in kwargs: del kwargs["aggressive"] self._downloader = http_downloader(*args, **kwargs) if aggressive: self.call() def __getitem__(self, *args, **kwargs): return self.__handle_accessor("__getitem__", *args, **kwargs) def __setitem__(self, *args, **kwargs): return self.__handle_accessor("__setitem__", *args, **kwargs) def __delitem__(self, *args, **kwargs): return self.__handle_accessor("__delitem__", *args, **kwargs) def __iter__(self): return self.__handle_accessor("__iter__") def __contains__(self, *args, **kwargs): return self.__handle_accessor("__contains__", *args, **kwargs) def __len__(self): return self.__handle_accessor("__len__") def __str__(self): return self.__handle_accessor("__str__") def call(self): """ Make the API call again and fetch fresh data. """ data = self._downloader.download() # Only try to pass errors arg if supported if sys.version >= "2.7": data = data.decode("utf-8", errors="ignore") else: data = data.decode("utf-8") self.update(json.loads(data)) self._fetched = True def get(self, *args, **kwargs): return self.__handle_accessor("get", *args, **kwargs) def keys(self): return self.__handle_accessor("keys") Lagg-steamodd-932987b/steam/apps.py000066400000000000000000000034611450602411500171030ustar00rootroot00000000000000""" Steam app metadata Copyright (c) 2010+, Anthony Garcia Distributed under the ISC License (see LICENSE) """ from . import api class AppError(api.APIError): pass class app_list(object): """ Retrieves a list of all Steam apps with their ID and localized name. """ _builtin = { 753: "Steam", 440: "Team Fortress 2", 520: "Team Fortress 2 Beta", 620: "Portal 2", 570: "DOTA 2", 205700: "DOTA 2 Test", 816: "DOTA 2 Internal", 730: "Counter Strike Global Offensive" } def __contains__(self, key): try: self[key] return True except KeyError: return False def __getitem__(self, key): try: return key, self._builtin[key] except KeyError: key = str(key).lower() for app, name in self: if str(app) == key or name.lower() == key: return app, name raise def __init__(self, **kwargs): self._api = api.interface("ISteamApps").GetAppList(version=2, **kwargs) self._cache = {} def __iter__(self): return next(self) def __len__(self): return len(self._apps) @property def _apps(self): if not self._cache: try: self._cache = self._api["applist"]["apps"] except KeyError: raise AppError("Bad app list returned") return self._cache def __next__(self): i = 0 data = self._apps while(i < len(data)): app = data[i]["appid"] name = data[i]["name"] i += 1 yield (app, name) next = __next__ Lagg-steamodd-932987b/steam/items.py000066400000000000000000001042431450602411500172610ustar00rootroot00000000000000""" Steam economy - Inventories, schemas, assets, etc. Copyright (c) 2010+, Anthony Garcia Distributed under the ISC License (see LICENSE) """ import time import operator from . import api, loc class SchemaError(api.APIError): pass class AssetError(api.APIError): pass class InventoryError(api.APIError): pass class BadID64Error(InventoryError): pass class ProfilePrivateError(InventoryError): pass class schema(object): """ Wrapper for item schema of certain games from Valve. Those are currently available (along with their ids): * ``260`` - Counter Strike: Source Beta * ``440`` - Team Fortress 2 * ``520`` - Team Fortress 2 Public Beta * ``570`` - Dota 2 * ``620`` - Portal 2 * ``710`` - Counter-Strike: Global Offensive Beta Dev * ``816`` - Dota 2 internal test * ``841`` - Portal 2 Beta * ``205790`` - Dota 2 (beta) test """ @property def _schema(self): if self._cache: return self._cache try: status = self._api["result"]["status"] # Client schema URL self._cache["client"] = self._api["result"]["items_game_url"] # ID:name origin map onames = self._api["result"].get("originNames", []) self._cache["origins"] = dict([(o["origin"], o["name"]) for o in onames]) # Two maps are built here, one for name:ID and one for ID:loc name. # Most of the time qualities will be resolved by ID (as that's what # they are in inventories, it's mostly just the schema that # specifies qualities by non-loc name) qualities = {} quality_names = {} for k, v in self._api["result"]["qualities"].items(): locname = self._api["result"]["qualityNames"][k] idname = k.lower() qualities[v] = (v, idname, locname) quality_names[idname] = v self._cache["qualities"] = qualities self._cache["quality_names"] = quality_names # Two maps are built here, one for name:ID and one for # ID:attribute. As with qualities it's mostly the schema that needs # this extra layer of mapping. Inventories specify attribute IDs # and quality IDs alike directly. attributes = {} attribute_names = {} for attrib in self._api["result"]["attributes"]: attrid = attrib["defindex"] attributes[attrid] = attrib attribute_names[attrib["name"].lower()] = attrid self._cache["attributes"] = attributes self._cache["attribute_names"] = attribute_names # ID:system particle map particles = self._api["result"].get("attribute_controlled_attached_particles", []) self._cache["particles"] = dict([(p["id"], p) for p in particles]) # Name:level eater rank map levels = self._api["result"].get("item_levels", []) self._cache["eater_ranks"] = dict([(l["name"], l["levels"]) for l in levels]) # Type ID:Type eater score count types killtypes = self._api["result"].get("kill_eater_score_types", []) self._cache["eater_types"] = dict([(k["type"], k) for k in killtypes]) # Schema ID:item map (building this is insanely fast, overhead is # minimal compared to lookup benefits in backpacks) if self._items is not None: items = self._items else: items = self._api["result"]["items"] self._cache["items"] = dict([(i["defindex"], i) for i in items]) except KeyError: # Due to the various fields needed we can't check for certain # fields and fall back ala 'inventory' if status is not None: raise SchemaError("Steam returned bad schema with error code " + str(status)) else: raise SchemaError("Empty or corrupt schema returned") return self._cache @property def client_url(self): """ Client schema URL """ return self._schema["client"] @property def language(self): """ The ISO code of the language the instance is localized to """ return self._language def _attribute_definition(self, attrid): """ Returns the attribute definition dict of a given attribute ID, can be the name or the integer ID """ attrs = self._schema["attributes"] try: # Make a new dict to avoid side effects return dict(attrs[attrid]) except KeyError: attr_names = self._schema["attribute_names"] attrdef = attrs.get(attr_names.get(str(attrid).lower())) if not attrdef: return None else: return dict(attrdef) def _quality_definition(self, qid): """ Returns the ID and localized name of the given quality, can be either ID type """ qualities = self._schema["qualities"] try: return qualities[qid] except KeyError: qid = self._schema["quality_names"].get(str(qid).lower(), 0) return qualities.get(qid, (qid, "normal", "Normal")) @property def attributes(self): """ Returns all attributes in the schema """ attrs = self._schema["attributes"] return [item_attribute(attr) for attr in sorted(attrs.values(), key=operator.itemgetter("defindex"))] @property def origins(self): """ Returns a map of all origins """ return self._schema["origins"] @property def qualities(self): """ Returns a dict of all possible qualities. The key(s) will be the ID, values are a tuple containing ID, name, localized name. To resolve a quality to a name intelligently use '_quality_definition' """ return self._schema["qualities"] @property def particle_systems(self): """ Returns a dictionary of particle system dicts keyed by ID """ return self._schema["particles"] @property def kill_ranks(self): """ Returns a list of ranks for weapons with kill tracking """ return self._schema["eater_ranks"] @property def kill_types(self): """ Returns a dict with keys that are the value of the kill eater type attribute and values that are the name string """ return self._schema["eater_types"] def origin_id_to_name(self, origin): """ Returns a localized origin name for a given ID """ try: oid = int(origin) except (ValueError, TypeError): return None return self.origins.get(oid) def _find_item_by_id(self, id): return self._schema["items"].get(id) def __iter__(self): return next(self) def __next__(self): iterindex = 0 iterdata = list(self._schema["items"].values()) while(iterindex < len(iterdata)): data = item(iterdata[iterindex], self) iterindex += 1 yield data next = __next__ def __getitem__(self, key): realkey = None try: realkey = key["defindex"] except: realkey = key schema_item = self._find_item_by_id(realkey) if schema_item: return item(schema_item, self) else: raise KeyError(key) def __len__(self): return len(self._schema["items"]) def __init__(self, app, lang=None, version=1, **kwargs): """ schema will be used to initialize the schema if given, lang can be any ISO language code. lm will be used to generate an HTTP If-Modified-Since header. """ self._language = loc.language(lang).code self._app = int(app) self._cache = {} # WORKAROUND: CS GO v1 returns 404 if self._app == 730 and version == 1: version = 2 # WORKAROUND: certain apps have moved to GetSchemaOverview/GetSchemaItems if self._app in [440]: self._api = api.interface("IEconItems_" + str(self._app)).GetSchemaOverview(language=self._language, version=version, **kwargs) items = [] next_start = 0 # HACK: build the entire item list immediately because Valve decided not to allow us to get the entire thing at once while next_start is not None: next_items = api.interface("IEconItems_" + str(self._app)).GetSchemaItems(language=self._language, version=version, aggressive=True, start=next_start, **kwargs) items.extend(next_items["result"]["items"]) next_start = next_items["result"].get("next", None) self._items = items else: self._api = api.interface("IEconItems_" + str(self._app)).GetSchema(language=self._language, version=version, **kwargs) self._items = None class item(object): """ Stores a single inventory item. """ @property def attributes(self): """ Returns a list of attributes """ overridden_attrs = self._attributes sortmap = {"neutral": 1, "positive": 2, "negative": 3} sortedattrs = list(overridden_attrs.values()) sortedattrs.sort(key=operator.itemgetter("defindex")) sortedattrs.sort(key=lambda t: sortmap.get(t.get("effect_type", "neutral"), 99)) return [item_attribute(theattr) for theattr in sortedattrs] @property def quality(self): """ Returns a tuple containing ID, name, and localized name of the quality """ return self._quality @property def inventory_token(self): """ Returns the item's inventory token (a bitfield), deprecated. """ return self._item.get("inventory", 0) @property def position(self): """ Returns a position in the inventory or -1 if there's no position available (i.e. an item hasn't dropped yet or got displaced) """ inventory_token = self.inventory_token if inventory_token == 0: return -1 else: return inventory_token & 0xFFFF @property def equipped(self): """ Returns a dict of classes that have the item equipped and in what slot """ equipped = self._item.get("equipped", []) # WORKAROUND: 0 is probably an off-by-one error # WORKAROUND: 65535 actually serves a purpose (according to Valve) return dict([(eq["class"], eq["slot"]) for eq in equipped if eq["class"] != 0 and eq["slot"] != 65535]) @property def equipable_classes(self): """ Returns a list of classes that _can_ use the item. """ sitem = self._schema_item return [c for c in sitem.get("used_by_classes", self.equipped.keys()) if c] @property def schema_id(self): """ Returns the item's ID in the schema. """ return self._item["defindex"] @property def name(self): """ Returns the item's undecorated name """ return self._schema_item.get("item_name", str(self.id)) @property def type(self): """ Returns the item's type. e.g. "Kukri" for the Tribalman's Shiv. If Valve failed to provide a translation the type will be the token without the hash prefix. """ return self._schema_item.get("item_type_name", '') @property def icon(self): """ URL to a small thumbnail sized image of the item, suitable for display in groups """ return self._schema_item.get("image_url", '') @property def image(self): """ URL to a full sized image of the item, for displaying 'zoomed-in' previews """ return self._schema_item.get("image_url_large", '') @property def id(self): """ Returns the item's unique serial number if it has one """ return self._item.get("id") @property def original_id(self): """ Returns the item's original ID if it has one. This is the last "version" of the item before it was customized or otherwise changed """ return self._item.get("original_id") @property def level(self): """ Returns the item's level (e.g. 10 for The Axtinguisher) if it has one """ return self._item.get("level") @property def slot_name(self): """ Returns the item's slot as a string, this includes "primary", "secondary", "melee", and "head". Note that this is the slot of the item as it appears in the schema, and not necessarily the actual equipable slot. (see 'equipped')""" return self._schema_item.get("item_slot") @property def cvar_class(self): """ Returns the item's class (what you use in the game to equip it, not the craft class)""" return self._schema_item.get("item_class") @property def craft_class(self): """ Returns the item's class in the crafting system if it has one. This includes hat, craft_bar, or craft_token. """ return self._schema_item.get("craft_class") @property def craft_material_type(self): return self._schema_item.get("craft_material_type") @property def custom_name(self): """ Returns the item's custom name if it has one. """ return self._item.get("custom_name") @property def custom_description(self): """ Returns the item's custom description if it has one. """ return self._item.get("custom_desc") @property def quantity(self): """ Returns the number of uses the item has, for example, a dueling mini-game has 5 uses by default """ return self._item.get("quantity", 1) @property def description(self): """ Returns the item's default description if it has one """ return self._schema_item.get("item_description") @property def min_level(self): """ Returns the item's minimum level (non-random levels will have the same min and max level) """ return self._schema_item.get("min_ilevel") @property def max_level(self): """ Returns the item's maximum level (non-random levels will have the same min and max level) """ return self._schema_item.get("max_ilevel") @property def contents(self): """ Returns the item in the container, if there is one. This will be a standard item object. """ rawitem = self._item.get("contained_item") if rawitem: return self.__class__(rawitem, self._schema) @property def tradable(self): """ Somewhat of a WORKAROUND since this flag is there sometimes, "cannot trade" is there sometimes and then there's "always tradable". Opposed to only occasionally tradable when it feels like it. Attr 153 = cannot trade """ return not (self._item.get("flag_cannot_trade") or (153 in self)) @property def craftable(self): """ Returns not craftable if the cannot craft flag exists. True, otherwise. """ return not self._item.get("flag_cannot_craft") @property def full_name(self): """ The full name of the item, generated depending on things such as its quality, rank, the schema language, and so on. """ qid, quality_str, pretty_quality_str = self.quality custom_name = self.custom_name item_name = self.name english = (self._language == "en_US") rank = self.rank prefixed = self._schema_item.get("proper_name", False) prefix = '' suffix = '' pfinal = '' if item_name.startswith("The ") and prefixed: item_name = item_name[4:] if quality_str != "unique" and quality_str != "normal": pfinal = pretty_quality_str if english: if prefixed: if quality_str == "unique": pfinal = "The" elif quality_str == "unique": pfinal = '' if rank and quality_str == "strange": pfinal = rank["name"] if english: prefix = pfinal elif pfinal: suffix = '(' + pfinal + ') ' + suffix return (prefix + " " + item_name + " " + suffix).strip() @property def kill_eaters(self): """ Returns a list of tuples containing the proper localized kill eater type strings and their values according to set/type/value "order" """ eaters = {} ranktypes = self._kill_types for attr in self: aname = attr.name.strip() aid = attr.id if aname.startswith("kill eater"): try: # Get the name prefix (matches up type and score and # determines the primary type for ranking) eateri = list(filter(None, aname.split(' ')))[-1] if eateri.isdigit(): eateri = int(eateri) else: # Probably the primary type/score which has no number eateri = 0 except IndexError: # Fallback to attr ID (will completely fail to make # anything legible but better than nothing) eateri = aid if aname.find("user") != -1: # User score types have lower sorting priority eateri += 100 eaters.setdefault(eateri, [None, None]) if aname.find("score type") != -1 or aname.find("kill type") != -1: # Score type attribute if eaters[eateri][0] is None: eaters[eateri][0] = attr.value else: # Value attribute eaters[eateri][1] = attr.value eaterlist = [] defaultleveldata = "KillEaterRank" for key, eater in sorted(eaters.items()): etype, count = eater # Eater type can be null (it still is in some older items), null # count means we're looking at either an uninitialized item or # schema item if count is not None: rank = ranktypes.get(etype or 0, {"level_data": defaultleveldata, "type_name": "Count"}) eaterlist.append((rank.get("level_data", defaultleveldata), rank["type_name"], count)) return eaterlist @property def rank(self): """ Returns the item's rank (if it has one) as a dict that includes required score, name, and level. """ if self._rank != {}: # Don't bother doing attribute lookups again return self._rank try: # The eater determining the rank levelkey, typename, count = self.kill_eaters[0] except IndexError: # Apparently no eater available self._rank = None return None rankset = self._ranks.get(levelkey, [{"level": 0, "required_score": 0, "name": "Strange"}]) for rank in rankset: self._rank = rank if count < rank["required_score"]: break return self._rank @property def available_styles(self): """ Returns a list of all styles defined for the item """ styles = self._schema_item.get("styles", []) return list(map(operator.itemgetter("name"), styles)) @property def style(self): """ The current style the item is set to or None if the item has no styles """ try: return self.available_styles[self._item.get("style", 0)] except IndexError: return None @property def capabilities(self): """ Returns a list of capabilities, these are flags for what the item can do or be done with """ return list(self._schema_item.get("capabilities", {}).keys()) @property def tool_metadata(self): """ A dict containing item dependant metadata such as holiday restrictions, types, and properties used by the client. Do not assume a stable syntax. """ return self._schema_item.get("tool") @property def origin(self): """ Returns the item's localized origin name """ return self._origin def __iter__(self): return next(self) def __next__(self): iterindex = 0 attrs = self.attributes while(iterindex < len(attrs)): data = attrs[iterindex] iterindex += 1 yield data next = __next__ def __getitem__(self, key): for attr in self: if attr.id == key or attr.name == key: return attr raise KeyError(key) def __contains__(self, key): try: self.__getitem__(key) return True except KeyError: return False def __str__(self): cname = self.custom_name fullname = self.full_name if cname: return "{0} ({1})".format(cname, fullname) else: return fullname def __init__(self, item, schema=None): self._item = item self._schema_item = None self._schema = schema self._rank = {} self._ranks = {} self._kill_types = {} self._origin = None self._attributes = {} if schema: self._schema_item = schema._find_item_by_id(self._item["defindex"]) if not self._schema_item: self._schema_item = self._item qualityid = self._item.get("quality", self._schema_item.get("item_quality", 0)) if schema: self._quality = schema._quality_definition(qualityid) else: self._quality = (qualityid, "normal", "Normal") if schema: self._language = schema.language else: self._language = "en_US" originid = self._item.get("origin") if schema: self._origin = schema.origin_id_to_name(originid) elif originid: self._origin = str(originid) if schema: self._ranks = schema.kill_ranks self._kill_types = schema.kill_types for attr in self._schema_item.get("attributes", []): index = attr.get("defindex", attr.get("name")) attrdef = None if schema: attrdef = schema._attribute_definition(index) if attrdef: index = attrdef["defindex"] self._attributes.setdefault(index, {}) if attrdef: self._attributes[index].update(attrdef) self._attributes[index].update(attr) if self._item != self._schema_item: for attr in self._item.get("attributes", []): index = attr["defindex"] if schema and index not in self._attributes: attrdef = schema._attribute_definition(index) if attrdef: self._attributes[index] = attrdef self._attributes.setdefault(index, {}) self._attributes[index].update(attr) class item_attribute(object): """ Wrapper around item attributes. """ @property def formatted_value(self): """ Returns a formatted value as a string""" # TODO: Cleanup all of this, it's just weird and unnatural maths val = self.value pval = val ftype = self.value_type if ftype == "percentage": pval = int(round(val * 100)) if self.type == "negative": pval = 0 - (100 - pval) else: pval -= 100 elif ftype == "additive_percentage": pval = int(round(val * 100)) elif ftype == "inverted_percentage": pval = 100 - int(round(val * 100)) # Can't remember what workaround this was, is it needed? if self.type == "negative": if self.value > 1: pval = 0 - pval elif ftype == "additive" or ftype == "particle_index" or ftype == "account_id": if int(val) == val: pval = int(val) elif ftype == "date": d = time.gmtime(int(val)) pval = time.strftime("%Y-%m-%d %H:%M:%S", d) return u"{0}".format(pval) @property def formatted_description(self): """ Returns a formatted description string (%s* tokens replaced) or None if unavailable """ desc = self.description if desc: return desc.replace("%s1", self.formatted_value) else: return None @property def name(self): """ The attribute's name """ return self._attribute.get("name", str(self.id)) @property def cvar_class(self): """ The attribute class, mostly non-useful except for console usage in some cases """ return self._attribute.get("attribute_class") @property def id(self): """ The attribute ID, used for indexing the description blocks in the schema """ # I'm basically making a pun here, Esky, when you find this. Someday. # You owe me a dollar. return self._attribute.get("defindex", id(self)) @property def type(self): """ Returns the attribute effect type (positive, negative, or neutral). This is not the same as the value type, see 'value_type' """ return self._attribute.get("effect_type") @property def value(self): """ Tries to intelligently return the raw value based on schema data. See also: 'value_int' and 'value_float' """ # TODO: No way to determine which value to use without schema, # could be problem if self._isint: return self.value_int else: return self.value_float @property def value_int(self): try: # This is weird, I know, but so is Valve. # They store floats in value fields sometimes, sometimes not. # Oh and they also store strings in there too now! val = self._attribute.get("value", 0) if not isinstance(val, float): return int(val) else: return float(val) except ValueError: return 0 @property def value_float(self): try: return float(self._attribute.get("float_value", self.value_int)) except ValueError: return 0.0 @property def description(self): """ Returns the attribute's description string, if it is intended to be printed with the value there will be a "%s1" token somewhere in the string. Use 'formatted_description' to build one automatically. """ return self._attribute.get("description_string") @property def value_type(self): """ The attribute's type, note that this is the type of the attribute's value and not its affect on the item (i.e. negative or positive). See 'type' for that. """ redundantprefix = "value_is_" vtype = self._attribute.get("description_format") if vtype and vtype.startswith(redundantprefix): return vtype[len(redundantprefix):] else: return vtype @property def hidden(self): """ True if the attribute is "hidden" (not intended to be shown to the end user). Note that hidden attributes also usually have no description string """ return self._attribute.get("hidden", False) or self.description is None @property def account_info(self): """ Certain attributes have a user's account information associated with it such as a gifted or crafted item. A dict with two keys: 'persona' and 'id64'. None if the attribute has no account information attached to it. """ account_info = self._attribute.get("account_info") if account_info: return {"persona": account_info.get("personaname", ""), "id64": account_info["steamid"]} else: return None def __str__(self): """ Pretty printing """ if not self.hidden: return self.formatted_description else: return self.name + ": " + self.formatted_value def __init__(self, attribute): self._attribute = attribute self._isint = self._attribute.get("stored_as_integer", False) class inventory(object): """ Wrapper around player inventory. """ @property def _inv(self): if self._cache: return self._cache status = None try: status = self._api["result"]["status"] items = self._api["result"]["items"] except KeyError: # Only try to check status code if items don't exist (why error out # when items are there) if status is not None: if status == 8: raise BadID64Error("Bad Steam ID64 given") elif status == 15: raise ProfilePrivateError("Profile is private") raise InventoryError("Backpack data incomplete or corrupt") self._cache = { "items": items, "cells": self._api["result"].get("num_backpack_slots", len(items)) } return self._cache @property def cells_total(self): """ The total number of cells in the inventory. This can be used to determine if the user has bought an expander. This is NOT the number of items in the inventory, but how many items CAN be stored in it. The actual current inventory size can be obtained by calling len on an inventory object """ return self._inv["cells"] def __getitem__(self, key): key = str(key) for item in self: if str(item.id) == key or str(item.original_id) == key: return item raise KeyError(key) def __iter__(self): return next(self) def __len__(self): return len(self._inv["items"]) def __next__(self): iterindex = 0 iterdata = self._inv["items"] while(iterindex < len(iterdata)): data = item(iterdata[iterindex], self._schema) iterindex += 1 yield data next = __next__ def __init__(self, profile, app, schema=None, **kwargs): """ 'profile': A user ID or profile object. 'app': Steam app to get the inventory for. 'schema': The schema to use for item lookup. """ self._app = app self._schema = schema self._cache = {} try: sid = profile.id64 except: sid = str(profile) self._api = api.interface("IEconItems_" + str(self._app)).GetPlayerItems(SteamID=sid, **kwargs) class asset_item: """ Stores a single item from a steam asset catalog """ def __init__(self, asset, catalog): self._catalog = catalog self._asset = asset def __str__(self): return self.name + " " + str(self.price) def _calculate_price(self, base=False): asset = self._asset pricemap = asset["prices"] if base: pricemap = asset.get("original_prices", pricemap) return dict([(currency, float(price) / 100) for currency, price in pricemap.items()]) @property def tags(self): """ Returns a dict containing tags and their localized labels as values """ return dict([(t, self._catalog.tags.get(t, t)) for t in self._asset.get("tags", [])]) @property def base_price(self): """ The price the item normally goes for, not including discounts. """ return self._calculate_price(base=True) @property def price(self): """ Returns the most current price available, which may include sales/discounts """ return self._calculate_price(base = False) @property def name(self): """ The asset "name" which is in fact a schema id of item. """ return self._asset.get("name") class assets(object): """ Class for building asset catalogs """ @property def _assets(self): if self._cache: return self._cache try: assets = dict([(asset["name"], asset) for asset in self._api["result"]["assets"]]) tags = self._api["result"].get("tags", {}) except KeyError: raise AssetError("Empty or corrupt asset catalog") self._cache = { "items": assets, "tags": tags } return self._cache @property def tags(self): """ Returns a dict that is a map of the internal tag names for this catalog to the localized labels. """ return self._assets["tags"] def __contains__(self, key): """ Returns a whether a given asset ID exists within this catalog or not. """ try: key = key.schema_id except AttributeError: pass return str(key) in self._assets["items"] def __getitem__(self, key): """ Returns an 'asset_item' for a given asset ID """ assets = self._assets["items"] try: key = key.schema_id except AttributeError: pass return asset_item(assets[str(key)], self) def __iter__(self): return next(self) def __next__(self): # This was previously sorted, but I don't think order matters here. # Does it? data = list(self._assets["items"].values()) iterindex = 0 while iterindex < len(data): ydata = asset_item(data[iterindex], self) iterindex += 1 yield ydata next = __next__ def __init__(self, app, lang=None, **kwargs): """ lang: Language of asset tags, defaults to english currency: The iso 4217 currency code, returns all currencies by default """ self._language = loc.language(lang).code self._app = app self._cache = {} self._api = api.interface("ISteamEconomy").GetAssetPrices(language=self._language, appid=self._app, **kwargs) Lagg-steamodd-932987b/steam/loc.py000066400000000000000000000046021450602411500167130ustar00rootroot00000000000000""" Localization related code Copyright (c) 2010+, Anthony Garcia Distributed under the ISC License (see LICENSE) """ import os from . import api class LanguageError(api.APIError): pass class LanguageUnsupportedError(LanguageError): pass class language(object): """ Steam API localization tools and reference """ # If there's a new language added feel free to add it here _languages = {"da_DK": "Danish", "nl_NL": "Dutch", "en_US": "English", "fi_FI": "Finnish", "fr_FR": "French", "de_DE": "German", "hu_HU": "Hungarian", "it_IT": "Italian", "ja_JP": "Japanese", "ko_KR": "Korean", "no_NO": "Norwegian", "pl_PL": "Polish", "pt_PT": "Portuguese", "pt_BR": "Brazilian Portuguese", "ro_RO": "Romanian", "ru_RU": "Russian", "zh_CN": "Simplified Chinese", "es_ES": "Spanish", "sv_SE": "Swedish", "zh_TW": "Traditional Chinese", "tr_TR": "Turkish"} _default_language = "en_US" def __init__(self, code=None): """ Raises LanguageUnsupportedError if the code isn't supported by the API or otherwise invalid, uses the default language if no code is given. 'code' is an ISO language code. """ self._code = None if not code: _system_language = os.environ.get("LANG", language._default_language).split('.')[0] if _system_language in language._languages.keys(): self._code = _system_language else: self._code = language._default_language else: code = code.lower() for lcode, lname in language._languages.items(): code_lower = lcode.lower() if code_lower == code or code_lower.split('_')[0] == code: self._code = lcode break try: self._name = language._languages[self._code] except KeyError: self._name = None raise LanguageUnsupportedError(code) @property def code(self): return self._code @property def name(self): return self._name Lagg-steamodd-932987b/steam/remote_storage.py000066400000000000000000000027041450602411500211560ustar00rootroot00000000000000""" Remote storage/UGC Copyright (c) 2010+, Anthony Garcia Distributed under the ISC License (see LICENSE) """ from . import api class UGCError(api.APIError): pass class FileNotFoundError(UGCError): pass class ugc_file(object): """ Resolves a UGC file ID into usable metadata. """ @property def size(self): """ Size in bytes """ return self._data["size"] @property def filename(self): """ Local filename is what the user named it, not the URL """ return self._data["filename"] @property def url(self): """ UGC link """ return self._data["url"] @property def _data(self): if self._cache: return self._cache data = None status = None try: data = self._api["data"] status = self._api["status"]["code"] except KeyError: if not data: if status is not None and status != 9: raise UGCError("Code " + str(status)) else: raise FileNotFoundError("File not found") except api.HTTPFileNotFoundError: raise FileNotFoundError("File not found") self._cache = data return self._cache def __init__(self, appid, ugcid64, **kwargs): self._cache = {} self._api = api.interface("ISteamRemoteStorage").GetUGCFileDetails(ugcid=ugcid64, appid=appid, **kwargs) Lagg-steamodd-932987b/steam/sim.py000066400000000000000000000254621450602411500167350ustar00rootroot00000000000000""" Steam Inventory Manager layer Copyright (c) 2010+, Anthony Garcia Distributed under the ISC License (see LICENSE) """ from xml.sax import saxutils import re import json import operator from urllib.parse import urlencode from . import api from . import items from . import loc class inventory_context(object): """ Builds context data that is fetched from a user's inventory page """ @property def ctx(self): if self._cache: return self._cache try: data = self._downloader.download() contexts = re.search("var g_rgAppContextData = (.+);", data.decode("utf-8")) match = contexts.group(1) self._cache = json.loads(match) except: raise items.InventoryError("No SIM inventory information available for this user") return self._cache def get(self, key): """ Returns context data for a given app, can be an ID or a case insensitive name """ keystr = str(key) res = None try: res = self.ctx[keystr] except KeyError: for k, v in self.ctx.items(): if "name" in v and v["name"].lower() == keystr.lower(): res = v break return res @property def apps(self): """ Returns a list of valid app IDs """ return list(self.ctx.keys()) def __getitem__(self, key): res = self.get(key) if not res: raise KeyError(key) return res def __iter__(self): return next(self) def __next__(self): iterindex = 0 iterdata = sorted(self.ctx.values(), key=operator.itemgetter("appid")) while iterindex < len(iterdata): data = iterdata[iterindex] iterindex += 1 yield data next = __next__ def __init__(self, user, **kwargs): self._cache = {} try: sid = user.id64 except: sid = user self._downloader = api.http_downloader("http://steamcommunity.com/profiles/{0}/inventory/".format(sid), **kwargs) self._user = sid class inventory(object): @property def cells_total(self): """ Returns the total amount of "cells" which in this case is just an amount of items """ return self._inv.get("count_total", len(self)) @property def page_end(self): """ Returns the last asset ID of this page if the inventory continues. Can be passed as page_start arg """ return self._inv.get("last_assetid") @property def pages_continue(self): """ Returns True if pages continue beyond the one loaded in this instance, False otherwise """ return self._inv.get("more", False) def __next__(self): iterindex = 0 classes = self._inv.get("classes", {}) for assetid, data in self._inv.get("items", {}).items(): clsid = data["classid"] + "_" + data["instanceid"] data.update(classes.get(clsid, {})) yield item(data) next = __next__ def __getitem__(self, key): key = str(key) for item in self: if str(item.id) == key or str(item.original_id) == key: return item raise KeyError(key) def __iter__(self): return next(self) def __len__(self): return len(self._inv.get("items", [])) @property def _inv(self): if self._cache: return self._cache invstr = "http://steamcommunity.com/inventory/{0}/{1}/{2}" page_url = invstr.format(self._user, self._app, self._section) page_url_args = {} if self._language: page_url_args["l"] = self._language if self._page_size: page_url_args["count"] = self._page_size if self._page_start: page_url_args["start_assetid"] = self._page_start page_url += "?" + urlencode(page_url_args) req = api.http_downloader(page_url, timeout=self._timeout) inventorysection = json.loads(req.download().decode("utf-8")) if not inventorysection: raise items.InventoryError("Empty context data returned") itemdescs = inventorysection.get("descriptions") inv = inventorysection.get("assets") if not itemdescs: raise items.InventoryError("No classes in inv output") if not inv: raise items.InventoryError("No assets in inv output") descs = {} items = {} for desc in itemdescs: descs[desc["classid"] + "_" + desc["instanceid"]] = desc for item in inv: items[item["assetid"]] = item self._cache = { "classes": descs, "items": items, "app": self._app, "section": self._section, "more": inventorysection.get("more_items", False), "count_total": inventorysection.get("total_inventory_count"), "last_assetid": inventorysection.get("last_assetid") } return self._cache def __init__(self, profile, app, section, page_start=None, page_size=2000, timeout=None, lang=None): """ 'profile': User ID or user object 'app': Steam app to get the inventory for 'section': Inventory section to operatoe on 'page_start': Asset ID to use as first item in inv chunk 'page_size': How many assets should be in a page """ self._app = app self._cache = {} self._page_size = page_size self._page_start = page_start self._section = section self._timeout = timeout or api.socket_timeout.get() self._language = loc.language(lang).name.lower() if not app: raise items.InventoryError("No inventory available") try: sid = profile.id64 except AttributeError: sid = profile self._user = sid class item_attribute(items.item_attribute): @property def value_type(self): # Because Valve uses this same data on web pages, it's /probably/ # trustworthy, so long as they have fixed all the XSS bugs... return "html" @property def description(self): desc = self.value if desc: return saxutils.unescape(desc) else: return " " @property def description_color(self): """ Returns description color as an RGB tuple """ return self._attribute.get("color") @property def type(self): return self._attribute.get("type") @property def value(self): return self._attribute.get("value") def __init__(self, attribute): super(item_attribute, self).__init__(attribute) class item(items.item): @property def background_color(self): """ Returns the color associated with the item as a hex RGB tuple """ return self._item.get("background_color") @property def name(self): name = self._item.get("market_name") if not name: name = self._item["name"] return saxutils.unescape(name) @property def custom_name(self): name = saxutils.unescape(self._item["name"]) if name.startswith("''"): return name.strip("''") @property def name_color(self): """ Returns the name color as an RGB tuple """ return self._item.get("name_color") @property def full_name(self): return self.custom_name or self.name @property def hash_name(self): """ The URL-friendly identifier for the item. Generates its own approximation if one isn't available """ name = self._item.get("market_hash_name") if not name: name = "{0.appid}-{0.name}".format(self) return name @property def tool_metadata(self): return self._item.get("app_data") @property def tags(self): """ A list of tags attached to the item if applicable, format is subject to change """ return self._item.get("tags") @property def tradable(self): return self._item.get("tradable") @property def craftable(self): for attr in self: desc = attr.description if desc.startswith("( Not") and desc.find("Usable in Crafting"): return False return True @property def quality(self): """ Can't really trust presence of a schema here, but there is an ID sometimes """ try: qid = int((self.tool_metadata or {}).get("quality", 0)) except: qid = 0 # We might be able to get the quality strings from the item's tags internal_name, name = "normal", "Normal" if self.tags: tags = {x.get('category'): x for x in self.tags} if 'Quality' in tags: internal_name, name = tags['Quality'].get('internal_name'), tags['Quality'].get('name') return qid, internal_name, name @property def quantity(self): return int(self._item["amount"]) @property def attributes(self): # Use descriptions here, with alternative attribute class descs = self._item.get("descriptions") or [] if descs: return [item_attribute(attr) for attr in descs] else: return [] @property def position(self): return self._item["pos"] @property def schema_id(self): """ This *will* return none if there is no schema ID, since it's a valve specific concept for the most part """ try: return int((self.tool_metadata or {}).get("def_index")) except TypeError: return None @property def type(self): return self._item.get("type", '') def _scaled_image_url(self, dims): urlbase = self._item.get("icon_url") if urlbase: cdn = "http://cdn.steamcommunity.com/economy/image/" return cdn + urlbase + '/' + dims else: return '' @property def icon(self): return self._scaled_image_url("96fx96f") @property def image(self): return self._scaled_image_url("512fx512f") @property def id(self): return int(self._item["assetid"]) @property def slot_name(self): # (present sometimes in the form of tags) TODO for tag in self._get_category("Type"): return tag["name"] @property def appid(self): """ Return the app ID that this item belongs to """ return self._item["appid"] def _get_category(self, name): cats = [] if self.tags: for tag in self.tags: if tag["category"] == name: cats.append(tag) return cats def __init__(self, theitem): super(item, self).__init__(theitem) Lagg-steamodd-932987b/steam/user.py000066400000000000000000000333461450602411500171230ustar00rootroot00000000000000""" Steam profile/account reading and ID resolution Copyright (c) 2010+, Anthony Garcia Distributed under the ISC License (see LICENSE) """ import time import os from . import api class ProfileError(api.APIError): pass class ProfileNotFoundError(ProfileError): pass class VanityError(ProfileError): pass class BansError(ProfileError): pass class BansNotFoundError(BansError): pass class vanity_url(object): """ Class for holding a vanity URL and its id64 """ @property def id64(self): if self._cache: return self._cache res = None try: res = self._api["response"] self._cache = int(res["steamid"]) except KeyError: if not self._cache: if res: raise VanityError(res.get("message", "Invalid vanity response")) else: raise VanityError("Empty vanity response") return self._cache def __str__(self): return str(self.id64) def __init__(self, vanity, **kwargs): """ Takes a vanity URL part and tries to resolve it. """ vanity = os.path.basename(str(vanity).strip('/')) self._cache = None self._api = api.interface("ISteamUser").ResolveVanityURL(vanityurl=vanity, **kwargs) class profile(object): """ Functions for reading user account data """ @property def id64(self): """ Returns the 64 bit steam ID (use with other API requests) """ return int(self._prof["steamid"]) @property def id32(self): """ Returns the 32 bit steam ID """ return int(self.id64) - 76561197960265728 @property def persona(self): """ Returns the user's persona (what you usually see in-game) """ return self._prof["personaname"] @property def profile_url(self): """ Returns a URL to the user's Community profile page """ return self._prof["profileurl"] @property def vanity(self): """ Returns the user's vanity url if it exists, None otherwise """ purl = self.profile_url.strip('/') if purl.find("/id/") != -1: return os.path.basename(purl) @property def avatar_small(self): return self._prof["avatar"] @property def avatar_medium(self): return self._prof["avatarmedium"] @property def avatar_large(self): return self._prof["avatarfull"] @property def status(self): """ Returns the user's status. 0: offline 1: online 2: busy 3: away 4: snooze 5: looking to trade 6: looking to play If player's profile is private, this will always be 0. """ return self._prof["personastate"] @property def persona_state_flags(self): """ personastateflags 0: NONE 1: Has Rich Presence 2: In Joinable Game 4: Golden 8: Remote Play Together 256: Client Type Web 512: Client Type Mobile 1024: Client Type Tenfoot 2048: Client Type VR 4096: Launch Type Gamepad 8182: Launch Type CompatTool """ return self._prof["personastateflags"] @property def visibility(self): """ Returns the visibility setting of the profile. 1: private 2: friends only 3: public """ return self._prof["communityvisibilitystate"] @property def configured(self): """ Returns true if the user has created a Community profile """ return bool(self._prof.get("profilestate")) @property def last_online(self): """ Returns the last time the user was online as a localtime time.struct_time struct """ return time.localtime(self._prof["lastlogoff"]) @property def comments_enabled(self): """ Returns true if the profile allows public comments """ return bool(self._prof.get("commentpermission")) @property def real_name(self): """ Returns the user's real name if it's set and public """ return self._prof.get("realname") @property def primary_group(self): """ Returns the user's primary group ID if set. """ return self._prof.get("primaryclanid") @property def creation_date(self): """ Returns the account creation date as a localtime time.struct_time struct if public""" timestamp = self._prof.get("timecreated") if timestamp: return time.localtime(timestamp) @property def current_game(self): """ Returns a tuple of 3 elements (each of which may be None if not available): Current game app ID, server ip:port, misc. extra info (eg. game title) """ obj = self._prof gameid = obj.get("gameid") gameserverip = obj.get("gameserverip") gameextrainfo = obj.get("gameextrainfo") return (int(gameid) if gameid else None, gameserverip, gameextrainfo) @property def location(self): """ Returns a tuple of 2 elements (each of which may be None if not available): State ISO code, country ISO code """ obj = self._prof return (obj.get("locstatecode"), obj.get("loccountrycode")) @property def lobbysteamid(self): """ Returns a lobbynumber as int from few Source games or 0 if not in lobby. """ return int(self._prof.get("lobbysteamid", 0)) @property def _prof(self): if not self._cache: try: res = self._api["response"]["players"] try: self._cache = res[0] except IndexError: raise ProfileNotFoundError("Profile not found") except KeyError: raise ProfileError("Bad player profile results returned") return self._cache @property def level(self): """ Returns the the user's profile level, note that this runs a separate request because the profile level data isn't in the standard player summary output even though it should be. Which is also why it's not implemented as a separate class. You won't need this output and not the profile output """ level_key = "player_level" if level_key in self._api["response"]: return self._api["response"][level_key] try: lvl = api.interface("IPlayerService").GetSteamLevel(steamid=self.id64)["response"][level_key] self._api["response"][level_key] = lvl return lvl except: return -1 @classmethod def from_def(cls, obj): """ Builds a profile object from a raw player summary object """ prof = cls(obj["steamid"]) prof._cache = obj return prof def __str__(self): return self.persona or str(self.id64) def __init__(self, sid, **kwargs): """ Creates a profile instance for the given user """ try: sid = sid.id64 except AttributeError: sid = os.path.basename(str(sid).strip('/')) self._cache = {} self._api = api.interface("ISteamUser").GetPlayerSummaries(version=2, steamids=sid, **kwargs) class _batched_request(object): """ Base class for implementations that support multiple results per request (for example GetPlayerSummaries takes multiple id64s) """ def __init__(self, batch, batchsize=100): self._batches = [] batchlen, rem = divmod(len(batch), batchsize) if rem > 0: batchlen += 1 for i in range(batchlen): offset = i * batchsize batch_chunk = batch[offset:offset + batchsize] self._batches.append(list(self._process_batch(batch_chunk))) def _process_batch(self, batch): """ Process the given batch and return an iterable """ return batch def _call_method(self, batch): """ Call the desired method for the given batch and return the processed results as an iterable """ raise NotImplementedError def __iter__(self): return next(self) def __next__(self): for batch in self._batches: for result in self._call_method(batch): yield result next = __next__ class profile_batch(_batched_request): def __init__(self, sids): """ Fetches user profiles en masse and generates 'profile' objects. The length of the ID list can be indefinite, separate requests will be made if the length exceeds the API's ID cap and the list split into batches. """ super(profile_batch, self).__init__(sids) def _process_batch(self, batch): processed = set() for sid in batch: try: sid = sid.id64 except AttributeError: sid = os.path.basename(str(sid).strip('/')) processed.add(str(sid)) return processed def _call_method(self, batch): response = api.interface("ISteamUser").GetPlayerSummaries(version=2, steamids=','.join(batch)) return [profile.from_def(player) for player in response["response"]["players"]] class bans(object): def __init__(self, sid, **kwargs): """ Fetch user ban information """ try: sid = sid.id64 except AttributeError: sid = os.path.basename(str(sid).strip('/')) self._cache = {} self._api = api.interface("ISteamUser").GetPlayerBans(steamids=sid, **kwargs) @property def _bans(self): if not self._cache: try: res = self._api["players"] try: self._cache = res[0] except IndexError: raise BansNotFoundError("No ban results for this profile") except KeyError: raise BansError("Bad ban data returned") return self._cache @property def id64(self): return int(self._bans["SteamId"]) @property def community(self): """ Community banned """ return self._bans["CommunityBanned"] @property def vac(self): """ User is currently VAC banned """ return self._bans["VACBanned"] @property def vac_count(self): """ Number of VAC bans on record """ return self._bans["NumberOfVACBans"] @property def days_unbanned(self): """ Number of days since the last ban. Note that users without a ban on record will have this set to 0 so make sure to test bans.vac """ return self._bans["DaysSinceLastBan"] @property def economy(self): """ Economy ban status which is a string for whatever reason """ return self._bans["EconomyBan"] @property def game_count(self): """ Number of bans in games, this includes CS:GO Overwatch bans """ return self._bans["NumberOfGameBans"] @classmethod def from_def(cls, obj): instance = cls(int(obj["SteamId"])) instance._cache = obj return instance class bans_batch(_batched_request): def __init__(self, sids): super(bans_batch, self).__init__(sids) def _process_batch(self, batch): processed = set() for sid in batch: try: sid = sid.id64 except AttributeError: sid = os.path.basename(str(sid).strip('/')) processed.add(str(sid)) return processed def _call_method(self, batch): response = api.interface("ISteamUser").GetPlayerBans(steamids=','.join(batch)) return [bans.from_def(player) for player in response["players"]] class friend(object): """ Class used to store friend obtained from GetFriendList. """ def __init__(self, friend_dict): self._friend_dict = friend_dict @property def steamid(self): """ Returns the 64 bit Steam ID """ return int(self._friend_dict["steamid"]) @property def relationship(self): """ Returns relationship qualifier """ return self._friend_dict["relationship"] @property def since(self): """ Returns date when relationship was created as a localtime time.struct_time """ return time.localtime(self._friend_dict["friend_since"]) class friend_list(object): """ Creates an iterator of friend objects fetched from given user's Steam ID. Allows for filtering by specyfing relationship argument in constructor, but API seems to always return items with friend relationship. Possible filter values: all, friend. """ def __init__(self, sid, relationship="all", **kwargs): try: sid = sid.id64 except AttributeError: sid = os.path.basename(str(sid).strip('/')) self._api = api.interface("ISteamUser").GetFriendList(steamid=sid, relationship=relationship, **kwargs) try: self._friends = self._api["friendslist"]["friends"] except api.HTTPFileNotFoundError: raise ProfileNotFoundError("Profile not found") except api.HTTPInternalServerError: raise ProfileNotFoundError("Invalid Steam ID given") self.index = 0 @property def count(self): """ Returns number of friends """ return len(self._friends) def __iter__(self): return self def __next__(self): if self.index < len(self._friends): self.index += 1 return friend(self._friends[self.index - 1]) else: self.index = 0 raise StopIteration next = __next__ Lagg-steamodd-932987b/steam/vdf.py000066400000000000000000000130271450602411500167160ustar00rootroot00000000000000""" VDF (de)serialization Copyright (c) 2010+, Anthony Garcia Distributed under the ISC License (see LICENSE) """ STRING = '"' NODE_OPEN = '{' NODE_CLOSE = '}' BR_OPEN = '[' BR_CLOSE = ']' COMMENT = '/' CR = '\r' LF = '\n' SPACE = ' ' TAB = '\t' WHITESPACE = set(' \t\r\n') try: from collections import OrderedDict as odict except ImportError: odict = dict def _symtostr(line, i, token=STRING): opening = i + 1 closing = 0 ci = line.find(token, opening) while ci != -1: if line[ci - 1] != '\\': closing = ci break ci = line.find(token, ci + 1) finalstr = line[opening:closing] return finalstr, i + len(finalstr) + 1 def _unquotedtostr(line, i): ci = i _len = len(line) while ci < _len: if line[ci] in WHITESPACE: break ci += 1 return line[i:ci], ci def _parse(stream, ptr=0): i = ptr laststr = None lasttok = None lastbrk = None next_is_value = False deserialized = {} while i < len(stream): c = stream[i] if c == NODE_OPEN: next_is_value = False # Make sure next string is interpreted as a key. if laststr in deserialized.keys(): # If this key already exists then we need to make it a list and append the current value. if type(deserialized[laststr]) is not list: # If the value already set is not a list, let's make it one. deserialized[laststr] = [deserialized[laststr]] # Append the current value to the list _value, i = _parse(stream, i + 1) deserialized[laststr].append(_value) else: # Key is brand new! deserialized[laststr], i = _parse(stream, i + 1) elif c == NODE_CLOSE: return deserialized, i elif c == BR_OPEN: lastbrk, i = _symtostr(stream, i, BR_CLOSE) elif c == COMMENT: if (i + 1) < len(stream) and stream[i + 1] == '/': i = stream.find('\n', i) elif c == CR or c == LF: ni = i + 1 if ni < len(stream) and stream[ni] == LF: i = ni if lasttok != LF: c = LF elif c != SPACE and c != TAB: string, i = ( _symtostr if c == STRING else _unquotedtostr)(stream, i) if lasttok == STRING and next_is_value: if laststr in deserialized and lastbrk is not None: # ignore this entry if it's the second bracketed expression lastbrk = None else: if laststr in deserialized.keys(): # If this key already exists then we're dealing with a list of items if type(deserialized[laststr]) is not list: # If the existing val is not a list, we need to cast it to one. deserialized[laststr] = [deserialized[laststr]] # Append current val to list deserialized[laststr].append(string) else: # First occurence of laststr in deserialized. Assign the value as normal deserialized[laststr] = string # force c = STRING so that lasttok will be set properly c = STRING laststr = string next_is_value = not next_is_value else: c = lasttok lasttok = c i += 1 return deserialized, i def _run_parse_encoded(string): try: encoded = bytearray(string, "utf-16") except: encoded = bytearray(string) # Already byte object? try: encoded = encoded.decode("ascii") except UnicodeDecodeError: try: encoded = encoded.decode("utf-8") except: encoded = encoded.decode("utf-16") except UnicodeEncodeError: pass # Likely already decoded res, ptr = _parse(encoded) return res def load(stream): """ Deserializes `stream` containing VDF document to Python object. """ return _run_parse_encoded(stream.read()) def loads(string): """ Deserializes `string` containing VDF document to Python object. """ return _run_parse_encoded(string) indent = 0 mult = 2 def _i(): return u' ' * (indent * mult) def _dump(obj): nodefmt = u'\n' + _i() + '"{0}"\n' + _i() + '{{\n{1}' + _i() + '}}\n\n' podfmt = _i() + '"{0}" "{1}"\n' lstfmt = _i() + (' ' * mult) + '"{0}" "1"' global indent indent += 1 nodes = [] for k, v in obj.items(): if isinstance(v, dict): nodes.append(nodefmt.format(k, _dump(v))) else: try: try: v.isdigit nodes.append(podfmt.format(k, v)) except AttributeError: lst = map(lstfmt.format, v) nodes.append(nodefmt.format(k, u'\n'.join(lst) + '\n')) except TypeError: nodes.append(podfmt.format(k, v)) indent -= 1 return u''.join(nodes) def _run_dump(obj): res = _dump(obj) return res.encode("utf-16") def dump(obj, stream): """ Serializes `obj` as VDF formatted stream to `stream` object, encoded as UTF-16 by default. """ stream.write(_run_dump(obj)) def dumps(obj): """ Serializes `obj` as VDF formatted string, encoded as UTF-16 by default. """ return _run_dump(obj) Lagg-steamodd-932987b/tests/000077500000000000000000000000001450602411500156135ustar00rootroot00000000000000Lagg-steamodd-932987b/tests/__init__.py000066400000000000000000000000001450602411500177120ustar00rootroot00000000000000Lagg-steamodd-932987b/tests/testapps.py000066400000000000000000000007121450602411500200300ustar00rootroot00000000000000import unittest from steam import apps class AppsTestCase(unittest.TestCase): @classmethod def setUpClass(cls): cls._apps = apps.app_list() def test_builtin_consistency(self): for app, name in self._apps._builtin.items(): self.assertIn(app, self._apps) self.assertEquals((app, name), self._apps[app]) def test_invalid_app(self): self.assertRaises(KeyError, self._apps.__getitem__, 12345678) Lagg-steamodd-932987b/tests/testitems.py000066400000000000000000000143151450602411500202120ustar00rootroot00000000000000import unittest import re from steam import items from steam import sim class BaseTestCase(unittest.TestCase): TEST_APP = (440, 'en_US') # TF2 English catalog ITEM_IN_CATALOG = 344 # Crocleather Slouch ITEM_NOT_IN_CATALOG = 1 # Bottle TEST_ID64 = 76561198811195748 # lagg-bot test acct TEST_APP_NO_TAGS = (570, 'en_US') # Dota 2 English catalog ITEM_IN_NO_TAGS_CATALOG = 4097 # Arctic Hunter's Ice Axe class AssetTestCase(BaseTestCase): def test_asset_contains(self): assets = items.assets(*self.TEST_APP) self.assertTrue(self.ITEM_IN_CATALOG in assets) self.assertFalse(self.ITEM_NOT_IN_CATALOG in assets) schema = items.schema(*self.TEST_APP) self.assertTrue(schema[self.ITEM_IN_CATALOG] in assets) self.assertFalse(schema[self.ITEM_NOT_IN_CATALOG] in assets) def test_asset_has_tags(self): assets_with_tags = items.assets(*self.TEST_APP) self.assertGreater(len(assets_with_tags.tags), 0) def test_asset_has_no_tags(self): assets_without_tags = items.assets(*self.TEST_APP_NO_TAGS) self.assertEqual(len(assets_without_tags.tags), 0) def test_asset_item_has_tags(self): assets_with_tags = items.assets(*self.TEST_APP) asset_item_with_tags = assets_with_tags[self.ITEM_IN_CATALOG] self.assertGreater(len(asset_item_with_tags.tags), 0) def test_asset_item_has_no_tags(self): assets_without_tags = items.assets(*self.TEST_APP_NO_TAGS) asset_item_without_tags = assets_without_tags[self.ITEM_IN_NO_TAGS_CATALOG] self.assertEqual(len(asset_item_without_tags.tags), 0) class InventoryBaseTestCase(BaseTestCase): _inv_cache = None _schema_cache = None _sim_cache = None @property def _inv(self): if not InventoryBaseTestCase._inv_cache: InventoryBaseTestCase._inv_cache = items.inventory(self.TEST_ID64, self.TEST_APP[0], self._schema) return InventoryBaseTestCase._inv_cache @property def _schema(self): if not InventoryBaseTestCase._schema_cache: InventoryBaseTestCase._schema_cache = items.schema(*self.TEST_APP) return InventoryBaseTestCase._schema_cache @property def _sim(self): if not InventoryBaseTestCase._sim_cache: InventoryBaseTestCase._sim_cache = sim.inventory(self.TEST_ID64, 440, 2, None, 2000) return InventoryBaseTestCase._sim_cache class ItemTestCase(InventoryBaseTestCase): def test_position(self): for item in self._inv: self.assertLessEqual(item.position, self._inv.cells_total) def test_equipped(self): for item in self._inv: self.assertNotIn(0, item.equipped.keys()) self.assertNotIn(65535, item.equipped.values()) def test_name(self): # Since sim names are generated by Valve we'll test against those for consistency # steamodd adds craft numbers to all names, valve doesn't, so they should be stripped # steamodd doesn't add crate series to names, valve does, so they should be stripped as well cn_exp = re.compile(r" (?:Series )?#\d+$") sim_names = set() for item in self._sim: # Removes quotes in case of custom name (steamodd leaves that aesthetic choice to the user) name = item.full_name.strip("'") # Don't even bother with strange items right now. I'm tired of unit tests failing whenever # the inventory does and no one from valve responds when I tell them of the issue. If it does # get fixed feel free to remove this as it is definitely a WORKAROUND. # Removes quotes in case of custom name (steamodd leaves that aesthetic choice to the user) if not name.startswith("Strange "): sim_names.add(cn_exp.sub('', name)) # See the above WORKAROUND about strange items and remove if/when it's fixed. our_names = set([cn_exp.sub('', item.custom_name or item.full_name) for item in self._inv if item.quality[1] != "strange" or item.custom_name]) self.assertEqual(our_names, sim_names) def test_attributes(self): # Similarly to the name, we'll test against Valve's strings to check for consistency in the math. schema_attr_exps = [] for attr in self._schema.attributes: if not attr.description: continue desc = attr.description.strip() exp = re.escape(desc).replace("\\%s1", r"[\d-]+") schema_attr_exps.append(re.compile(exp)) sim_attrs = {} for item in self._sim: sim_attrs.setdefault(item.id, set()) for attr in item: # Due to lack of contextual data, we'll have to do fuzzy matching to separate actual attrs from fluff/descriptions desc = attr.description.strip() if desc: # Stop processing if we hit item set attrs, for now if desc.startswith("Item Set Bonus:"): break # Valve for some reason insists on this being attached by the client, since they're not actually attached we skip it. if desc == "Given to valuable Community Contributors": continue for exp in schema_attr_exps: if exp.match(desc): sim_attrs[item.id].add(desc) break for item in self._inv: # Ignore hidden, special (for now) and date values (timestamp formatting is an eternal battle, let it not be fought on these hallowed testgrounds) attrs = set([attr.formatted_description for attr in item if not attr.hidden and not attr.formatted_description.startswith("Attrib_") and attr.value_type not in ("date", "particle_index")]) self.assertTrue(item.id in sim_attrs) self.assertEqual(attrs, sim_attrs[item.id]) class InventoryTestCase(InventoryBaseTestCase): def test_cell_count(self): self.assertLessEqual(len(list(self._inv)), self._inv.cells_total) Lagg-steamodd-932987b/tests/testloc.py000066400000000000000000000017221450602411500176440ustar00rootroot00000000000000import unittest import os import steam class LocTestCase(unittest.TestCase): DEFAULT_LANGUAGE_CODE = "en_US" VALID_LANGUAGE_CODE = "fi_FI" INVALID_LANGUAGE_CODE = "en_GB" def test_supported_language(self): lang = steam.loc.language(LocTestCase.VALID_LANGUAGE_CODE) self.assertEquals(lang._code, LocTestCase.VALID_LANGUAGE_CODE) def test_unsupported_language(self): self.assertRaises(steam.loc.LanguageUnsupportedError, steam.loc.language, LocTestCase.INVALID_LANGUAGE_CODE) def test_supported_language_via_environ(self): os.environ['LANG'] = LocTestCase.VALID_LANGUAGE_CODE lang = steam.loc.language(None) self.assertEquals(lang._code, LocTestCase.VALID_LANGUAGE_CODE) def test_unsupported_language_via_environ(self): os.environ['LANG'] = LocTestCase.INVALID_LANGUAGE_CODE lang = steam.loc.language(None) self.assertEquals(lang._code, LocTestCase.DEFAULT_LANGUAGE_CODE) Lagg-steamodd-932987b/tests/testremote_storage.py000066400000000000000000000022361450602411500221070ustar00rootroot00000000000000import unittest try: from urllib.parse import urlparse except ImportError: from urllib2 import urlparse urlparse = urlparse.urlparse from steam import remote_storage class RemoteStorageTestCase(unittest.TestCase): APPID = 440 INVALID_UGCID = "wobwobwobwob" VALID_UGCID = 650994986817657344 VALID_UGC_SIZE = 134620 VALID_UGC_FILENAME = "steamworkshop/tf2/_thumb.jpg" VALID_UGC_PATH = "/ugc/650994986817657344/D2ADAD7F19BFA9A99BD2B8850CC317DC6BA01BA9/" #Silly tea hat made by RJ @classmethod def setUpClass(cls): cls._test_file = remote_storage.ugc_file(cls.APPID, cls.VALID_UGCID) def test_invalid_ugcid(self): ugc_file = remote_storage.ugc_file(self.APPID, self.INVALID_UGCID) self.assertRaises(remote_storage.FileNotFoundError) def test_valid_ugcid_filename(self): self.assertEqual(self._test_file.filename, self.VALID_UGC_FILENAME) def test_valid_ugcid_size(self): self.assertEqual(self._test_file.size, self.VALID_UGC_SIZE) def test_valid_ugcid_url(self): parsed_url = urlparse(self._test_file.url) self.assertEqual(parsed_url.path, self.VALID_UGC_PATH) Lagg-steamodd-932987b/tests/testuser.py000066400000000000000000000063401450602411500200460ustar00rootroot00000000000000import unittest from steam import user from steam import api class ProfileTestCase(unittest.TestCase): VALID_ID64 = 76561198811195748 VALID_ID32 = 850930020 INVALID_ID64 = 123 WEIRD_ID64 = (VALID_ID64 >> 33 << 33) ^ VALID_ID64 VALID_VANITY = "spacecadet01" INVALID_VANITY = "*F*SDF9" class VanityTestCase(ProfileTestCase): def test_invalid_vanity(self): vanity = user.vanity_url(self.INVALID_VANITY) self.assertRaises(user.VanityError, lambda: vanity.id64) def test_pathed_vanity(self): vanity = user.vanity_url('/' + self.VALID_VANITY + '/') self.assertEqual(vanity.id64, ProfileTestCase.VALID_ID64) def test_valid_vanity(self): vanity = user.vanity_url(self.VALID_VANITY) self.assertEqual(vanity.id64, ProfileTestCase.VALID_ID64) class ProfileIdTestCase(ProfileTestCase): def test_invalid_id(self): profile = user.profile(self.INVALID_ID64) self.assertRaises(user.ProfileNotFoundError, lambda: profile.id64) def test_pathed_id(self): profile = user.profile('/' + str(self.VALID_ID64) + '/') self.assertEqual(profile.id64, self.VALID_ID64) def test_valid_id(self): profile = user.profile(self.VALID_ID64) self.assertEqual(profile.id64, self.VALID_ID64) self.assertEqual(profile.id32, self.VALID_ID32) def test_weird_id(self): profile = user.profile(self.WEIRD_ID64) self.assertRaises(user.ProfileNotFoundError, lambda: profile.id64) class ProfileLevelTestCase(ProfileTestCase): def test_level(self): profile = user.profile(self.VALID_ID64) self.assertGreater(profile.level, 0) class ProfileBatchTestCase(ProfileTestCase): def test_big_list(self): # Test id64 now lagg-bot test account, might need friends list adds # TODO: Implement GetFriendList in steamodd proper friends = api.interface("ISteamUser").GetFriendList(steamid = self.VALID_ID64) testsids = [friend["steamid"] for friend in friends["friendslist"]["friends"]] self.assertEqual(set(testsids), set(map(lambda x: str(x.id64), user.profile_batch(testsids)))) self.assertEqual(set(testsids), set(map(lambda x: str(x.id64), user.bans_batch(testsids)))) def test_compatibility(self): userlist = [self.VALID_ID64, user.vanity_url("windpower"), user.vanity_url("rjackson"), user.profile(self.VALID_ID64)] resolvedids = set() for u in userlist: try: sid = u.id64 except AttributeError: sid = str(u) resolvedids.add(str(sid)) self.assertEqual(resolvedids, set(map(lambda x: str(x.id64), user.profile_batch(userlist)))) self.assertEqual(resolvedids, set(map(lambda x: str(x.id64), user.bans_batch(userlist)))) class FriendListTestCase(ProfileTestCase): def test_sids(self): profile_batch_friends = api.interface("ISteamUser").GetFriendList(steamid = self.VALID_ID64) profile_batch_testsids = [friend["steamid"] for friend in profile_batch_friends["friendslist"]["friends"]] friend_list = user.friend_list(self.VALID_ID64) self.assertEqual(set(profile_batch_testsids), set(map(lambda x: str(x.steamid), friend_list))) Lagg-steamodd-932987b/tests/testvdf.py000066400000000000000000000154121450602411500176470ustar00rootroot00000000000000import unittest from steam import vdf class SyntaxTestCase(unittest.TestCase): # Deserialization UNQUOTED_VDF = """ node { key value } """ QUOTED_VDF = """ "node" { "key" "value" } """ MACRO_UNQUOTED_VDF = """ node { key value [$MACRO] } """ MACRO_QUOTED_VDF = """ "node" { "key" "value" [$MACRO] } """ COMMENT_QUOTED_VDF = """ "node" { // Hi I'm a comment. "key" "value" [$MACRO] } """ SUBNODE_QUOTED_VDF = """ "node" { "subnode" { "key" "value" } } """ MIXED_VDF = """ node { "key" value key2 "value" "key3" "value" [$MACRO] // Comment "subnode" [$MACRO] { key value } "key4" "value" } """ MULTIKEY_KV = """ node { "key" "k1v1" "key" "k1v2" "key" "k1v3" "key2" "k2v1" "key3" "k3v1" "key3" "k3v2" } """ MULTIKEY_KNODE = """ node { "key" { "name" "k1v1" "extra" ":D" } "key" { "name" "k1v2" "extra" ":!" } "key" { "name" "k1v3" "extra" ":O" } "key2" { "name" "k2v1" "extra" "I'm lonely" } "key3" { "name" "k3v1" "extra" { "smiley" ":O" "comment" "Wow!" } } "key3" { "name" "k3v2" "extra" { "smiley" ":Z" "comment" "BZZ!" } } } """ # Expectations EXPECTED_DICT = { u"node": { u"key": u"value" } } EXPECTED_SUBNODE_DICT = { u"node": { u"subnode": { u"key": u"value" } } } EXPECTED_MIXED_DICT = { u"node": { u"key": u"value", u"key2": u"value", u"key3": u"value", u"subnode": { u"key": u"value" }, u"key4": u"value" } } EXPECTED_MULTIKEY_KV = { u"node": { u"key": [ u"k1v1", u"k1v2", u"k1v3" ], u"key2": u"k2v1", u"key3": [ u"k3v1", u"k3v2" ] } } EXPECTED_MULTIKEY_KNODE = { u"node": { u"key": [ { u"name": u"k1v1", u"extra": u":D" }, { u"name": u"k1v2", u"extra": u":!" }, { u"name": u"k1v3", u"extra": u":O" } ], u"key2": { u"name": u"k2v1", u"extra": u"I'm lonely", }, u"key3": [ { u"name": u"k3v1", u"extra": { u"smiley": u":O", u"comment": u"Wow!" } }, { u"name": u"k3v2", u"extra": { u"smiley": u":Z", u"comment": u"BZZ!" } } ] } } # Serialization SIMPLE_DICT = EXPECTED_DICT SUBNODE_DICT = EXPECTED_SUBNODE_DICT ARRAY_DICT = { u"array": [ u"a", u"b", u"c"] } NUMERICAL_DICT = { u"number": 1, u"number2": 2 } COMBINATION_DICT = { u"node": { u"key": u"value", u"subnode": { u"key": u"value" }, u"array": [u"a", u"b", u"c", 1, 2, 3], u"number": 1024 } } # Expectations EXPECTED_SIMPLE_DICT = SIMPLE_DICT EXPECTED_SUBNODE_DICT = SUBNODE_DICT EXPECTED_ARRAY_DICT = { "array": { "a": "1", "b": "1", "c": "1" } } EXPECTED_NUMERICAL_DICT = { "number": "1", "number2": "2" } EXPECTED_COMBINATION_DICT = { "node": { "key": "value", "subnode": { "key": "value" }, "array": { "a": "1", "b": "1", "c": "1", "1": "1", "2": "1", "3": "1" }, "number": "1024" } } class DeserializeTestCase(SyntaxTestCase): def test_unquoted(self): self.assertEqual(self.EXPECTED_DICT, vdf.loads(self.UNQUOTED_VDF)) def test_quoted(self): self.assertEqual(self.EXPECTED_DICT, vdf.loads(self.QUOTED_VDF)) def test_macro_unquoted(self): self.assertEqual(self.EXPECTED_DICT, vdf.loads(self.MACRO_UNQUOTED_VDF)) def test_macro_quoted(self): self.assertEqual(self.EXPECTED_DICT, vdf.loads(self.MACRO_QUOTED_VDF)) def test_comment_quoted(self): self.assertEqual(self.EXPECTED_DICT, vdf.loads(self.COMMENT_QUOTED_VDF)) def test_subnode_quoted(self): self.assertEqual(self.EXPECTED_SUBNODE_DICT, vdf.loads(self.SUBNODE_QUOTED_VDF)) def test_mixed(self): self.assertEqual(self.EXPECTED_MIXED_DICT, vdf.loads(self.MIXED_VDF)) def test_multikey_kv(self): self.assertEqual(self.EXPECTED_MULTIKEY_KV, vdf.loads(self.MULTIKEY_KV)) def test_multikey_knode(self): self.maxDiff = 80*80 self.assertEqual(self.EXPECTED_MULTIKEY_KNODE, vdf.loads(self.MULTIKEY_KNODE)) class SerializeTestCase(SyntaxTestCase): def test_simple_dict(self): self.assertEqual(self.EXPECTED_SIMPLE_DICT, vdf.loads(vdf.dumps(self.SIMPLE_DICT))) def test_subnode_dict(self): self.assertEqual(self.EXPECTED_SUBNODE_DICT, vdf.loads(vdf.dumps(self.SUBNODE_DICT))) def test_array_dict(self): self.assertEqual(self.EXPECTED_ARRAY_DICT, vdf.loads(vdf.dumps(self.ARRAY_DICT))) def test_numerical_dict(self): self.assertEqual(self.EXPECTED_NUMERICAL_DICT, vdf.loads(vdf.dumps(self.NUMERICAL_DICT))) def test_combination_dict(self): self.assertEqual(self.EXPECTED_COMBINATION_DICT, vdf.loads(vdf.dumps(self.COMBINATION_DICT)))