From d6a06aa697e02346852504f40c038972ee5d51a3 Mon Sep 17 00:00:00 2001 From: bvn13 Date: Sat, 7 Dec 2024 01:17:53 +0300 Subject: [PATCH] working client --- .gitignore | 105 ++++ .readthedocs.yml | 22 + LICENSE | 165 ++++++ README.md | 22 + dev-requirements.txt | 5 + devtools/luaconsole.py | 64 ++ docs/Makefile | 20 + docs/_static/style.css | 11 + docs/_templates/layout.html | 13 + docs/conf.py | 81 +++ docs/helpers.rst | 7 + docs/index.rst | 81 +++ docs/make.bat | 35 ++ docs/minetest-logo.png | Bin 0 -> 12188 bytes docs/minetest.svg | 183 ++++++ docs/miney-logo.png | Bin 0 -> 20704 bytes docs/miney-slogan.png | Bin 0 -> 49983 bytes docs/objects/Chat.rst | 7 + docs/objects/Exceptions.rst | 5 + docs/objects/Inventory.rst | 7 + docs/objects/Lua.rst | 7 + docs/objects/Minetest.rst | 23 + docs/objects/Node.rst | 13 + docs/objects/Player.rst | 7 + docs/objects/index.rst | 20 + docs/python-logo.png | Bin 0 -> 12302 bytes docs/quickstart.rst | 59 ++ docs/requirements.txt | 2 + docs/tech_background.rst | 55 ++ examples/02_player_locator.py | 23 + examples/Chatbot.py | 8 + examples/donut.py | 73 +++ examples/spawn_a_house.py | 4 + miney/__init__.py | 17 + miney/callback.py | 31 + miney/chat.py | 58 ++ miney/exceptions.py | 54 ++ miney/helper.py | 132 +++++ miney/inventory.py | 39 ++ miney/lua.py | 73 +++ miney/minetest.py | 382 ++++++++++++ miney/node.py | 250 ++++++++ miney/player.py | 335 +++++++++++ miney/tool.py | 37 ++ miney/vector.py | 65 +++ poetry.lock | 1033 +++++++++++++++++++++++++++++++++ pyproject.toml | 19 + pytest.ini | 2 + setup.py | 35 ++ tests/__init__.py | 0 tests/conftest.py | 15 + tests/test_minetest.py | 109 ++++ tests/test_nodes.py | 48 ++ tests/test_player.py | 65 +++ 54 files changed, 3926 insertions(+) create mode 100644 .gitignore create mode 100644 .readthedocs.yml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 dev-requirements.txt create mode 100644 devtools/luaconsole.py create mode 100644 docs/Makefile create mode 100644 docs/_static/style.css create mode 100644 docs/_templates/layout.html create mode 100644 docs/conf.py create mode 100644 docs/helpers.rst create mode 100644 docs/index.rst create mode 100644 docs/make.bat create mode 100644 docs/minetest-logo.png create mode 100644 docs/minetest.svg create mode 100644 docs/miney-logo.png create mode 100644 docs/miney-slogan.png create mode 100644 docs/objects/Chat.rst create mode 100644 docs/objects/Exceptions.rst create mode 100644 docs/objects/Inventory.rst create mode 100644 docs/objects/Lua.rst create mode 100644 docs/objects/Minetest.rst create mode 100644 docs/objects/Node.rst create mode 100644 docs/objects/Player.rst create mode 100644 docs/objects/index.rst create mode 100644 docs/python-logo.png create mode 100644 docs/quickstart.rst create mode 100644 docs/requirements.txt create mode 100644 docs/tech_background.rst create mode 100644 examples/02_player_locator.py create mode 100644 examples/Chatbot.py create mode 100644 examples/donut.py create mode 100644 examples/spawn_a_house.py create mode 100644 miney/__init__.py create mode 100644 miney/callback.py create mode 100644 miney/chat.py create mode 100644 miney/exceptions.py create mode 100644 miney/helper.py create mode 100644 miney/inventory.py create mode 100644 miney/lua.py create mode 100644 miney/minetest.py create mode 100644 miney/node.py create mode 100644 miney/player.py create mode 100644 miney/tool.py create mode 100644 miney/vector.py create mode 100644 poetry.lock create mode 100644 pyproject.toml create mode 100644 pytest.ini create mode 100644 setup.py create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/test_minetest.py create mode 100644 tests/test_nodes.py create mode 100644 tests/test_player.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cbf968d --- /dev/null +++ b/.gitignore @@ -0,0 +1,105 @@ +# Created by .ignore support plugin (hsz.mobi) +### Windows template +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +### VirtualEnv template +# Virtualenv +# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ +.Python +[Bb]in +[Ii]nclude +[Ll]ib +[Ll]ib64 +[Ll]ocal +[Ss]cripts +pyvenv.cfg +.venv +env +venv +pip-selfcheck.json + +### macOS template +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### Linux template +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +# Sphinx +_build + +# Pycharm/VScode +.idea +.idea/** +.vscode + +# Python +build +dist +miney.egg-info +__pycache__/ +*.pyc +.pytest_cache/ + +# miney +tmp +Minetest diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 0000000..ae0c77f --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,22 @@ +# .readthedocs.yml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: docs/conf.py + +# Optionally build your docs in additional formats such as PDF and ePub +formats: all + +build: + image: latest + +# Optionally set the version of Python and requirements required to build your docs +python: + version: 3.8 + install: + - requirements: docs/requirements.txt \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..153d416 --- /dev/null +++ b/LICENSE @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..1bc19d5 --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +

+ +

