commit d6a06aa697e02346852504f40c038972ee5d51a3 Author: bvn13 Date: Sat Dec 7 01:17:53 2024 +0300 working client 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 0000000..4879367 Binary files /dev/null and b/docs/minetest-logo.png differ diff --git a/docs/minetest.svg b/docs/minetest.svg new file mode 100644 index 0000000..fe036c3 --- /dev/null +++ b/docs/minetest.svg @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/miney-logo.png b/docs/miney-logo.png new file mode 100644 index 0000000..5088d6f Binary files /dev/null and b/docs/miney-logo.png differ diff --git a/docs/miney-slogan.png b/docs/miney-slogan.png new file mode 100644 index 0000000..e0a2ff0 Binary files /dev/null and b/docs/miney-slogan.png differ 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 0000000..3381e9a Binary files /dev/null and b/docs/python-logo.png differ 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")