+ +# Miney - The python interface to minetest + +Miney is an Python API to Minetest. + +Miney connects locally, over network or internet to the [mineysocket](https://github.com/miney-py/mineysocket) mod of minetest. + +## Documentation + +https://miney.readthedocs.io/en/latest/ + +## Status + +Beta, the current todo list is in the [wiki](https://github.com/miney-py/miney/wiki). + +## Requirement + +* Python 3.6+ (tested on 3.8) +* A minetest-server with [mineysocket](https://github.com/miney-py/mineysocket) mod diff --git a/dev-requirements.txt b/dev-requirements.txt new file mode 100644 index 0000000..7bc5814 --- /dev/null +++ b/dev-requirements.txt @@ -0,0 +1,5 @@ +Sphinx +sphinx_rtd_theme +wheel +twine +pytest \ No newline at end of file diff --git a/devtools/luaconsole.py b/devtools/luaconsole.py new file mode 100644 index 0000000..a276281 --- /dev/null +++ b/devtools/luaconsole.py @@ -0,0 +1,64 @@ +import sys +import os +import socket + +try: + from miney import Minetest, exceptions +except ModuleNotFoundError: + sys.path.append(os.getcwd()) + from miney import Minetest, exceptions + +server = sys.argv[1] if 1 < len(sys.argv) else "127.0.0.1" +port = sys.argv[2] if 2 < len(sys.argv) else 29999 +playername = sys.argv[3] if 3 < len(sys.argv) else "Player" +password = sys.argv[4] if 4 < len(sys.argv) else "" + +mt = Minetest(server, playername, password, port) + +print("python luaconsole.py [ ] - All parameter optional on localhost") +print("Press ctrl+c to quit. Start multiline mode with \"--\", run it with two empty lines, exit it with ctrl+c") + +multiline_mode = False +multiline_cmd = "" +ret = "" + +while True: + if mt.event_queue: + print(mt.event_queue) + try: + if not multiline_mode: + cmd = input(">> ") + multiline_cmd = "" + else: + cmd = input("-- ") + + if cmd == "--": + multiline_mode = True + else: + if multiline_mode: + multiline_cmd = multiline_cmd + cmd + "\n" + + if "\n\n" in multiline_cmd: + ret = mt.lua.run(multiline_cmd) + multiline_mode = False + if not isinstance(ret, type(None)): # print everything but none + print("<<", ret) + else: + ret = mt.lua.run(cmd) + + if not isinstance(ret, type(None)): # print everything but none + print("<<", ret) + + except exceptions.LuaError as e: + print("<<", e) + if multiline_mode: + multiline_mode = False + except (socket.timeout, ConnectionResetError) as e: + print(e) + sys.exit(-1) + except KeyboardInterrupt: + if multiline_mode: + multiline_mode = False + print("") + else: + sys.exit() diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d4bb2cb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/_static/style.css b/docs/_static/style.css new file mode 100644 index 0000000..c498f21 --- /dev/null +++ b/docs/_static/style.css @@ -0,0 +1,11 @@ +.wy-side-nav-search { +/* Permalink - use to edit and share this gradient: https://colorzilla.com/gradient-editor/#c9de96+0,8ab66b+44,398235+100;Green+3D+%233 */ +background: #c9de96; /* Old browsers */ +background: -moz-linear-gradient(top, #c9de96 0%, #8ab66b 44%, #398235 100%); /* FF3.6-15 */ +background: -webkit-linear-gradient(top, #c9de96 0%,#8ab66b 44%,#398235 100%); /* Chrome10-25,Safari5.1-6 */ +background: linear-gradient(to bottom, #c9de96 0%,#8ab66b 44%,#398235 100%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */ +filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#c9de96', endColorstr='#398235',GradientType=0 ); /* IE6-9 */ +} + +.miney-logo-mainpage { padding: 2em; } +.wy-nav-content { max-width: 1200px; } \ No newline at end of file diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html new file mode 100644 index 0000000..4e3baf4 --- /dev/null +++ b/docs/_templates/layout.html @@ -0,0 +1,13 @@ +{% extends "!layout.html" %} +{% block extrahead %} + +{% endblock %} +{% block footer %} + +{% endblock %} \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..1cf407b --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,81 @@ +import re +import io +import os +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# http://www.sphinx-doc.org/en/master/config + +# -- Path setup -------------------------------------------------------------- + +# 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. +# +import os +import sys +sys.path.insert(0, os.path.abspath("..")) + +# -- Project information ----------------------------------------------------- + +project = 'Miney' +copyright = '2020, Robert Lieback' +author = 'Robert Lieback' + + +__version__ = re.search( + r'__version__\s*=\s*[\'"]([^\'"]*)[\'"]', # It excludes inline comment too + io.open(os.path.abspath(os.path.join("..", "miney", "__init__.py")), encoding='utf_8_sig').read() + ).group(1) + +# The full version, including alpha/beta/rc tags +release = __version__ + + +# -- General configuration --------------------------------------------------- + +# 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", "sphinx.ext.viewcode", "sphinx.ext.intersphinx" +] +autodoc_default_options = { + 'members': True, + 'member-order': 'alphabetical', + # 'special-members': '__init__', +} + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + + +# -- 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 = 'sphinx_rtd_theme' + +html_theme_options = { + 'logo_only': False, + # 'collapse_navigation': False, + # 'titles_only': True +} + +html_logo = "miney-logo.png" + +# 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'] + +intersphinx_mapping = {'python': ('https://docs.python.org/3', None)} + +todo_include_todos = True \ No newline at end of file diff --git a/docs/helpers.rst b/docs/helpers.rst new file mode 100644 index 0000000..29782c2 --- /dev/null +++ b/docs/helpers.rst @@ -0,0 +1,7 @@ +Helper functions +================================= + +.. autofunction:: miney.doc +.. autofunction:: miney.is_miney_available +.. autofunction:: miney.run_miney_game +.. autofunction:: miney.run_minetest diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..1b027c0 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,81 @@ +.. image:: miney-slogan.png + :alt: Miney logo + :align: center + :class: miney-logo-mainpage + + +Welcome to the Miney documentation! +==================================== + +Miney provides an `Python `_ interface to `Minetest `_. + +First goal is to have fun with a sandbox for Python. + +**Do whatever you like:** + +* Play and fiddle around while learning python +* Visualize data in unusual ways +* Automate things with bots +* Connect minetest to external services like twitter, ebay or whatsoever +* Do whatever you want! + +.. important:: + + For the best way to get everything running, take a look at the :doc:`quickstart` page. + +.. warning:: + + Miney is currently in beta, so it's usable but the API may change at any point. + +Why Python? +------------- + +.. image:: python-logo.png + :alt: Python logo + :align: right +Some marketing text from the `Python website `_: + + | Python is powerful... and fast; + | plays well with others; + | runs everywhere; + | is friendly & easy to learn; + | is Open. + + These are some of the reasons people who use Python would rather not use anything else. + +And it's popular! And cause of that it has a `giant package index `_ filled by over 400.000 users! + + +Why Minetest? +--------------- +.. image:: minetest-logo.png + :alt: Python logo + :align: left + +Why not Minecraft? Minetest is free. Not only you don't have to pay for Minetest (consider to `donate `_!), it's also open source! +That's a big point, if you try to use this for example in a classroom. + +Also modding for minecraft isn't that easy, cause there is no official API or an embedded scripting language like Lua +in Minetest. Mods have to be written in Java, but have to be recompiled on every Minecraft update. +Cause of that many attempt for APIs appeared and disappeared in recent years. + +In contrast Minetest modding in Lua is easy: no compilation, a official API and all game logic is also in Lua. + +Table of Contents +----------------- + +.. toctree:: + :maxdepth: 1 + :caption: Getting started + + quickstart + +.. toctree:: + :maxdepth: 1 + :caption: Reference + + objects/index + helpers + tech_background + +Miney version: |release| \ No newline at end of file diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..922152e --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 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 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/minetest-logo.png b/docs/minetest-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..48793678f178139f0713f0a4aada1e9736b78654 GIT binary patch literal 12188 zcmV;NFJsV&P)lSzZ-??XQ?w#@hVf}Xfb|2<>&U4@U-aDC@^LtNyZ)|S8ZoO{3ZoO{3 zZoO{3ZoO{m`3fo5ao8xBu>(G0(0y2Z+Ha~2!9i0 z`)CYeg(dra@AIh$Lrf0u^fv6=pK`@}KR3M^LVF+teAW&xqy8a_w+d+~^79{yK0!0>c%a#4? zSYi62m7haau!}(aKu3hNDllP=y&-kAJv1xr4RO;IhTsMT=(-@E(fii~=gQ>+PX^goPkjNY6aoBe=4Jc?v^l6In|syRmNw@L__KYGN(7g1%KMYZ7cpX(=Sn zQE_X6i_d> zH$>g5ARwG3|3shn9(~?an_DkSD0Wo@=GSByKherX)MQfL3Iegt-jM!?g9w9TqJl7WM2##5B=6Pu>*B*SNdoAu? z$M=hWOP)_No)ZuS{QegUNlaGs2`Y4sA{Z|Q6gwiI$O%y`E(YCG{S8&m^vC$gt_Ee9 zlK@44^chJWiEV&MoFP#E;Vz3ULkR>l4?50!BXgw(LaUt(%2El;YRbTdD2tt> z!iMJlQ-iuC5c?YdV7Wbpw6&*UCcYlY&B370&FEbo=_V(z`GBfM6&_d4uP8 zJn}~%e!365efmjD*F$P>abCLspFQADQV(>Qx zbqKB}y3N&)ztO|cdBNMvD(Gk}3`hiZ*$AeBVodBrSHZeC?hugINYVmIocqcdo6~=W z8w;=kA}Ux4-g4cc?Gfpkq-;6JTZr|0WS6C?rn#kk(RA7ildtrEprZhT#m8DMit`q3 ztdr*RHuN(@wYo55|3bEyCn!M(3n+4wqCUl{|Nm*PUr$h9D*-Xjsw{!X*JLX&xt3;r zWD&D3G0QRniRyO5z3=l%XN8Kfg2uu*&#{|yR5>}-@} z_BN#FE0;zvt0J$#@M7BG_H{Zb!oac;Kt@A-myvqnHE8r(*6eH+-3IRGoZKPU? zZMXemPtgQcSf;&lJz<^B{(K?J;TR$V8E6r(dxV2}Z-w1e>sc=pZt91)$*vfaXOD=o zz9_62ip-?G$c=E@R}$hr++4}r>DG`A_7ez@9=JZDVtFrFOFng@U4NNMz2w?JdJl&8 zN@T@f>mF+$=IO0?>|~L~*4G&r2Xuaa*-lR+&2l59I>0yA5fvT3!p`+t>{*loeQbZ^ zhPdv|_Uq@<^Sk}Z6;1?UDp|&7!gmpxtFiYf3?YrY%L#(qoB(e*q%0G<+zNg! zAh@2l2Yw`1*7QpNc}Or*;Y&=ItuXX#!D{6lLQ%vyE3r4bmL_Lra&VXb7X5ARWl2CL z5fCwB5|z&)XHO(_F2lLRAUK=KSkV`el;(~m_Y_*pT0~HKRG>V!&sX|_K0B*(`^1dpxS@Do0HR(=zk9v>8^hZbAeZ$60jd85p0!dW z(--3h8G@?$IV;{j&)6XQe67@*vcIud0%2juEcECQLd1Rch&BhKg_zeCbZ!BJ$HtlL?-Q7=U95M_B!S0I13=rMJR#CCn6Y*1B zq0&3yo*Y}`G!4aGiW46ktjC*AR$y_hAF2|(34}9hHT`jDqY_`fm5iUh%)pPIrQy9- zV+`w;4o6jCALQrS{5_=H?jIr$(^^UbLJPJ2-yP6Rvn^-(a(?^(0znDX_dJ6owoTTH zZIl~=k!z0(0zv!oh?=Be$kgVCQm;Vv!|rH2GXNd5>aE)%=B<-S@A^<0>jm{(cZ5=$ z7@bdxrMxfJEgO$_p02?s2O6+pQY7k922tR4LqUWa9-B8BpPo-72wE6^pUFV}A;?!5 z`2HgeF1`?Em^Nhq!gLDIZ#FOxxz{EG(ftw@LMfoq{J}Uk`Z5V5=SYOdMTPBMt{uo) zq%Un@4KfIJPdsLrsyhC0pu_Q zNNIDUSYc@)_Ldg%%RigNm1?o0PhhG2oAjl?V5$`Ca@5NuFk-Q1;jT6qvnYbdRyMBK z7`!VFd7|w#R=#;!Yb^|;=J^2_f3F+2Km3a5{hfE>g)Ig6;z%oA*;t4vg?FJe#uM4W zu4qj6!kN8c`17Y}lHwW0mpqo`3nL(q)qM1J0yG`&@GB=EtIe995)g9dHG(d5IsfL0 zJvQBi9wcy-&S`(5g3dptUAv$B3mmE9bpnGaH-d?oYOLzD>rHxHlmrNmmxgx=AUJ4~ zo3cm2=Duiq)r*3%7smu^@v<6Eq>ZTZU7qZs#*tNomb+pNrLvc|>9DLJ5an^6$P05r zQM5aDuJXtCAEx4OpQp3JS?R2NlF#`l3xi2#H}>IM2~fAYa|=haTFMq>3n{bvYfP!_ z@*bU37WT0z)&3i*_TU0%*(wSgLkPr+v}gTyYRDM`g_LaXXXv=#h1z5NF=3h$#{pVB z?26*&awf)bsP#gy_z6Y6p1RBx%`b7B;B3hvoh6aR)^i--y`g)$KcYKag+3HpN<_X3 z(#N|YFVq#wCf8j@Y3@c3gw{B7lhCiw5#?P2@yT0>_!}wsZ-T<5IKg8Ud5%CZDA`gB z3`Dk&RI(6`ed}Zkq3vZuIxW8HLR;wa`}{?p*XJ>Pt}XYtvLcN=%IY#3n~Ln8(HT}1 z$RPZSTy2Pk^j+6HJ{I47 zq#-CKal_&$3qcX%3Ivzc6GJ$tTgQ;FY6q-&^d4+ja+{&5$QG3ZhKkDH>+)^eb-8wZ z^;xoB_4(w^((V2^w-l{*vI!}0cBH2}k_p5)vI^eNiD`E=2s>{Kf<*4N%0alBfXtol zh}8wQ*w^Zc-BUbJ)#!w_W@kKmzbAIoJLADj8%&ph$hCzG*4*m&+$h%IyyZ+xtkA24 zr%xui@|Le}z9W(wJaG7M2>xUl(B-k^oCqXQ0K%4Sk@iYJcJ2=VzRkv^QxS$)lROR8 z#dZP|5PYaockTIidM^R0`lSvPrNkU6EBjSA=y&r#6YI4Cm`p|EJOw?L1B^H znheAmiVTK}M6TG2p6G|jW>*N01&zsdK+W_axNtTW-xHXBSV7^V9Mr$c#IN6Amt|%l zS`#3l1Z2x@f8eVu;Oi{><+oZK-tKFd)aYucF0vEcm>^sA`L=-s$Lztdip(1X|F~4= zKEe4;wvRc02Ln*5dTAua6tclJ>AT2I9O%P5@ z6Qmzb^#CE4HD!edT2Bj4hsX^=7I_O+9GiZQ2*q5?-zzm7jZoUDPRXQxR#?3Y#!+k$ z7TLVDcjJRg3HUQ9P_9E51do5D`ETFX;`FgNBsO2W4hgArz^V=7@Z~#N{PAP0@Hg06 zzIrzakFUDJ&|c%rZmc4&&mRfQG;(EkmuK7c&Tr99*#L?kkMh%h^u;4vTm<(-KZ?7i za)u)-MaqcU&yoK=JqWdjhN9-lA(+wOg4H4rf?y3K+XfTo^+WaEA!s-@7_F}iWT0|` zkjExaSgyH$E?@UHhsRaE5lsDYFX7Q41XVb3qcO4G1Bb|s;hQX0BnhqXbbR}pG<q;+x#qLNu#oAZhE2F(jCPEL!V}3nw*r=d7B*q*1}CrNT0k|L)r_Mq&A! z!G;M%dnUXmxICi&O(-Ah-`O3;pTp5JSAj33`K?^W;pHV z$s%4Hh`MKopmNVp=y%+P>L-R`W~U2Q6$viO1!rdT$4LT0!ea)KLqO(wqH^19(C-?C zngc`8bYc+N$-+7qy5zzv09d%H>t2`R3b`<@{HvZBAS^WceNOPrb3p6NA;2GUIN<;B zBP~9AGZmL#RO6MSDR}j$nt;S2p@}U-e3n3#uy&(`EG8i5j%#rCND5v*k%ITnY4G)X zX$%)vv-xkId^*rDqpKeWegg6`iPsH*l!Ukq$qRSgmzmrbS(O73UD1!bq~q^(Md@}= zTE#t22eNyjowtFgKROsyPYs2RKpdfXqPfxcYE^q%ZFaC0WNf z3i9!K4%w87*Yt8eg1$A(h|rGRfH^$+&bT1z*0KCcxk}4E~&t(+8Ajsd5zD*GXMLpBn?o zk8mBD8|w0OO@a?L&y2&SwMi&xyBz`fPTay)&vOSoKT9Bd&~$PTuWDM|5{$~7!=Wds z+ByuAX7|Hl`Vq_XY_N`Mk)6#h*w*Nb)zqt6Mso`(9d2FejY@_%oQ}i!Z|F#>_6|e+ zb3@Sb67LfBs*NJUl7c#?!F9(5AopPpgwtn_%2FVqY5?w=8Gwi8sZdOb#i}`jfIsEn zhfmY-+2vHcN!IZyK_TU`g&aK?kFu$Q2#77YEn9AY($)aChsM!dShXV_qHgi)WFZWL zQWj zkXAPkW3ucKT^TgnnL(+6{HSQZhaY1Dga zS>uDs?IH|@!ayY`Y-tpaIxbiNxn>>0@Gi?6Qw5LHzB#47qjMCzG1YP3SR8zm6o04* zYi4A_KjJRbHwNIF4>HJY83X&bA87I6#ZW?|AM&rnV34HIl7gBKL6*aziCyn}L#?}k2X5n)xE)Q)Ti~HI=z%Hwxmq7AO zfIPiWjc<>4;_8tOeED1}j&9Dwv{_0-mUv*4)&*fz{h^*Q2qljVL-ig}v8+_tQgpkA zqju9!juo?;odh@p$qUs4OSk8C2?oDM9E(6G9+CTcZm{X5lz5b_L_SLO2giw$ZJt!P zx?^0C1ILN#?z^yUT{1p-rVghbZA3?7G5lk-@EDbhUxlThwlx)ppVs0pze@+c%i!bJ zzfR*-e*Oe0`nVC!rY#|8o;@6?Z627iNn5nbw~^*5Ks+ zKrEi_gL3i;CD}G&ePY{73kf#?vLJ%^+B*MjjOX87m-kj;%j#t0HQk1h8IDjEdLXvN2iePqqG}JVbb`QcXZvsxan%bJ!@-+H zoHun*WNA+ir4*Pzydq~>EEmOx+*qV}f?!N%YmW>-+G2OAGn@!SA55t1k7bJ@aeg=L zn;&S#vS}4aO3%hXza)5$O-4xGWE9Taf%?^_P&RKrVk%~1kTMGc#>68gU5!OcQt|l} zEm;fqx;Xp#^s+JgdhHm&Avbnj&Zekl;Ubb@e0*_&P=BDa&KU(6HfYtmW80!|T-l?; z{+01)u5hI+%jQI7zD@63R*orOxhFZgoWpxaKu$~PC9V`29s1o7twqvd7FuEZjdn~Ch61PI(nWML3=oOp%m zp0i%aqKzqf@(`m7?4T@irhpxY)7uKjs++K7aTPLhbMUKRHTsU6fUzlc$epqo^{Y;z zd(#`}di)|w%fm0DaK?5hvpV59CI!QTl8{=g!4sRfT~CTmr`4|&tbncI*RN^t<&{*n zAOc3$GkJ$`-=@)++SV6kdBT3Y9o9`BhgY}cbIkhgcsCBNN9CnVq}yWB{dqn zbSw?{RtVA=f;Di!W>8;ea=+`$AtfG|)E~8_b{r?>b_~bqjTvN}b>vmr@D=Imhg01+ zv?>|7>Ayl)`}^zg{Np*$b=--&v+OBV?u*D8 zFJvwn%!=puVp^;0A7WAnih-F#ZWhWhLaZlPDi8EW{8Tq6bxs(SXNUBL!Pv1*gWo<| zhl@KJ&{AK7fVgz{j7!46z%<0yEJF3NBWT<38iBaLAdE1CF$;}(jzP6=d>wTJE~$PQ zhN!aOO@C+XcnvzIq~dq)XbDV)0EUD1`>)1g>8ybes_+sjbUQ0O@XYE&{O0KzVbKzZ zHgfT8p^aQtHCl8j2#*TKxS(JbKb7i)zbfeFvMkv`OcYKcLLjSXbL9I9eBa%Ij4E_a7|jwc#k;1-NrObzIs#+q%2*^YXSbE0aq>v&vhsh+ z@{=LO)DrLPDfrkbcx>KKBxogDJrPbF+Q1UyzY3JJ-;U!HB)>k|iqntRA-^yW!-7)T zJ>3ymfb_0M(7f&?R=%ukRYow7LtuY0UG+;6l|QE&0ax_shov= zez*lYmXshhC5EkGY@iB>8V&BfZ!&5c>JS_mMe{*F4~Y1Il6*@mVB-S?Y=(0>GT3E^ z_pw;s|64NSeH+q|**KWMI0)@;?vGVJJHXOKF^C$PkLz}05P<|Dv1S0atyMGpxA!!m ztEm_xg3~b2UyY%v48-Z@qJdIaQl5n(#iOMaTpz}D!8AYg5=IdGA<9%dwW^vHC;vwF z9)hjsYv zGc6L3;P5c|;9xS#2t2u9T0rOrzJ)5#7@Yt}OPt{Uq5@2bQ4nfU?`DNv0e(=5oEfRh_5$)7e$@kTUhvx@)1 zt_rkG8H;d=D*=U06d#<80lS<)6gwld%oVLu$KcJq!ryAefi=|_ugOBcv1$ya%xQE& z74q-jYD`y2+DT#Uth5^_)@0mn0i@mfeH`dz$U;1Mof3t^xIHYFl7}83{JS`2Ea=X} zUE_j~S5$~)%NH?-`3vXp(P$xNF6(AnCsI&nATwIhDLV+kNPiU~qa#q7M*%7%lwr_3 z9YX|Qe7v_I4VQM*u{BZPms4b;1+le!ceE4FJeGyR*5L%eO>kp;EN&CxL>}GOj+eJI zptLNXK@1E?!(B1OP&coj)bt#$R8eUrnUZiWDP100fRPn$HnzT3oTJa9V)6u4^be4m z)L^JmgE5g>9N$>Szwg~W4S0A)9_z)Iet&*RF_tb{i1`F!!9o(rLh62wg{W~=C=3N4 zCnO*Yz*or@!DA_vGZ~KxApc46W$pAl{CaN#1$PEx1S0xd0z-|y`DR>STr{RDdRXPI=RAEng;w{NNk%&MfD5fc_`MEf@7OQLKv4f83 zoe#5B_>4=&J>h96F3H1dPu7zgZNh<-Wyng46I>N%QVD7`I=VZk{W4#)4wiowh?`g# z4iI};o%oy-_B~sO7JdxF1cVU0gt?wE2?HY;OZScP58^MocUdVu*w={9<)BYvIllbi zSO-Zbtu!G{=xFt&!Gl^Dw}UjaI@v|BRYb?fLtkA<5M~pM zd3;nYQ`KV@}Il4}Fyd9xlD0Xd30qf0o&zOuIoUl`*dyJ3A{0{ntQQBz-wCFGJw@eG7S z0fRAA#^StB?+Kw*?|Orj*$!{gyKyw;uE6q)bdh% zcdXs4M)~1nI|uH;0m+occA=K~NE10uTr>-~9AIS;=Vj@zzQ_807OVHjWAnes)>*s! z819TIfsX)W4+Al#@s#dX6(uyjNz3Hg$ALY|Cck}7C=HQpl~pkvoFE5kq`{d%*=sUyR4E@0^*;-8$|j=hcJ

&YxL6?`cGr&BUFY{w?w5+biIl~)Z#ajH?hWuuZG@MulwfPhtu}s>tWgdq4>EhWO+ZgHSB`(tF4v z_7Q}1mCD~u`e#~|>e?Xk!dxxEco&4b718|5`C-n83%OHJu7O;W!4yVVbPUvKYGmhT zQad~cx%t^h%Sc0P`~-$U_i!B&H$D=3A1uX>r#kQzSL&vFD=oCM)4re8@0my-1a}ytn~gi7invd9cUUTB zPcFl`tquI$$VExtP2L%0AZ#J`O`F0HMBxnU{^?VZnVV}6f;@W$rSf~FDqx;Ub+||65Ompn*P+Q{cFSctdV}7F&i92uXnqC6CUt zax#&mNkLF}1iP`&2+G@wl5qSHy=mi-|KB29N9*Uiy!Yx}O~VpJ}9RwxqLGTOPSwyL_t zYu3Wo#3O-a_vEW?z|i1K^!H6cWO62UEw99fyt!t+Fyt7=U_=psgpu94E$65v&Xf_#iBJbYWQV7&nZU^%xs+=l}8lrmK|FI3UQc4?%GuD0mQ`_q?NIlz;K`^i!nrvk3)7&rf3-?d0AvJ0u%(@uu)Tq{lzUb_gOKGpQKWfpgi7s_sA%JpBV-WE6tVxh`+Y_1%-x7S_{jeIXo_a@}5Dd%Y8EIg>m#WS-;khjv4mlAQR1%Y5Yk zLsA$6qt*-v`1ZbuaSxGCshKKbPbC-c_G2rom4?`u8O{pcd4YO3*e|wv2G%jJsDI`l zWtTVeJ?XgDD6gc$TbfnrU-e}YwL9_H7N^n<`gnIe6&C4&3#+K`7HE0jm3gwlHb_Vx zuIy8xr}t*UzVjN;SO9V6>v$Pn$WpUE;1`G|0$^}#iA1G`Oyh>mTJ#gnqV;JV#8YvF zd+BSj0~+i3XQ&~F3H+59W;-gAI<=*R_wa~lA?PW?9puT7`@I#ket83Xgk@NRI1?yP9(dqMb{L)Kv9f10(}c1{P?L{`<;ntK(9;6?g%i78!AKli!me+T z`0%CKV)dK}1Q4M1ziN!sFY)@nh^;@$B)2<((%hv;3XjKt(MU54^7&^9(69yE-?1w*bPAN#4+NL`ELP?ZY6E6_LUf55;))LPT>2EtnwA^-Rcz+8cH9H|He z1d>XDHPhGTl~DlFs}b3Nm(giF{YERbJzPhv3un~;HpCON_l$wJYy=?Rz*_`7gK5k= zWr_eGyz4r5gmZ(QRM89)Ne1RODAwxp9_~{C8*K&omN{{JTU4xR%%O;9sMOy9#0doH zSqY6h+pQ!VUvk}s$$K;d!0ZDs7h>i5*u2+R#z7~Mw%-Xx3nOcRBJ;UmICjo2Qb|-zGOQ+G{4GCN1h8X zg%Jbn|Eju|jcbSDgd}b*^@%ot_mSm`WkIhqLSx+oJS*n??=OhEt_Bm}+##N$i$7prg(7 zl$3aUF$<`p1W)M}%!gg7Di}hY;Vjy_vRHs>=+eY?%Ct#v-} z{oz*iuXd{DWa?pH{8iEuAx?;8b>pNrfTHX;+V`NxQ(iOGF+hQz^orgEK$t+XXToSr zC|#U_$I!gek+g0>o;bE8!T}moro08`94p>?AxvHKbHv<|5hU*csK@Y|bpRv%wvQ7x zD%gG|oZE9?#HTPbb{>YMmYJhzQ+t8<^wqgcW0#M064v|~zLOw8B|SNo;Q9v@*q$`} zRuYUG10`|j!fPJ{P&_d5cEkcvH#v(XvrhPcR4})`JHwU{XoJ;+nwvb1dxbK{d6@3H@07=2_=q`NL3_S<^ z8QO}^MwttLNbr*J;{TMW;gDdMM_&g3h)ie(z_>$wE(?Kr5{Nz!D+TFe5@S!QEh#qv ejYa!;a`!jqOEU>mGwA;S0000 + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/miney-logo.png b/docs/miney-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..5088d6f5d00fcf495082f662a2c5e74ac8e14eb9 GIT binary patch literal 20704 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 literal 0 HcmV?d00001 diff --git a/docs/miney-slogan.png b/docs/miney-slogan.png new file mode 100644 index 0000000000000000000000000000000000000000..e0a2ff027fb92703410ae85de378b9167fad63b1 GIT binary patch literal 49983 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

!I$0 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 Fe*j@+y8i$G literal 0 HcmV?d00001 diff --git a/docs/objects/Chat.rst b/docs/objects/Chat.rst new file mode 100644 index 0000000..5511403 --- /dev/null +++ b/docs/objects/Chat.rst @@ -0,0 +1,7 @@ +Chat +==== + +Get controls of the chat. + +.. autoclass:: miney.Chat + :members: \ No newline at end of file diff --git a/docs/objects/Exceptions.rst b/docs/objects/Exceptions.rst new file mode 100644 index 0000000..624bde5 --- /dev/null +++ b/docs/objects/Exceptions.rst @@ -0,0 +1,5 @@ +Exceptions +========== + +.. automodule:: miney.exceptions + :members: \ No newline at end of file diff --git a/docs/objects/Inventory.rst b/docs/objects/Inventory.rst new file mode 100644 index 0000000..1521612 --- /dev/null +++ b/docs/objects/Inventory.rst @@ -0,0 +1,7 @@ +Inventory +========= + +Lua related functions + +.. autoclass:: miney.Inventory + :members: \ No newline at end of file diff --git a/docs/objects/Lua.rst b/docs/objects/Lua.rst new file mode 100644 index 0000000..f734f76 --- /dev/null +++ b/docs/objects/Lua.rst @@ -0,0 +1,7 @@ +Lua +=== + +Lua related functions + +.. autoclass:: miney.Lua + :members: \ No newline at end of file diff --git a/docs/objects/Minetest.rst b/docs/objects/Minetest.rst new file mode 100644 index 0000000..1d6dce5 --- /dev/null +++ b/docs/objects/Minetest.rst @@ -0,0 +1,23 @@ +Minetest +======== + +This is the starting point for this library. With creating a Minetest object you also connect to minetest. + +In this object are all functions that targets minetest itself. +There is also some properties inside, to get other objects like players or nodes. + +:Example: + + >>> from miney import Minetest + >>> + >>> mt = Minetest() + >>> + >>> # We set the time to midday. + >>> mt.time_of_day = 0.5 + >>> + >>> # Write to the servers log + >>> mt.log("Time is set to midday ...") + + +.. autoclass:: miney.Minetest + :members: \ No newline at end of file diff --git a/docs/objects/Node.rst b/docs/objects/Node.rst new file mode 100644 index 0000000..338128c --- /dev/null +++ b/docs/objects/Node.rst @@ -0,0 +1,13 @@ +Node +==== + +Manipulate and get information's about nodes. + +Nodes are defined as dicts in the form + + >>> {'param1': 15, 'name': 'air', 'param2': 0, 'x': -158.67400138786, 'y': 3.5000000521541, 'z': -16.144999935403} + +The keys "param1" and "param2" are optional storage variables. + +.. autoclass:: miney.Node + :members: \ No newline at end of file diff --git a/docs/objects/Player.rst b/docs/objects/Player.rst new file mode 100644 index 0000000..f4a372e --- /dev/null +++ b/docs/objects/Player.rst @@ -0,0 +1,7 @@ +Player +====== + +Change properties of a single player, like there view, speed or gravity. + +.. autoclass:: miney.Player + :members: \ No newline at end of file diff --git a/docs/objects/index.rst b/docs/objects/index.rst new file mode 100644 index 0000000..f1c46e7 --- /dev/null +++ b/docs/objects/index.rst @@ -0,0 +1,20 @@ +API +================================= + +.. rubric:: Objects + +.. toctree:: + + Minetest + Lua + Chat + Player + Node + Inventory + Exceptions + +.. rubric:: Indices and tables + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/python-logo.png b/docs/python-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..3381e9acac2675e839aea471eb07867148d94f97 GIT binary patch literal 12302 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 OKt)kgp-j#^`2PSZCPrrf literal 0 HcmV?d00001 diff --git a/docs/quickstart.rst b/docs/quickstart.rst new file mode 100644 index 0000000..b17ff7b --- /dev/null +++ b/docs/quickstart.rst @@ -0,0 +1,59 @@ +Quickstart +========== + +Welcome in the block sandbox! + +Blockgames like Minecraft or Minetest give you the ideal playground for creative playing and building just like a real sandbox. +But other than real sandboxes, you can work on very large worlds together with your friends over the internet. +And you can use (very simplified) physics, save the progress and many more. + +But what about learning programming while expressing your creativity? Why not automate things? Or build even greater things? + +Installation +------------ + +Windows +^^^^^^^ + + * Download the latest precompiled Miney distribution: https://github.com/miney-py/miney_distribution/releases + * Start the miney-launcher.exe and click on "Quickstart". This will open Minetest directly into a game and IDLE, the IDE shipped with python. + +Linux +^^^^^ + +Tested under lubuntu 20.04LTS + +$ sudo apt-get install minetest fonts-crosextra-caladea fonts-crosextra-carlito minetest-mod-moreblocks minetest-mod-moreores minetest-mod-pipeworks minetest-server minetestmapper + +$ sudo apt-get install luajit lua-socket lua-cjson idle3 python3-pip + +$ pip3 install miney + +Then install the mineysocket mod in minetest + +$ cd ~/.minetest/mods + +$ git clone https://github.com/miney-py/mineysocket.git + +Don't forget to enable the mods in the configuration tab for your new game! + +MacOS +^^^^^ + +Untested + +First lines of code +------------------- + +The first lines of code with miney should be the import statement and the creation of the miney object "mt". This will +connect miney to your already running Minetest. + +:: + + import miney + + mt = miney.Minetest() + +.. Important:: + + Whenever you see a object "mt" in the documentation, it was created with this line! diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..2bb0495 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,2 @@ +Sphinx==3.3.1 +sphinx_rtd_theme==0.5.0 \ No newline at end of file diff --git a/docs/tech_background.rst b/docs/tech_background.rst new file mode 100644 index 0000000..9d93d20 --- /dev/null +++ b/docs/tech_background.rst @@ -0,0 +1,55 @@ +.. image:: python-logo.png + :alt: Python logo + :align: right + +Technical Background +===================== + +This page provides an inside view of how Miney works. + +Miney's basic idea is, to use `Minetest `_ with `Python `_. + +Minetest's main programming language (besides C++) is `Lua `_ and it provides an mighty Lua-API for mod programming. +But Lua isn't the ideal programming language to start programming and mod programming isn't fun, +if you just want to play around with a sandbox. +So we need something like an interface that is accessible by Python. + +The interface +------------------------------ + +For this we've written the `Mineysocket `_ mod as a regular Lua mod. +This mod opens a network port and receives JSON encoded commands. +The most important command is the "lua" command, where it just executes the received Lua code and +sends any return value back. + +Miney is using this lua command to execute Lua code inside Minetest. + +.. note:: + + **And you can use Miney without knowing any Lua or even seeing a single line of Lua code.** + +Mineysocket, Windows and the Miney distribution +---------------------------------------------------- + +Python is the language with batteries included and it ships with a very complete library for nearly everything. +In contrast Lua has the batteries explicitly excluded, so there are nearly no libraries and it misses also a +network library. + +So we need a Lua extension for networking, thats `luasocket `_. +And an extension for JSON, thats `lua-cjson `_ + +Under Linux this should be no big deal, just install these packages (most distributions provide them) and you are ready to go. + +Windows +^^^^^^^^^^^^ + +It isn't that easy for Minetest on Windows. The Minetest binary's where compiled with Visual Studio and the extension +has to be linked against minetest also with the same version of Visual Studio. +So the best way under windows is, to compile Minetest and the Lua extensions by yourself with the same Visual Studio. + +And when we already do this, why not replace Lua with `lua-jit `_ for more speed? + +And when we've done all this, why not also bundle a actual python interpreter? And why not preinstall Miney in this +interpreter? Now it would be nice to have a comfortable `launcher `_. + +We've done all this for windows and we call it `Miney Distibution `_. \ No newline at end of file diff --git a/examples/02_player_locator.py b/examples/02_player_locator.py new file mode 100644 index 0000000..862068f --- /dev/null +++ b/examples/02_player_locator.py @@ -0,0 +1,23 @@ +from miney.minetest import Minetest + + +mt = Minetest(server='127.0.0.1', port=29999, playername='bvn13') + +print("Connected to", mt) + +players = mt.player +if len(players): + mt.chat.send_to_all("I'm running the example script...") + + print("Player positions:") + while True: + for player in players: + + standing_position = player.position + standing_position["y"] = standing_position["y"] - 0.5 # Position of the block under my feet + + print("\r", player.name, player.position, player.look_horizontal, player.look_vertical, mt.node.get(standing_position), end='') + + +else: + raise Exception("There is no player on the server but we need at least one...") diff --git a/examples/Chatbot.py b/examples/Chatbot.py new file mode 100644 index 0000000..de4b3b7 --- /dev/null +++ b/examples/Chatbot.py @@ -0,0 +1,8 @@ +""" +This example shows a simple chatbot, that listens on commands but also on any messages. + +""" +from miney import Minetest + +mt = Minetest() + diff --git a/examples/donut.py b/examples/donut.py new file mode 100644 index 0000000..1671880 --- /dev/null +++ b/examples/donut.py @@ -0,0 +1,73 @@ +""" +The MIT License (MIT) + +Pycraft Mod Copyright (c) Giuseppe Menegoz (@gmenegoz) & Alessandro Norfo (@alenorfo) +RaspberryJamMod Copyright (c) 2015 Alexander R. Pruss +Lua Copyright (c) 1994�2015 Lua.org, PUC-Rio. +luasocket Copyright (c) Diego Nehab (with socket.lua changed by ARP to load x64/x86 as needed, and minetest compatibility) +tools.lua adapted from lua-websockets Copyright (c) 2012 Gerhard Lipp (with base64 inactivated and minetest compatibility) +base64.lua Copyright (c) 2014 Mark Rogaski and (C) 2012 Paul Moore (changed by ARP to MIME encoding and minetest compatibility) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import math +import miney + + +def draw_donut(mt, mcx, mcy, mcz, R, r, mcblock): + + positions = [] + + for x in range(-R - r, R + r): + for y in range(-R - r, R + r): + xy_dist = math.sqrt(x ** 2 + y ** 2) + if xy_dist > 0: + ringx = x / xy_dist * R # nearest point on major ring + ringy = y / xy_dist * R + ring_dist_sq = (x - ringx) ** 2 + (y - ringy) ** 2 + + for z in range(-R - r, R + r): + if ring_dist_sq + z ** 2 <= r ** 2: + positions.append( + { + "x": (mcx + x), + "y": (mcz + y), + "z": (mcy + z) + } + ) + print("Spawning", len(positions), "nodes of", mcblock) + print(positions) + mt.node.set(nodes=positions, name=mcblock) + + +if miney.is_miney_available(): + mt = miney.Minetest(server='127.0.0.1', port=29999, playername='bvn13') + + playerPos = mt.player[0].position + + draw_donut(mt, playerPos["x"], playerPos["y"] + 1, playerPos["z"], 4, 2, 'basenodes:dirt_with_grass') + #mt.chat.send_to_all(mt.node.type.default.glass + " donut done") + #print(mt.node.type.default.glass + " donut done") + + draw_donut(mt, playerPos["x"], playerPos["y"] + 1, playerPos["z"], 4, 1, 'basenodes:sand') + #mt.chat.send_to_all(mt.node.type.default.lava_source + " donut done") + #print(mt.node.type.default.lava_source + " donut done") +else: + raise miney.MinetestRunError("Please start Minetest with the miney game") diff --git a/examples/spawn_a_house.py b/examples/spawn_a_house.py new file mode 100644 index 0000000..da628b6 --- /dev/null +++ b/examples/spawn_a_house.py @@ -0,0 +1,4 @@ +import miney + +mt = miney.Minetest + diff --git a/miney/__init__.py b/miney/__init__.py new file mode 100644 index 0000000..2acbc17 --- /dev/null +++ b/miney/__init__.py @@ -0,0 +1,17 @@ +""" +Miney is the python interface to minetest +""" +from .minetest import Minetest +from .player import Player +from .chat import Chat +from .node import Node +from .lua import Lua +from .inventory import Inventory +from .exceptions import * +from .tool import ToolIterable +from .player import PlayerIterable +from .helper import * + +__version__ = "0.2.2" +default_playername = "MineyPlayer" + diff --git a/miney/callback.py b/miney/callback.py new file mode 100644 index 0000000..27583d9 --- /dev/null +++ b/miney/callback.py @@ -0,0 +1,31 @@ +import string +from random import choices +import miney + + +class Callback: + """Register callbacks inside minetest, to receive events from Lua""" + def __init__(self, mt: miney.Minetest): + self.mt = mt + + def activate(self, event: str, callback: callable, parameters: dict = None): + """ + Register a callback for an event. + + :param event: Event to register for + :param callback: The function to be called on events + :param parameters: Some events need parameters + :return: None + """ + # Match answer to request + result_id = ''.join(choices(string.ascii_lowercase + string.ascii_uppercase + string.digits, k=6)) + + self.mt.send( + { + 'activate_event': + { + 'event': event, + }, + 'id': result_id + } + ) diff --git a/miney/chat.py b/miney/chat.py new file mode 100644 index 0000000..69d25c5 --- /dev/null +++ b/miney/chat.py @@ -0,0 +1,58 @@ +from typing import Dict +import miney + + +class Chat: + """ + Chat functions. + """ + def __init__(self, mt: miney.Minetest): + self.mt = mt + + def __repr__(self): + return '' + + def send_to_all(self, message: str) -> None: + """ + Send a chat message to all connected players. + + :param message: The chat message + :return: None + """ + self.mt.lua.run("minetest.chat_send_all('{}')".format(message.replace("\'", "\\'"))) + + def send_to_player(self, playername: str, message: str): + return self.mt.lua.run("return minetest.chat_send_player({}, {}})".format(playername, message)) + + def format_message(self, playername: str, message: str): + return self.mt.lua.run("return minetest.format_chat_message({}}, {}})".format(playername, message)) + + def registered_commands(self): + return self.mt.lua.run("return minetest.registered_chatcommands") + + def register_command(self, name, parameter: str, callback_function, description: str = "", privileges: Dict = None): + + if isinstance(callback_function, callable): + pass + elif isinstance(callback_function, str): + pass + + return self.mt.lua.run( + """ + return minetest.register_chatcommand( + {name}, + {{params = {params}, description={description}, privs={privs}, func={func}} + )""".format( + name=name, + params=parameter, + description=description, + privs=self.mt.lua.dumps(privileges) if privileges else "{}", + func=callback_function + ) + ) + + def override_command(self, definition): + pass + + def unregister_command(self, name: str): + return self.mt.lua.run("return minetest.register_chatcommand({})".format(name)) diff --git a/miney/exceptions.py b/miney/exceptions.py new file mode 100644 index 0000000..2dc23a0 --- /dev/null +++ b/miney/exceptions.py @@ -0,0 +1,54 @@ +# Minetest exceptions +class MinetestRunError(Exception): + """ + Error: minetest was not found. + """ + pass + + +class LuaError(Exception): + """ + Error during Lua code execution. + """ + pass + + +class LuaResultTimeout(Exception): + """ + The answer from Lua takes to long. + """ + pass + + +class DataError(Exception): + """ + Malformed data received. + """ + pass + + +class AuthenticationError(Exception): + """ + Authentication error. + """ + pass + + +class SessionReconnected(Exception): + """ + We had to reconnect and reauthenticate. + """ + pass + + +# Player exceptions +class PlayerInvalid(Exception): + pass + + +class PlayerOffline(Exception): + pass + + +class NoValidPosition(Exception): + pass diff --git a/miney/helper.py b/miney/helper.py new file mode 100644 index 0000000..794f7a8 --- /dev/null +++ b/miney/helper.py @@ -0,0 +1,132 @@ +import socket +import time +import os +import platform +import subprocess +import webbrowser +import miney + + +def is_miney_available(ip: str = "127.0.0.1", port: int = 29999, timeout: int = 1.0) -> bool: + """ + Check if there is a running miney game available on an optional given host and/or port. + This functions pings mineysocket and waits **timeout** seconds for a pong. + + :param ip: Optional IP or hostname + :param port: Optional port + :param timeout: Optional timeout + :return: True or False + """ + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(timeout) + try: + s.connect((ip, int(port))) + s.send(b"ping\n") + reply = s.recv(4096).decode() + if reply == "pong\n": + return True + else: + return False + except (socket.timeout, ConnectionResetError, ConnectionRefusedError): + return False + finally: + s.close() + + +def run_miney_game(): + """ + Run minetest with the miney world. Miney will look for the minetest executable in common places for itself, + but it's also possible to provide the path as parameter or as environment variable :envvar:`MINETEST_BIN`. + + :return: None + """ + if is_miney_available(): + raise miney.MinetestRunError("A miney game is already running") + else: + run_minetest(show_menu=False) + wait = 0 + while wait < 12: + time.sleep(1) + available = is_miney_available() + if available: + time.sleep(2) # some extra time to get everything initialized + return True + wait = wait + 1 + raise miney.MinetestRunError("Timeout while waiting for minetest with an open mineysocket") + + +def run_minetest( + minetest_path: str = None, + show_menu: bool = True, + world_path: str = "Miney", + seed: str = "746036489947438842" +) -> None: + """ + Run minetest. Miney will look for the minetest executable in common places for itself, + but it's also possible to provide the path as parameter or as environment variable 'MINETEST_BIN'. + + :param minetest_path: Path to the minetest executable + :param show_menu: Start in the world or in the menu + :param world_path: Optional world path + :param seed: Optional world seed + :return: None + """ + if not minetest_path: + if os.environ.get('MINETEST_BIN') and os.path.isfile(os.environ.get('MINETEST_BIN')): + minetest_path = os.environ['MINETEST_BIN'] + else: + if platform.system() == 'Windows': + exe_name = "minetest.exe" + else: + exe_name = "minetest" + + # we have to guess the path + possible_paths = [ + os.path.join(os.getcwd(), "Minetest", "bin"), + ] + for p in possible_paths: + path = os.path.join(p, exe_name) + if os.path.isfile(path): + minetest_path = os.path.join(p, exe_name) + break + else: + raise miney.MinetestRunError("Minetest was not found") + if world_path == "Miney": + world_path = os.path.abspath(os.path.join(minetest_path, "..", "..", "worlds", "miney")) + + if not os.path.isdir(world_path): # We have to create the default world + if not os.path.isdir(os.path.abspath(os.path.join(world_path, "..", "..", "worlds"))): + os.mkdir(os.path.abspath(os.path.join(world_path, "..", "..", "worlds"))) + os.mkdir(world_path) + with open(os.path.join(world_path, "world.mt"), "w") as world_config_file: + world_config_file.write( + "enable_damage = true\ncreative_mode = false\ngameid = minetest\nplayer_backend = sqlite3\n" + "backend = sqlite3\nauth_backend = sqlite3\nload_mod_mineysocket = true\nserver_announce = false\n" + ) + with open(os.path.join(world_path, "map_meta.txt"), "w") as world_meta_file: + world_meta_file.write(f"seed = {seed}") + + if not os.path.isdir(os.path.abspath(os.path.join(minetest_path, "..", "..", "mods", "mineysocket"))): + raise miney.MinetestRunError("Mineysocket mod is not installed") + + # todo: run_minetest - implementation for linux/macos + + if show_menu: + subprocess.Popen( + f"{minetest_path} " + f"--world \"{world_path}\" --name {miney.default_playername} --address \"\"" + ) + else: + subprocess.Popen( + f"{minetest_path} " + f"--go --world \"{world_path}\" --name {miney.default_playername} --address \"\"" + ) + + +def doc() -> None: + """ + Open the documention in the webbrower. This is just a shortcut for IDLE or the python interactive console. + + :return: None + """ + webbrowser.open("https://miney.readthedocs.io/en/latest/") \ No newline at end of file diff --git a/miney/inventory.py b/miney/inventory.py new file mode 100644 index 0000000..217466a --- /dev/null +++ b/miney/inventory.py @@ -0,0 +1,39 @@ +import miney + + +class Inventory: + """ + Inventories are places to store items, like Chests or player inventories. + """ + + def __init__(self, minetest: miney.Minetest, parent: object): + self.mt = minetest + self.parent = parent + + def add(self, item: str, amount: int = 1) -> None: + """ + Add an item to an inventory. Possible items can be obtained from :attr:`~miney.Node.type`. + + :param item: item type + :param amount: item amount + :return: None + """ + if isinstance(self.parent, miney.Player): + self.mt.lua.run( + f"minetest.get_inventory(" + f"{{type = \"player\", name = \"{self.parent.name}\"}}" + f"):add_item(\"main\", ItemStack(\"{item} {amount}\"))" + ) + + def remove(self, item: str, amount: int = 1) -> None: + """ + Remove an item from an inventory. Possible items can be obtained from mt.node.type. + + :param item: item type + :param amount: item amount + :return: None + """ + if isinstance(self.parent, miney.Player): + self.mt.lua.run( + f"minetest.get_inventory({{type = \"player\", " + f"name = \"{self.parent.name}\"}}):remove_item(\"main\", ItemStack(\"{item} {amount}\"))") diff --git a/miney/lua.py b/miney/lua.py new file mode 100644 index 0000000..77e5716 --- /dev/null +++ b/miney/lua.py @@ -0,0 +1,73 @@ +import re +import string +from random import choices +import miney + + +class Lua: + """ + Lua specific functions. + """ + def __init__(self, mt: miney.Minetest): + self.mt = mt + + def run(self, lua_code: str, timeout: float = 10.0): + """ + Run load code on the minetest server. + + :param lua_code: Lua code to run + :param timeout: How long to wait for a result + :return: The return value. Multiple values as a list. + """ + # generates nearly unique id's (under 1000 collisions in 10 million values) + result_id = ''.join(choices(string.ascii_lowercase + string.ascii_uppercase + string.digits, k=6)) + + self.mt.send({"lua": lua_code, "id": result_id}) + + try: + return self.mt.receive(result_id, timeout=timeout) + except miney.SessionReconnected: + # We rerun the code, cause he was dropped during reconnect + return self.run(lua_code, timeout=timeout) + + def run_file(self, filename): + """ + Loads and runs Lua code from a file. This is useful for debugging, cause Minetest can throws errors with + correct line numbers. It's also easier usable with a Lua capable IDE. + + :param filename: + :return: + """ + with open(filename, "r") as f: + return self.run(f.read()) + + def dumps(self, data) -> str: + """ + Convert python data type to a string with lua data type. + + :param data: Python data + :return: Lua data + """ + # credits: + # https://stackoverflow.com/questions/54392760/serialize-a-dict-as-lua-table/54392761#54392761 + if type(data) is str: + return '"{}"'.format(re.escape(data)) + if type(data) in (int, float): + return '{}'.format(data) + if type(data) is bool: + return data and "true" or "false" + if type(data) is list: + l = "{" + l += ", ".join([self.dumps(item) for item in data]) + l += "}" + return l + if type(data) is dict: + t = "{" + t += ", ".join( + [ + '[\"{}\"]={}'.format(re.escape(k), self.dumps(v)) for k, v in data.items() + ] + ) + t += "}" + return t + raise ValueError("Unknown type {}".format(type(data))) \ No newline at end of file diff --git a/miney/minetest.py b/miney/minetest.py new file mode 100644 index 0000000..c98bc5a --- /dev/null +++ b/miney/minetest.py @@ -0,0 +1,382 @@ +import socket +import json +import math +from typing import Dict, Union +import time +import string +from random import choices +import miney + + +class Minetest: + """__init__([server, playername, password, [port]]) + The Minetest server object. All other objects are accessable from here. By creating an object you connect to Minetest. + + **Parameters aren't required, if you run miney and minetest on the same computer.** + + *If you connect over LAN or Internet to a Minetest server with installed mineysocket, you have to provide a valid + playername with a password:* + + :: + + >>> mt = Minetest("192.168.0.2", "MyNick", "secret_password") + + Account creation is done by starting Minetest and connect to a server with a username + and password. https://wiki.minetest.net/Getting_Started#Play_Online + + :param str server: IP or DNS name of an minetest server with installed apisocket mod + :param str playername: A name to identify yourself to the server. + :param str password: Your password + :param int port: The apisocket port, defaults to 29999 + """ + def __init__(self, server: str = "127.0.0.1", playername: str = None, password: str = "", port: int = 29999): + """ + Connect to the minetest server. + + :param server: IP or DNS name of an minetest server with installed apisocket mod + :param port: The apisocket port, defaults to 29999 + """ + self.server = server + self.port = port + if playername: + self.playername = playername + else: + self.playername = miney.default_playername + self.password = password + + # setup connection + self.connection = None + self._connect() + + self.event_queue = [] # List for collected but unprocessed events + self.result_queue = {} # List for unprocessed results + self.callbacks = {} + + self.clientid = None # The clientid we got from mineysocket after successful authentication + self._authenticate() + + # objects representing local properties + self._lua: miney.lua.Lua = miney.Lua(self) + self._chat: miney.chat.Chat = miney.Chat(self) + self._node: miney.node.Node = miney.Node(self) + + player = self.lua.run( + """ + local players = {} + for _,player in ipairs(minetest.get_connected_players()) do + table.insert(players,player:get_player_name()) + end + return players + """ + ) + if player: + player = [] if len(player) == 0 else player + else: + raise miney.DataError("Received malformed player data.") + + self._player = miney.PlayerIterable(self, player) + + self._tools_cache = self.lua.run( + """ + local nodes = {} + for name, def in pairs(minetest.registered_tools) do + table.insert(nodes, name) + end return nodes + """ + ) + self._tool = miney.ToolIterable(self, self._tools_cache) + + def _connect(self): + # setup connection + self.connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.connection.settimeout(2.0) + self.connection.connect((self.server, self.port)) + + def _authenticate(self): + """ + Authenticate to mineysocket. + + :return: None + """ + # authenticate + self.send({"playername": self.playername, "password": self.password}) + result = self.receive("auth") + + if result: + if "auth_ok" not in result: + raise miney.AuthenticationError("Wrong playername or password") + else: + self.clientid = result[1] # Clientid = : + else: + raise miney.DataError("Unexpected authentication result.") + + def send(self, data: Dict): + """ + Send json objects to the miney-socket. + + :param data: + :return: + """ + chunk_size = 4096 + raw_data: bytes = str.encode(json.dumps(data) + "\n") + + try: + if len(raw_data) < chunk_size: + self.connection.sendall(raw_data) + else: # we need to break the message in chunks + for i in range(0, int(math.ceil((len(raw_data)/chunk_size)))): + self.connection.sendall( + raw_data[i * chunk_size:chunk_size + (i * chunk_size)] + ) + time.sleep(0.01) # Give luasocket a chance to read the buffer in time + # todo: Protocol change, that every chunked message needs a response before sending the next + except ConnectionAbortedError: + self._connect() + self.send(data) + + def receive(self, result_id: str = None, timeout: float = None) -> Union[str, bool]: + """ + Receive data and events from minetest. + + With an optional result_id this function waits for a result with that id by call itself until the right result + was received. **If lua.run() was used this is not necessary, cause miney already takes care of it.** + + With the optional timeout the blocking waiting time for data can be changed. + + :Example to receive and print all events: + + >>> while True: + >>> print("Event received:", mt.receive()) + + :param str result_id: Wait for this result id + :param float timeout: Block further execution until data received or timeout in seconds is over. + :rtype: Union[str, bool] + :return: Data from mineysocket + """ + + def format_result(result_data): + if type(result_data["result"]) in (list, dict): + if len(result_data["result"]) == 0: + return + if len(result_data["result"]) == 1: # list with single element doesn't needs a list + return result_data["result"][0] + if len(result_data["result"]) > 1: + return tuple(result_data["result"]) + else: + return result_data["result"] + + # Check if we have to return something received earlier + if result_id in self.result_queue: + result = format_result(self.result_queue[result_id]) + del self.result_queue[result_id] + return result + # Without a result_id we run an event callback + elif not result_id and len(self.event_queue): + result = self.event_queue[0] + del self.event_queue[0] + self._run_callback(result) + try: + # Set a new timeout to prevent long waiting for a timeout + if timeout: + self.connection.settimeout(timeout) + + try: + # receive the raw data and try to decode json + data_buffer = b"" + while "\n" not in data_buffer.decode(): + data_buffer = data_buffer + self.connection.recv(4096) + data = json.loads(data_buffer.decode()) + except socket.timeout: + raise miney.LuaResultTimeout() + + # process data + if "result" in data: + if result_id: # do we need a specific result? + if data["id"] == result_id: # we've got the result we wanted + return format_result(data) + # We store this for later processing + self.result_queue[data["id"]] = data + elif "error" in data: + if data["error"] == "authentication error": + if self.clientid: + # maybe a server restart or timeout. We just reauthenticate. + self._authenticate() + raise miney.SessionReconnected() + else: # the server kicked us + raise miney.AuthenticationError("Wrong playername or password") + else: + raise miney.LuaError("Lua-Error: " + data["error"]) + elif "event" in data: + self._run_callback(data) + + # if we don't got our result we have to receive again + if result_id: + self.receive(result_id) + + except ConnectionAbortedError: + self._connect() + self.receive(result_id, timeout) + + def on_event(self, name: str, callback: callable) -> None: + """ + Sets a callback function for specific events. + + :param name: The name of the event + :param callback: A callback function + :return: None + """ + # Match answer to request + result_id = ''.join(choices(string.ascii_lowercase + string.ascii_uppercase + string.digits, k=6)) + self.callbacks[name] = callback + self.send({'activate_event': {'event': name}, 'id': result_id}) + + def _run_callback(self, data: dict): + if data['event'] in self.callbacks: + # self.callbacks[data['event']](**data['event']['params']) + if type(data['params']) is dict: + self.callbacks[data['event']](**data['params']) + elif type(data['params']) is list: + self.callbacks[data['event']](*data['params']) + + @property + def chat(self): + """ + Object with chat functions. + + :Example: + + >>> mt.chat.send_to_all("My chat message") + + :return: :class:`miney.Chat`: chat object + """ + return self._chat + + @property + def node(self): + """ + Manipulate and get information's about nodes. + + :return: :class:`miney.Node`: Node manipulation functions + """ + return self._node + + def log(self, line: str): + """ + Write a line in the servers logfile. + + :param line: The log line + :return: None + """ + return self.lua.run('minetest.log("action", "{}")'.format(line)) + + @property + def player(self): + """ + Get a single players object. + + :Examples: + + Make a player 5 times faster: + + >>> mt.player.MyPlayername.speed = 5 + + Use a playername from a variable: + + >>> player = "MyPlayername" + >>> mt.player[player].speed = 5 + + Get a list of all players + + >>> list(mt.player) + [, , ...] + + :return: :class:`miney.Player`: Player related functions + """ + return self._player + + @property + def lua(self): + """ + Functions to run Lua inside Minetest. + + :return: :class:`miney.Lua`: Lua related functions + """ + return self._lua + + @property + def time_of_day(self) -> int: + """ + Get and set the time of the day between 0 and 1, where 0 stands for midnight, 0.5 for midday. + + :return: time of day as float. + """ + return self.lua.run("return minetest.get_timeofday()") + + @time_of_day.setter + def time_of_day(self, value: float): + if 0 <= value <= 1: + self.lua.run("return minetest.set_timeofday({})".format(value)) + else: + raise ValueError("Time value has to be between 0 and 1.") + + @property + def settings(self) -> dict: + """ + Receive all server settings defined in "minetest.conf". + + :return: A dict with all non-default settings. + """ + return self.lua.run("return minetest.settings:to_table()") + + @property + def tool(self) -> 'miney.ToolIterable': + """ + All available tools in the game, sorted by categories. In the end it just returns the corresponding + minetest tool string, so `mt.tool.default.axe_stone` returns the string 'default:axe_stone'. + It's a nice shortcut in REPL, cause with auto completion you have only pressed 2-4 keys to get to your type. + + :Examples: + + Directly access a tool: + + >>> mt.tool.default.pick_mese + 'default:pick_mese' + + Iterate over all available types: + + >>> for tool_type in mt.tool: + >>> print(tool_type) + default:shovel_diamond + default:sword_wood + default:shovel_wood + ... (there should be around 34 different tools) + >>> print(len(mt.tool)) + 34 + + Get a list of all types: + + >>> list(mt.tool) + ['default:pine_tree', 'default:dry_grass_5', 'farming:desert_sand_soil', ... + + Add a diamond pick axe to the first player's inventory: + + >>> mt.player[0].inventory.add(mt.tool.default.pick_diamond, 1) + + :rtype: :class:`ToolIterable` + :return: :class:`ToolIterable` object with categories. Look at the examples above for usage. + """ + return self._tool + + def __del__(self) -> None: + """ + Close the connection to the server. + + :return: None + """ + self.connection.close() + + def __repr__(self): + return ''.format(self.server, self.port) + + def __delete__(self, instance): + self.connection.close() diff --git a/miney/node.py b/miney/node.py new file mode 100644 index 0000000..5d494b3 --- /dev/null +++ b/miney/node.py @@ -0,0 +1,250 @@ +import miney +from typing import Union +from copy import deepcopy + + +class Node: + """ + Manipulate and get information's about nodes. + + **Node manipulation is currently tested for up to 25.000 nodes, more optimization will come later** + + """ + def __init__(self, mt: miney.Minetest): + self.mt = mt + + self._types_cache = self.mt.lua.run( + """ + local nodes = {} + for name, def in pairs(minetest.registered_nodes) do + table.insert(nodes, name) + end return nodes + """ + ) + self._types = TypeIterable(self, self._types_cache) + + @property + def type(self) -> 'TypeIterable': + """ + All available node types in the game, sorted by categories. In the end it just returns the corresponding + minetest type string, so `mt.node.types.default.dirt` returns the string 'default:dirt'. + It's a nice shortcut in REPL, cause with auto completion you have only pressed 2-4 keys to get to your type. + + :Examples: + + Directly access a type: + + >>> mt.node.type.default.dirt + 'default:dirt' + + Iterate over all available types: + + >>> for node_type in mt.node.type: + >>> print(node_type) + default:pine_tree + default:dry_grass_5 + farming:desert_sand_soil + ... (there should be over 400 different types) + >>> print(len(mt.node.type)) + 421 + + Get a list of all types: + + >>> list(mt.node.type) + ['default:pine_tree', 'default:dry_grass_5', 'farming:desert_sand_soil', ... + + Add 99 dirt to player "IloveDirt"'s inventory: + + >>> mt.player.IloveDirt.inventory.add(mt.node.type.default.dirt, 99) + + :rtype: :class:`TypeIterable` + :return: :class:`TypeIterable` object with categories. Look at the examples above for usage. + """ + return self._types + + def set(self, nodes: Union[dict, list], name: str = None, offset: dict = None) -> None: + """ + Set a single or multiple nodes at given position to another node type + (something like mt.node.type.default.apple). + You can get a list of all available nodes with :attr:`~miney.Minetest.node.type` + + A node is defined as a dict with these keys: + * "x", "y", and "z" keys to define the absolute position + * "name" for a the node type like "default:dirt" (you can also get that from mt.node.type.default.dirt). + Dicts without name will be set as "air" + * some other optional minetest parameters + + **The nodes parameter can be a single dict with the above parameters + or a list of these dicts for bulk spawning.** + + :Examples: + + Set a single node over : + + >>> mt.node.set(mt.player[0].nodes, mt.node) + + :param nodes: A dict or a list of dicts with node definitions + :param name: a type name like "default:dirt" as string or from :attr:`~miney.Minetest.node.type`. This overrides + node names defined in the :attr:`nodes` dict + :param offset: A dict with "x", "y", "z" keys. All node positions will be added with this values. + """ + + _nodes = deepcopy(nodes) + + if offset: + if not all(pos in ['x', 'y', 'z'] for pos in offset): + raise ValueError("offset parameter dict needs y, x and z keys") + + # Set a single node + if type(_nodes) is dict: + if offset: + _nodes["x"] = _nodes["x"] + offset["x"] + _nodes["y"] = _nodes["y"] + offset["y"] + _nodes["z"] = _nodes["z"] + offset["z"] + if name: + _nodes["name"] = name + self.mt.lua.run(f"minetest.set_node({self.mt.lua.dumps(_nodes)}, {{name=\"{_nodes['name']}\"}})") + + # Set many blocks + elif type(_nodes) is list: + + lua = "" + # Loop over nodes, modify name/type, position/offset and generate lua code + for node in _nodes: + # default name to 'air' + if "name" not in node and not name: + node["name"] = "air" + + if name: + if "name" not in node or (node["name"] != "air" and node["name"] != "ignore"): + node["name"] = name + + if offset: + node["x"] = node["x"] + offset["x"] + node["y"] = node["y"] + offset["y"] + node["z"] = node["z"] + offset["z"] + + if node["name"] != "ignore": + lua = lua + f"minetest.set_node(" \ + f"{self.mt.lua.dumps({'x': node['x'], 'y': node['y'], 'z': node['z']})}, " \ + f"{{name=\"{node['name']}\"}})\n" + self.mt.lua.run(lua) + + def get(self, position: dict, position2: dict = None, relative: bool = True, + offset: dict = None) -> Union[dict, list]: + """ + Get the node at given position. It returns a dict with the node definition. + This contains the "x", "y", "z", "param1", "param2" and "name" keys, where "name" is the node type like + "default:dirt". + + If also position2 is given, this function returns a list of dicts with node definitions. This list contains a + cuboid of definitions with the diagonal between position and position2. + + You can get a list of all available node types with :attr:`~miney.Minetest.node.type`. + + :param position: A dict with x,y,z keys + :param position2: Another point, to get multiple nodes as a list + :param relative: Return relative or absolute positions + :param offset: A dict with "x", "y", "z" keys. All node positions will be added with this values. + :return: The node type on this position + """ + if type(position) is dict and not position2: # for a single node + _position = deepcopy(position) + if offset: + _position["x"] = _position["x"] + offset["x"] + _position["y"] = _position["y"] + offset["y"] + _position["z"] = _position["z"] + offset["z"] + + node = self.mt.lua.run(f"return minetest.get_node({self.mt.lua.dumps(position)})") + node["x"] = position["x"] + node["y"] = position["y"] + node["z"] = position["z"] + return node + elif type(position) is dict and type(position2) is dict: # Multiple nodes + _position = deepcopy(position) + _position2 = deepcopy(position2) + + if offset: + _position["x"] = _position["x"] + offset["x"] + _position["y"] = _position["y"] + offset["y"] + _position["z"] = _position["z"] + offset["z"] + _position2["x"] = _position2["x"] + offset["x"] + _position2["y"] = _position2["y"] + offset["y"] + _position2["z"] = _position2["z"] + offset["z"] + + nodes_relative = """ + node["x"] = x - start_x + node["y"] = y - start_y + node["z"] = z - start_z + """ + + nodes_absolute = """ + node["x"] = x + node["y"] = y + node["z"] = z + """ + + return self.mt.lua.run( + f""" + pos1 = {self.mt.lua.dumps(_position)} + pos2 = {self.mt.lua.dumps(_position2)} + minetest.load_area(pos1, pos2) + nodes = {{}} + if pos1.x <= pos2.x then start_x = pos1.x end_x = pos2.x else start_x = pos2.x end_x = pos1.x end + if pos1.y <= pos2.y then start_y = pos1.y end_y = pos2.y else start_y = pos2.y end_y = pos1.y end + if pos1.z <= pos2.z then start_z = pos1.z end_z = pos2.z else start_z = pos2.z end_z = pos1.z end + + for x = start_x, end_x do + for y = start_y, end_y do + for z = start_z, end_z do + node = minetest.get_node({{x = x, y = y, z = z}}) + {nodes_relative if relative else nodes_absolute} + nodes[#nodes+1] = node -- append node + end + end + end + return nodes""", timeout=180) + + def __repr__(self): + return '' + + +class TypeIterable: + """Node type, implemented as iterable for easy autocomplete in the interactive shell""" + def __init__(self, parent, nodes_types=None): + + self.__parent = parent + + if nodes_types: + + # get type categories list + type_categories = {} + for ntype in nodes_types: + if ":" in ntype: + type_categories[ntype.split(":")[0]] = ntype.split(":")[0] + for tc in dict.fromkeys(type_categories): + self.__setattr__(tc, TypeIterable(parent)) + + # values to categories + for ntype in nodes_types: + if ":" in ntype: + self.__getattribute__(ntype.split(":")[0]).__setattr__(ntype.split(":")[1], ntype) + else: + self.__setattr__(ntype, ntype) # for 'air' and 'ignore' + + def __iter__(self): + # todo: list(mt.node.type.default) should return only default group + return iter(self.__parent._types_cache) + + def __getitem__(self, item_key): + if type(self.__parent) is not type(self): # if we don't have a category below + return self.__getattribute__(item_key) + if item_key in self.__parent.node_types: + return item_key + else: + if type(item_key) == int: + return self.__parent.node_types[item_key] + raise IndexError("unknown node type") + + def __len__(self): + return len(self.__parent._types_cache) diff --git a/miney/player.py b/miney/player.py new file mode 100644 index 0000000..a714c7c --- /dev/null +++ b/miney/player.py @@ -0,0 +1,335 @@ +from typing import Union +import miney + +# todo: set modes creative/survival -> Not possible without installed minetest mods + + +class Player: + """ + A player of the minetest server. + """ + def __init__(self, minetest: miney.Minetest, name): + """ + Initialize the player object. + + :param minetest: Parent minetest object + :param name: Player name + """ + self.mt = minetest + self.name = name + + # get user data: password hash, last login, privileges + data = self.mt.lua.run("return minetest.get_auth_handler().get_auth('{}')".format(self.name)) + if data and all(k in data for k in ("password", "last_login", "privileges")): # if we have all keys + self.password = data["password"] + self.last_login = data["last_login"] + self.privileges = data["privileges"] + else: + raise miney.PlayerInvalid("There is no player with that name") + + self.inventory: miney.Inventory = miney.Inventory(minetest, self) + """Manipulate player's inventory. + + :Example to add 99 dirt to player "IloveDirt"'s inventory: + + >>> import miney + >>> mt = miney.Minetest() + >>> mt.player.IloveDirt.inventory.add(mt.node.type.default.dirt, 99) + + :Example to remove 99 dirt from player "IhateDirt"'s inventory: + + >>> import miney + >>> mt = miney.Minetest() + >>> mt.player.IhateDirt.inventory.remove(mt.node.type.default.dirt, 99) + + """ + + def __repr__(self): + return ''.format(self.name) + + @property + def is_online(self) -> bool: + """ + Returns the online status of this player. + + :return: True or False + """ + # TODO: Better check without provoke a lua error + try: + if self.name == self.mt.lua.run( + "return minetest.get_player_by_name('{}'):get_player_name()".format(self.name)): + return True + except miney.LuaError: + return False + + @property + def position(self) -> dict: + """ + Get the players current position. + + :return: A dict with x,y,z keys: {"x": 0, "y":1, "z":2} + """ + try: + return self.mt.lua.run("return minetest.get_player_by_name('{}'):get_pos()".format(self.name)) + except miney.LuaError: + raise miney.PlayerOffline("The player has no position, he could be offline") + + @position.setter + def position(self, values: dict): + """ + Set player position + :param values: + :return: + """ + if all(k in values for k in ("x", "y", "z")): # if we have all keys + self.mt.lua.run( + "return minetest.get_player_by_name('{}'):set_pos({{x = {}, y = {}, z = {}}})".format( + self.name, + values["x"], + values["y"], + values["z"], + ) + ) + else: + raise miney.NoValidPosition( + "A valid position need x,y,z values in an dict ({\"x\": 12, \"y\": 70, \"z\": 12}).") + + @property + def speed(self) -> int: + """ + Get or set the players speed. Default is 1. + + :return: Float + """ + return self.mt.lua.run( + "return minetest.get_player_by_name('{}'):get_physics_override()".format(self.name))["speed"] + + @speed.setter + def speed(self, value: int): + self.mt.lua.run( + "return minetest.get_player_by_name('{}'):set_physics_override({{speed = {}}})".format(self.name, value)) + + @property + def jump(self): + """ + Get or set the players jump height. Default is 1. + + :return: Float + """ + return self.mt.lua.run( + "return minetest.get_player_by_name('{}'):get_physics_override()".format(self.name))["jump"] + + @jump.setter + def jump(self, value): + self.mt.lua.run( + "return minetest.get_player_by_name('{}'):set_physics_override({{jump = {}}})".format(self.name, value)) + + @property + def gravity(self): + """ + Get or set the players gravity. Default is 1. + + :return: Float + """ + return self.mt.lua.run( + "return minetest.get_player_by_name('{}'):get_physics_override()".format(self.name))["gravity"] + + @gravity.setter + def gravity(self, value): + self.mt.lua.run( + "return minetest.get_player_by_name('{}'):set_physics_override({{gravity = {}}})".format(self.name, value)) + + @property + def look(self) -> dict: + """ + Get and set look in radians. Horizontal angle is counter-clockwise from the +z direction. Vertical angle ranges + between -pi/2 (~-1.563) and pi/2 (~1.563), which are straight up and down respectively. + + :return: A dict like {'v': 0.34, 'h': 2.50} where h is horizontal and v = vertical + """ + + return self.mt.lua.run( + f"return {{" + f"h=minetest.get_player_by_name('{self.name}'):get_look_horizontal(), " + f"v=minetest.get_player_by_name('{self.name}'):get_look_vertical()" + f"}}" + ) + + @look.setter + def look(self, value: dict): + if type(value) is dict: + if "v" in value and "h" in value: + if type(value["v"]) in [int, float] and type(value["h"]) in [int, float]: + self.mt.lua.run( + f""" + local player = minetest.get_player_by_name('{self.name}') + player:set_look_horizontal({value["h"]}) + player:set_look_vertical({value["v"]}) + return true + """ + ) + else: + raise TypeError("values for v or h aren't float or int") + else: + raise TypeError("There isn't the required v or h key in the dict") + else: + raise TypeError("The value isn't a dict, as required. Use a dict in the form: {\"h\": 1.1, \"v\": 1.1}") + + @property + def look_vertical(self): + """ + Get and set pitch in radians. Angle ranges between -pi/2 (~-1.563) and pi/2 (~1.563), which are straight + up and down respectively. + + :return: Pitch in radians + """ + return self.mt.lua.run("return minetest.get_player_by_name('{}'):get_look_vertical()".format(self.name)) + + @look_vertical.setter + def look_vertical(self, value): + self.mt.lua.run("return minetest.get_player_by_name('{}'):set_look_vertical({})".format(self.name, value)) + + @property + def look_horizontal(self): + """ + Get and set yaw in radians. Angle is counter-clockwise from the +z direction. + + :return: Pitch in radians + """ + return self.mt.lua.run("return minetest.get_player_by_name('{}'):get_look_horizontal()".format(self.name)) + + @look_horizontal.setter + def look_horizontal(self, value): + self.mt.lua.run("return minetest.get_player_by_name('{}'):set_look_horizontal({})".format(self.name, value)) + + @property + def hp(self): + """ + Get and set the number of hitpoints (2 * number of hearts) between 0 and 20. + By setting his hitpoint to zero you instantly kill this player. + + :return: + """ + return self.mt.lua.run(f"return minetest.get_player_by_name('{self.name}'):get_hp()") + + @hp.setter + def hp(self, value: int): + if type(value) is int and value in range(0, 21): + self.mt.lua.run( + f"return minetest.get_player_by_name('{self.name}'):set_hp({value}, {{type=\"set_hp\"}})") + else: + raise ValueError("HP has to be between 0 and 20.") + + @property + def breath(self): + return self.mt.lua.run(f"return minetest.get_player_by_name('{self.name}'):get_breath()") + + @breath.setter + def breath(self, value: int): + if type(value) is int and value in range(0, 21): + self.mt.lua.run( + f"return minetest.get_player_by_name('{self.name}'):set_breath({value}, {{type=\"set_hp\"}})") + else: + raise ValueError("HP has to be between 0 and 20.") + + @property + def fly(self) -> bool: + """ + Get and set the privilege to fly to this player. Press K to enable and disable fly mode. + As a shortcut you can set fly to a number instead if `True` to also changes the players speed to this number. + + .. Example: + + >>> mt.player.MineyPlayer.fly = True # the can player fly + >>> mt.player.MineyPlayer.fly = 5 # the can player fly 5 times faster + + :return: + """ + return self.mt.lua.run( + f""" + local privs = minetest.get_player_privs(\"{self.name}\") + if privs["fly"] then + return true + else + return false + end + """ + ) + + @fly.setter + def fly(self, value: Union[bool, int]): + if value: + state = "true" + if type(value) is int: + if value > 0: + self.speed = value + else: + state = "false" + self.mt.lua.run( + f""" + local privs = minetest.get_player_privs(\"{self.name}\") + privs["fly"] = {state} + minetest.set_player_privs(\"{self.name}\", privs) + """ + ) + + @property + def creative(self) -> bool: + return self.mt.lua.run( + f""" + local privs = minetest.get_player_privs(\"{self.name}\") + if privs["creative"] then + return true + else + return false + end + """ + ) + + @creative.setter + def creative(self, value: bool): + if type(value) is not bool: + raise ValueError("creative needs to be true or false") + if value is True: + state = "true" + else: + state = "false" + + luastring = f""" + local privs = minetest.get_player_privs(\"{self.name}\") + privs["creative"] = {state} + minetest.set_player_privs(\"{self.name}\", privs) + """ + self.mt.lua.run( + luastring + ) + + +class PlayerIterable: + """Player, implemented as iterable for easy autocomplete in the interactive shell""" + def __init__(self, minetest: miney.Minetest, online_players: list = None): + if online_players: + self.__online_players = online_players + self.__mt = minetest + + # update list + for player in online_players: + self.__setattr__(player, miney.Player(minetest, player)) + + def __iter__(self): + player_object = [] + for player in self.__online_players: + player_object.append(miney.Player(self.__mt, player)) + + return iter(player_object) + + def __getitem__(self, item_key) -> Player: + if item_key in self.__online_players: + return self.__getattribute__(item_key) + else: + if type(item_key) == int: + return self.__getattribute__(self.__online_players[item_key]) + raise IndexError("unknown player") + + def __len__(self): + return len(self.__online_players) \ No newline at end of file diff --git a/miney/tool.py b/miney/tool.py new file mode 100644 index 0000000..73e5b65 --- /dev/null +++ b/miney/tool.py @@ -0,0 +1,37 @@ +class ToolIterable: + """Tool type, implemented as iterable for easy autocomplete in the interactive shell""" + def __init__(self, parent, tool_types=None): + + self.__parent = parent + + if tool_types: + + # get type categories list + type_categories = {} + for ntype in tool_types: + if ":" in ntype: + type_categories[ntype.split(":")[0]] = ntype.split(":")[0] + for tc in dict.fromkeys(type_categories): + self.__setattr__(tc, ToolIterable(parent)) + + # values to categories + for ttype in tool_types: + if ":" in ttype: + self.__getattribute__(ttype.split(":")[0]).__setattr__(ttype.split(":")[1], ttype) + else: + self.__setattr__(ttype, ttype) # for 'air' and 'ignore' + + def __iter__(self): + # todo: list(mt.node.tool.default) should return only default group + return iter(self.__parent._tools_cache) + + def __getitem__(self, item_key): + if item_key in self.__parent.node_types: + return item_key + else: + if type(item_key) == int: + return self.__parent.node_types[item_key] + raise IndexError("unknown node type") + + def __len__(self): + return len(self.__parent._tools_cache) diff --git a/miney/vector.py b/miney/vector.py new file mode 100644 index 0000000..18948ee --- /dev/null +++ b/miney/vector.py @@ -0,0 +1,65 @@ +# https://github.com/kirill-belousov/vector3d/blob/master/vector3d/point.py +from math import sqrt, acos, degrees + + +class Vector: + x = float() + y = float() + z = float() + + def __init__(self, x: float = 0, y: float = 0, z: float = 0): + self.x = x + self.y = y + self.z = z + + def __eq__(self, other): + if self.x == other.x and self.y == other.y and self.z == other.z: + return True + else: + return False + + def __len__(self): + return int(self.length()) + + def __add__(self, o): + return Vector((self.x + o.x), (self.y + o.y), (self.z + o.z)) + + def __sub__(self, o): + return Vector((self.x - o.x), (self.y - o.y), (self.z - o.z)) + + def __mul__(self, o): + return (self.x * o.x) + (self.y * o.y) + (self.z * o.z) + + def __iadd__(self, o): + self.x += o.x + self.y += o.y + self.z += o.z + return self + + def __isub__(self, o): + self.x -= o.x + self.y -= o.y + self.z -= o.z + return self + + def __neg__(self): + return Vector(-self.x, -self.y, -self.z) + + def length(self): + return sqrt((self.x * self.x) + (self.y * self.y) + (self.z * self.z)) + + def normalize(self): + return Vector((self.x / self.length()), (self.y / self.length()), (self.z / self.length())) + + +def angle(a, b): + m = a.x * b.x + a.y * b.y + a.z * b.z + return degrees(acos(m / (a.length() * b.length()))) + + +def horizontal_angle(a, b): + return angle(Vector(a.x, a.y, 0), Vector(b.x, b.y, 0)) + + +def vertical_angle(a, b): + return angle(Vector(0, a.y, a.z), Vector(0, b.y, b.z)) diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..c6d28a7 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1033 @@ +# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. + +[[package]] +name = "alabaster" +version = "1.0.0" +description = "A light, configurable Sphinx theme" +optional = false +python-versions = ">=3.10" +files = [ + {file = "alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b"}, + {file = "alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e"}, +] + +[[package]] +name = "babel" +version = "2.16.0" +description = "Internationalization utilities" +optional = false +python-versions = ">=3.8" +files = [ + {file = "babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b"}, + {file = "babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"}, +] + +[package.extras] +dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] + +[[package]] +name = "certifi" +version = "2024.8.30" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, + {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, +] + +[[package]] +name = "cffi" +version = "1.17.1" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, + {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, + {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, + {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, + {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, + {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, + {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, + {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, + {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, + {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, + {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, + {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, + {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, + {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, + {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, +] + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "charset-normalizer" +version = "3.4.0" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"}, + {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"}, + {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "cryptography" +version = "44.0.0" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = "!=3.9.0,!=3.9.1,>=3.7" +files = [ + {file = "cryptography-44.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:84111ad4ff3f6253820e6d3e58be2cc2a00adb29335d4cacb5ab4d4d34f2a123"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15492a11f9e1b62ba9d73c210e2416724633167de94607ec6069ef724fad092"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831c3c4d0774e488fdc83a1923b49b9957d33287de923d58ebd3cec47a0ae43f"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:60eb32934076fa07e4316b7b2742fa52cbb190b42c2df2863dbc4230a0a9b385"}, + {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e"}, + {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e"}, + {file = "cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053"}, + {file = "cryptography-44.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:abc998e0c0eee3c8a1904221d3f67dcfa76422b23620173e28c11d3e626c21bd"}, + {file = "cryptography-44.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:660cb7312a08bc38be15b696462fa7cc7cd85c3ed9c576e81f4dc4d8b2b31591"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1923cb251c04be85eec9fda837661c67c1049063305d6be5721643c22dd4e2b7"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:404fdc66ee5f83a1388be54300ae978b2efd538018de18556dde92575e05defc"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9abcc2e083cbe8dde89124a47e5e53ec38751f0d7dfd36801008f316a127d7ba"}, + {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64"}, + {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285"}, + {file = "cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417"}, + {file = "cryptography-44.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:708ee5f1bafe76d041b53a4f95eb28cdeb8d18da17e597d46d7833ee59b97ede"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:37d76e6863da3774cd9db5b409a9ecfd2c71c981c38788d3fcfaf177f447b731"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:f677e1268c4e23420c3acade68fac427fffcb8d19d7df95ed7ad17cdef8404f4"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f5e7cb1e5e56ca0933b4873c0220a78b773b24d40d186b6738080b73d3d0a756"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:8b3e6eae66cf54701ee7d9c83c30ac0a1e3fa17be486033000f2a73a12ab507c"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:be4ce505894d15d5c5037167ffb7f0ae90b7be6f2a98f9a5c3442395501c32fa"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:62901fb618f74d7d81bf408c8719e9ec14d863086efe4185afd07c352aee1d2c"}, + {file = "cryptography-44.0.0.tar.gz", hash = "sha256:cd4e834f340b4293430701e772ec543b0fbe6c2dea510a5286fe0acabe153a02"}, +] + +[package.dependencies] +cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=3.0.0)"] +docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] +nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2)"] +pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] +sdist = ["build (>=1.0.0)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi (>=2024)", "cryptography-vectors (==44.0.0)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] +test-randomorder = ["pytest-randomly"] + +[[package]] +name = "docutils" +version = "0.21.2" +description = "Docutils -- Python Documentation Utilities" +optional = false +python-versions = ">=3.9" +files = [ + {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, + {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, +] + +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "imagesize" +version = "1.4.1" +description = "Getting image size from png/jpeg/jpeg2000/gif file" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, + {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "jaraco-classes" +version = "3.4.0" +description = "Utility functions for Python class constructs" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790"}, + {file = "jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd"}, +] + +[package.dependencies] +more-itertools = "*" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] + +[[package]] +name = "jaraco-context" +version = "6.0.1" +description = "Useful decorators and context managers" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4"}, + {file = "jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3"}, +] + +[package.extras] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +test = ["portend", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] + +[[package]] +name = "jaraco-functools" +version = "4.1.0" +description = "Functools like those found in stdlib" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jaraco.functools-4.1.0-py3-none-any.whl", hash = "sha256:ad159f13428bc4acbf5541ad6dec511f91573b90fba04df61dafa2a1231cf649"}, + {file = "jaraco_functools-4.1.0.tar.gz", hash = "sha256:70f7e0e2ae076498e212562325e805204fc092d7b4c17e0e86c959e249701a9d"}, +] + +[package.dependencies] +more-itertools = "*" + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["jaraco.classes", "pytest (>=6,!=8.1.*)"] +type = ["pytest-mypy"] + +[[package]] +name = "jeepney" +version = "0.8.0" +description = "Low-level, pure Python DBus protocol wrapper." +optional = false +python-versions = ">=3.7" +files = [ + {file = "jeepney-0.8.0-py3-none-any.whl", hash = "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755"}, + {file = "jeepney-0.8.0.tar.gz", hash = "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806"}, +] + +[package.extras] +test = ["async-timeout", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"] +trio = ["async_generator", "trio"] + +[[package]] +name = "jinja2" +version = "3.1.4" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, + {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "keyring" +version = "25.5.0" +description = "Store and access your passwords safely." +optional = false +python-versions = ">=3.8" +files = [ + {file = "keyring-25.5.0-py3-none-any.whl", hash = "sha256:e67f8ac32b04be4714b42fe84ce7dad9c40985b9ca827c592cc303e7c26d9741"}, + {file = "keyring-25.5.0.tar.gz", hash = "sha256:4c753b3ec91717fe713c4edd522d625889d8973a349b0e582622f49766de58e6"}, +] + +[package.dependencies] +"jaraco.classes" = "*" +"jaraco.context" = "*" +"jaraco.functools" = "*" +jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""} +pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""} +SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +completion = ["shtab (>=1.1.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["pyfakefs", "pytest (>=6,!=8.1.*)"] +type = ["pygobject-stubs", "pytest-mypy", "shtab", "types-pywin32"] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.8" +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "markupsafe" +version = "3.0.2" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +files = [ + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "more-itertools" +version = "10.5.0" +description = "More routines for operating on iterables, beyond itertools" +optional = false +python-versions = ">=3.8" +files = [ + {file = "more-itertools-10.5.0.tar.gz", hash = "sha256:5482bfef7849c25dc3c6dd53a6173ae4795da2a41a80faea6700d9f5846c5da6"}, + {file = "more_itertools-10.5.0-py3-none-any.whl", hash = "sha256:037b0d3203ce90cca8ab1defbbdac29d5f993fc20131f3664dc8d6acfa872aef"}, +] + +[[package]] +name = "nh3" +version = "0.2.19" +description = "Python bindings to the ammonia HTML sanitization library." +optional = false +python-versions = "*" +files = [ + {file = "nh3-0.2.19-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:ec9c8bf86e397cb88c560361f60fdce478b5edb8b93f04ead419b72fbe937ea6"}, + {file = "nh3-0.2.19-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0adf00e2b2026fa10a42537b60d161e516f206781c7515e4e97e09f72a8c5d0"}, + {file = "nh3-0.2.19-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3805161c4e12088bd74752ba69630e915bc30fe666034f47217a2f16b16efc37"}, + {file = "nh3-0.2.19-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3dedd7858a21312f7675841529941035a2ac91057db13402c8fe907aa19205a"}, + {file = "nh3-0.2.19-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:0b6820fc64f2ff7ef3e7253a093c946a87865c877b3889149a6d21d322ed8dbd"}, + {file = "nh3-0.2.19-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:833b3b5f1783ce95834a13030300cea00cbdfd64ea29260d01af9c4821da0aa9"}, + {file = "nh3-0.2.19-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5d4f5e2189861b352b73acb803b5f4bb409c2f36275d22717e27d4e0c217ae55"}, + {file = "nh3-0.2.19-cp313-cp313t-win32.whl", hash = "sha256:2b926f179eb4bce72b651bfdf76f8aa05d167b2b72bc2f3657fd319f40232adc"}, + {file = "nh3-0.2.19-cp313-cp313t-win_amd64.whl", hash = "sha256:ac536a4b5c073fdadd8f5f4889adabe1cbdae55305366fb870723c96ca7f49c3"}, + {file = "nh3-0.2.19-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:c2e3f0d18cc101132fe10ab7ef5c4f41411297e639e23b64b5e888ccaad63f41"}, + {file = "nh3-0.2.19-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11270b16c1b012677e3e2dd166c1aa273388776bf99a3e3677179db5097ee16a"}, + {file = "nh3-0.2.19-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fc483dd8d20f8f8c010783a25a84db3bebeadced92d24d34b40d687f8043ac69"}, + {file = "nh3-0.2.19-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d53a4577b6123ca1d7e8483fad3e13cb7eda28913d516bd0a648c1a473aa21a9"}, + {file = "nh3-0.2.19-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fdb20740d24ab9f2a1341458a00a11205294e97e905de060eeab1ceca020c09c"}, + {file = "nh3-0.2.19-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d8325d51e47cb5b11f649d55e626d56c76041ba508cd59e0cb1cf687cc7612f1"}, + {file = "nh3-0.2.19-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8eb7affc590e542fa7981ef508cd1644f62176bcd10d4429890fc629b47f0bc"}, + {file = "nh3-0.2.19-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2eb021804e9df1761abeb844bb86648d77aa118a663c82f50ea04110d87ed707"}, + {file = "nh3-0.2.19-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a7b928862daddb29805a1010a0282f77f4b8b238a37b5f76bc6c0d16d930fd22"}, + {file = "nh3-0.2.19-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ed06ed78f6b69d57463b46a04f68f270605301e69d80756a8adf7519002de57d"}, + {file = "nh3-0.2.19-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:df8eac98fec80bd6f5fd0ae27a65de14f1e1a65a76d8e2237eb695f9cd1121d9"}, + {file = "nh3-0.2.19-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:00810cd5275f5c3f44b9eb0e521d1a841ee2f8023622de39ffc7d88bd533d8e0"}, + {file = "nh3-0.2.19-cp38-abi3-win32.whl", hash = "sha256:7e98621856b0a911c21faa5eef8f8ea3e691526c2433f9afc2be713cb6fbdb48"}, + {file = "nh3-0.2.19-cp38-abi3-win_amd64.whl", hash = "sha256:75c7cafb840f24430b009f7368945cb5ca88b2b54bb384ebfba495f16bc9c121"}, + {file = "nh3-0.2.19.tar.gz", hash = "sha256:790056b54c068ff8dceb443eaefb696b84beff58cca6c07afd754d17692a4804"}, +] + +[[package]] +name = "packaging" +version = "24.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, +] + +[[package]] +name = "pkginfo" +version = "1.11.2" +description = "Query metadata from sdists / bdists / installed packages." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pkginfo-1.11.2-py3-none-any.whl", hash = "sha256:9ec518eefccd159de7ed45386a6bb4c6ca5fa2cb3bd9b71154fae44f6f1b36a3"}, + {file = "pkginfo-1.11.2.tar.gz", hash = "sha256:c6bc916b8298d159e31f2c216e35ee5b86da7da18874f879798d0a1983537c86"}, +] + +[package.extras] +testing = ["pytest", "pytest-cov", "wheel"] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pycparser" +version = "2.22" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] + +[[package]] +name = "pygments" +version = "2.18.0" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, + {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pytest" +version = "8.3.4" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, + {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2" + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +description = "A (partial) reimplementation of pywin32 using ctypes/cffi" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755"}, + {file = "pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8"}, +] + +[[package]] +name = "readme-renderer" +version = "44.0" +description = "readme_renderer is a library for rendering readme descriptions for Warehouse" +optional = false +python-versions = ">=3.9" +files = [ + {file = "readme_renderer-44.0-py3-none-any.whl", hash = "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151"}, + {file = "readme_renderer-44.0.tar.gz", hash = "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1"}, +] + +[package.dependencies] +docutils = ">=0.21.2" +nh3 = ">=0.2.14" +Pygments = ">=2.5.1" + +[package.extras] +md = ["cmarkgfm (>=0.8.0)"] + +[[package]] +name = "requests" +version = "2.32.3" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.8" +files = [ + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +description = "A utility belt for advanced users of python-requests" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"}, + {file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"}, +] + +[package.dependencies] +requests = ">=2.0.1,<3.0.0" + +[[package]] +name = "rfc3986" +version = "2.0.0" +description = "Validating URI References per RFC 3986" +optional = false +python-versions = ">=3.7" +files = [ + {file = "rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd"}, + {file = "rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c"}, +] + +[package.extras] +idna2008 = ["idna"] + +[[package]] +name = "rich" +version = "13.9.4" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, + {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "secretstorage" +version = "3.3.3" +description = "Python bindings to FreeDesktop.org Secret Service API" +optional = false +python-versions = ">=3.6" +files = [ + {file = "SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99"}, + {file = "SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77"}, +] + +[package.dependencies] +cryptography = ">=2.0" +jeepney = ">=0.6" + +[[package]] +name = "snowballstemmer" +version = "2.2.0" +description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." +optional = false +python-versions = "*" +files = [ + {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, + {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, +] + +[[package]] +name = "sphinx" +version = "8.1.3" +description = "Python documentation generator" +optional = false +python-versions = ">=3.10" +files = [ + {file = "sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2"}, + {file = "sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927"}, +] + +[package.dependencies] +alabaster = ">=0.7.14" +babel = ">=2.13" +colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""} +docutils = ">=0.20,<0.22" +imagesize = ">=1.3" +Jinja2 = ">=3.1" +packaging = ">=23.0" +Pygments = ">=2.17" +requests = ">=2.30.0" +snowballstemmer = ">=2.2" +sphinxcontrib-applehelp = ">=1.0.7" +sphinxcontrib-devhelp = ">=1.0.6" +sphinxcontrib-htmlhelp = ">=2.0.6" +sphinxcontrib-jsmath = ">=1.0.1" +sphinxcontrib-qthelp = ">=1.0.6" +sphinxcontrib-serializinghtml = ">=1.1.9" + +[package.extras] +docs = ["sphinxcontrib-websupport"] +lint = ["flake8 (>=6.0)", "mypy (==1.11.1)", "pyright (==1.1.384)", "pytest (>=6.0)", "ruff (==0.6.9)", "sphinx-lint (>=0.9)", "tomli (>=2)", "types-Pillow (==10.2.0.20240822)", "types-Pygments (==2.18.0.20240506)", "types-colorama (==0.4.15.20240311)", "types-defusedxml (==0.7.0.20240218)", "types-docutils (==0.21.0.20241005)", "types-requests (==2.32.0.20240914)", "types-urllib3 (==1.26.25.14)"] +test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "setuptools (>=70.0)", "typing_extensions (>=4.9)"] + +[[package]] +name = "sphinx-rtd-theme" +version = "3.0.2" +description = "Read the Docs theme for Sphinx" +optional = false +python-versions = ">=3.8" +files = [ + {file = "sphinx_rtd_theme-3.0.2-py2.py3-none-any.whl", hash = "sha256:422ccc750c3a3a311de4ae327e82affdaf59eb695ba4936538552f3b00f4ee13"}, + {file = "sphinx_rtd_theme-3.0.2.tar.gz", hash = "sha256:b7457bc25dda723b20b086a670b9953c859eab60a2a03ee8eb2bb23e176e5f85"}, +] + +[package.dependencies] +docutils = ">0.18,<0.22" +sphinx = ">=6,<9" +sphinxcontrib-jquery = ">=4,<5" + +[package.extras] +dev = ["bump2version", "transifex-client", "twine", "wheel"] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" +optional = false +python-versions = ">=3.9" +files = [ + {file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"}, + {file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" +optional = false +python-versions = ">=3.9" +files = [ + {file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"}, + {file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" +optional = false +python-versions = ">=3.9" +files = [ + {file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"}, + {file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["html5lib", "pytest"] + +[[package]] +name = "sphinxcontrib-jquery" +version = "4.1" +description = "Extension to include jQuery on newer Sphinx releases" +optional = false +python-versions = ">=2.7" +files = [ + {file = "sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a"}, + {file = "sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae"}, +] + +[package.dependencies] +Sphinx = ">=1.8" + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +description = "A sphinx extension which renders display math in HTML via JavaScript" +optional = false +python-versions = ">=3.5" +files = [ + {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, + {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, +] + +[package.extras] +test = ["flake8", "mypy", "pytest"] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" +optional = false +python-versions = ">=3.9" +files = [ + {file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"}, + {file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["defusedxml (>=0.7.1)", "pytest"] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" +optional = false +python-versions = ">=3.9" +files = [ + {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"}, + {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["pytest"] + +[[package]] +name = "twine" +version = "6.0.1" +description = "Collection of utilities for publishing packages on PyPI" +optional = false +python-versions = ">=3.8" +files = [ + {file = "twine-6.0.1-py3-none-any.whl", hash = "sha256:9c6025b203b51521d53e200f4a08b116dee7500a38591668c6a6033117bdc218"}, + {file = "twine-6.0.1.tar.gz", hash = "sha256:36158b09df5406e1c9c1fb8edb24fc2be387709443e7376689b938531582ee27"}, +] + +[package.dependencies] +keyring = {version = ">=15.1", markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\""} +packaging = "*" +pkginfo = ">=1.8.1" +readme-renderer = ">=35.0" +requests = ">=2.20" +requests-toolbelt = ">=0.8.0,<0.9.0 || >0.9.0" +rfc3986 = ">=1.4.0" +rich = ">=12.0.0" +urllib3 = ">=1.26.0" + +[package.extras] +keyring = ["keyring (>=15.1)"] + +[[package]] +name = "urllib3" +version = "2.2.3" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, + {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "wheel" +version = "0.45.1" +description = "A built-package format for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "wheel-0.45.1-py3-none-any.whl", hash = "sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248"}, + {file = "wheel-0.45.1.tar.gz", hash = "sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729"}, +] + +[package.extras] +test = ["pytest (>=6.0.0)", "setuptools (>=65)"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.12" +content-hash = "5c35f60d80ad556e377fa58532b56dbb0826955d2dd1283f5729ced0a51e316b" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..40d1b34 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,19 @@ +[tool.poetry] +name = "minetest-network-api-client" +version = "0.1.0" +description = "" +authors = ["bvn13 "] +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.12" +sphinx = "^8.1.3" +sphinx-rtd-theme = "^3.0.2" +wheel = "^0.45.1" +twine = "^6.0.1" +pytest = "^8.3.4" + + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..6e2cf84 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +addopts = -x tests/ \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..c43f3e9 --- /dev/null +++ b/setup.py @@ -0,0 +1,35 @@ +import setuptools +import re, io + +with open("README.md", "r") as fh: + long_description = fh.read() + +# get version from package's __version__ +__version__ = re.search( + r'__version__\s*=\s*[\'"]([^\'"]*)[\'"]', # It excludes inline comment too + io.open('miney/__init__.py', encoding='utf_8_sig').read() + ).group(1) + +setuptools.setup( + name="miney", + version=__version__, + author="Robert Lieback", + author_email="robertlieback@zetabyte.de", + description="The python interface to minetest", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/miney-py/miney", + project_urls={ + "Documentation": "https://miney.readthedocs.io" + }, + packages=["miney"], + classifiers=[ + "Programming Language :: Python :: 3", + "Operating System :: OS Independent", + "Development Status :: 4 - Beta", + "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", + "Operating System :: OS Independent", + "Topic :: Games/Entertainment" + ], + python_requires='>=3.6', +) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..f4b22f5 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,15 @@ +""" +The place for fixtures +""" +import pytest +import miney + + +@pytest.fixture(scope="session") +def mt(): + # if not miney.is_miney_available(): + # assert miney.run_miney_game(), "Minetest with mineysocket isn't running." + + mt = miney.Minetest() + assert len(mt.player) >= 1 + return miney.Minetest() diff --git a/tests/test_minetest.py b/tests/test_minetest.py new file mode 100644 index 0000000..67ff915 --- /dev/null +++ b/tests/test_minetest.py @@ -0,0 +1,109 @@ +""" +Tests Minetest class' methods +""" +import socket +import miney +import pytest + + +def test_connection(mt: miney.Minetest): + """ + Test connection with some missconfigurations. + + :param mt: fixture + :return: None + """ + # wrong server name + with pytest.raises(socket.gaierror) as e: + mt_fail = miney.Minetest("someunresolveableserver", "someuser", "somepass") + + # wrong port / apisocket unreachable or not running + with pytest.raises((socket.timeout, ConnectionResetError)) as e: + mt_fail = miney.Minetest(port=12345) + + +def test_send_corrupt_data(mt: miney.Minetest): + """ + Send corrupt data, they shouldn't crash the server. + + :param mt: fixture + :return: None + """ + mt.connection.sendto( + str.encode( + "}s876" + "\n" + ), + ("127.0.0.1", 29999) + ) + with pytest.raises(miney.exceptions.LuaError): + mt.receive() + + +def test_minetest(mt: miney.Minetest): + """ + Test basic functionality. + + :param mt: fixture + :return: None + """ + assert str(mt) == ''.format("127.0.0.1", "29999") + nodes = mt.node.type + assert "air" in nodes + assert "default:stone" in nodes + + assert (mt.log("Pytest is running...")) is None + + settings = mt.settings + assert "secure.trusted_mods" in settings + assert "name" in settings + + assert 0 < mt.time_of_day < 1.1 + mt.time_of_day = 0.99 + assert 1 > mt.time_of_day > 0.95 + mt.time_of_day = 0.5 + assert 0.51 > mt.time_of_day > 0.49 + + +def test_lua(mt: miney.Minetest): + """ + Test running lua code. + + :param mt: fixture + :return: None + """ + with pytest.raises(miney.LuaError) as e: + mt.lua.run("thatshouldntworkatall") + + # multiple return values + returnvalues = mt.lua.run( + """ + mytable = {} + mytable["var"] = 99 + return 12 , "test", {8, "9"}, mytable + """ + ) + assert returnvalues == (12, 'test', [8, '9'], {'var': 99}) + + +def test_players(mt: miney.Minetest): + """ + Test player count and object creation. + + :param mt: fixture + :return: None + """ + players = mt.player + assert str(type(players)) == "" + assert len(players) >= 1, "You should join the server for tests!" + + assert (mt.chat.send_to_all("Pytest is running...")) is None + + # get unknown player + with pytest.raises(AttributeError) as e: + x = mt.player.stupidname123 + + player = players[0] + assert isinstance(player, miney.player.Player) + assert len(player.name) > 0 + + assert str(player) == "".format(player.name) diff --git a/tests/test_nodes.py b/tests/test_nodes.py new file mode 100644 index 0000000..64b6eaf --- /dev/null +++ b/tests/test_nodes.py @@ -0,0 +1,48 @@ +""" +Test all node related. +""" + +import miney + + +def test_node_types(mt: miney.Minetest): + assert len(mt.node.type) > 400 + assert mt.node.type.default.dirt == "default:dirt" + assert mt.node.type["default"]["dirt"] == "default:dirt" + assert len(mt.node.type["default"]) > 400 + + +def test_node_set_and_get(mt: miney.Minetest): + pos1 = {"x": 22, "y": 28, "z": 22} + + mt.node.set(pos1, name=mt.node.type.default.dirt, offset={"x": -1, "y": -1, "z": -1}) + + pos1_node = mt.node.get(pos1) + assert "name" in pos1_node + assert pos1_node["name"] in mt.node.type + assert "param1" in pos1_node + assert "param2" in pos1_node + + # we create a cube of dirt + nodes = [] + for x in range(0, 9): + for y in range(0, 9): + for z in range(0, 9): + nodes.append({"x": pos1["x"] + x, "y": pos1["y"] + y, "z": pos1["z"] + z, "name": "default:dirt"}) + + # save for later restore + before = mt.node.get(nodes[0], nodes[-1], relative=False) + + mt.node.set(nodes, name="default:dirt") + dirt_nodes = mt.node.get(nodes[0], nodes[-1]) + assert dirt_nodes[0]["name"] in mt.node.type + assert dirt_nodes[0]["name"] == "default:dirt" + assert dirt_nodes[-1]["name"] in mt.node.type + assert dirt_nodes[-1]["name"] == "default:dirt" + assert "param1" in dirt_nodes[0] + assert "param2" in dirt_nodes[0] + + mt.node.set(before, name="default:dirt") + before_nodes = mt.node.get(before[0], before[-1]) + assert before_nodes[0]["name"] == before[0]["name"] + assert before_nodes[-1]["name"] == before[-1]["name"] diff --git a/tests/test_player.py b/tests/test_player.py new file mode 100644 index 0000000..38dafa6 --- /dev/null +++ b/tests/test_player.py @@ -0,0 +1,65 @@ +import pytest +import miney +import math +from time import sleep + + +@pytest.fixture(scope="module") +def mt_player(mt: miney.Minetest): + return mt.player[0] + + +def test_player(mt: miney.Minetest, mt_player: miney.Player): + """ + Test player basics. + + :param mt: fixture + :param mt_player: fixture + :return: None + """ + assert mt_player.is_online is True + + position = mt_player.position + assert "x" in position + assert "y" in position + assert "z" in position + + mt_player.gravity = 0 + assert mt_player.gravity == 0 + + # set fly or player will go into free fall + mt_player.fly = True + assert mt_player.fly + + mt_player.creative = True + assert mt_player.creative + + mt_player.position = {"x": 12, "y": 8.5, "z": 12} + sleep(0.1) # give the value some time to get to the client + position = mt_player.position + assert 12.1 > position["x"] > 11.9 + assert 9 > position["y"] > 8 + assert 12.1 > position["z"] > 11.9 + + mt_player.gravity = 0.8 + assert round(mt_player.gravity, 1) == 0.8 + + mt_player.speed = 2.0 + assert mt_player.speed == 2.0 + + mt_player.jump = 2.0 + assert mt_player.jump == 2.0 + + look_vertical = mt_player.look_vertical + assert 1.563 >= look_vertical >= -1.563 # 1.5620696544647 -1.5620696544647 + mt_player.look_vertical = 1.5620696544647 + sleep(0.01) # give the value some time to get to the client + assert 1.5622 > mt_player.look_vertical > 1.5616 + + look_horizontal = mt_player.look_horizontal + assert 6.25 > look_horizontal > 0 # 0 6.28 + mt_player.look_horizontal = math.pi + sleep(0.1) # give the value some time to get to the client + assert 3.15 > mt_player.look_horizontal > 3.13 + + mt_player.inventory.add("default:axe_wood")