diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..b504c97 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: M0NsTeRRR \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..a14c640 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,17 @@ +version: 2 +updates: +- package-ecosystem: pip + directory: "/" + schedule: + interval: daily + time: "04:00" + assignees: + - M0NsTeRRR +# Check github actions are up to date. +- package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + time: "04:00" + assignees: + - M0NsTeRRR \ No newline at end of file diff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml new file mode 100644 index 0000000..c1cbade --- /dev/null +++ b/.github/workflows/releases.yml @@ -0,0 +1,25 @@ +name: Python release +on: + push: + tags: + - '*' +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: "3.x" + - name: Install poetry & dependencies + run: | + curl -sSL https://install.python-poetry.org | python3 - + export PATH="$PATH:$HOME/.local/bin" + poetry install + - name: Publish package + run: | + export PATH="$PATH:$HOME/.local/bin" + source `poetry env info --path`/bin/activate + poetry version $(git describe --tags --abbrev=0) + poetry publish --build -u __token__ -p ${{ secrets.PYPI_API_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..1a6eb5f --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,44 @@ +name: Python test +on: + - push + - pull_request +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: + - "3.9" + - "3.10" + - "3.11" + - "3.12" + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install poetry + run: pipx install poetry + + - uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + cache: 'poetry' + + - name: Install poetry dependencies + run: | + export PATH="$PATH:$HOME/.local/bin" + poetry install --with=dev + - name: Run pre-commit + run: | + export PATH="$PATH:$HOME/.local/bin" + source `poetry env info --path`/bin/activate + poetry run black . + - name: Run test + run: | + export PATH="$PATH:$HOME/.local/bin" + source `poetry env info --path`/bin/activate + poetry run pytest --cov diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0a9b105 --- /dev/null +++ b/.gitignore @@ -0,0 +1,160 @@ +# Created by https://www.toptal.com/developers/gitignore/api/python +# Edit at https://www.toptal.com/developers/gitignore?templates=python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +.idea/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d2fdf60 --- /dev/null +++ b/LICENSE @@ -0,0 +1,517 @@ + CeCILL FREE SOFTWARE LICENSE AGREEMENT + +Version 2.1 dated 2013-06-21 + + + Notice + +This Agreement is a Free Software license agreement that is the result +of discussions between its authors in order to ensure compliance with +the two main principles guiding its drafting: + + * firstly, compliance with the principles governing the distribution + of Free Software: access to source code, broad rights granted to users, + * secondly, the election of a governing law, French law, with which it + is conformant, both as regards the law of torts and intellectual + property law, and the protection that it offers to both authors and + holders of the economic rights over software. + +The authors of the CeCILL (for Ce[a] C[nrs] I[nria] L[ogiciel] L[ibre]) +license are: + +Commissariat à l'énergie atomique et aux énergies alternatives - CEA, a +public scientific, technical and industrial research establishment, +having its principal place of business at 25 rue Leblanc, immeuble Le +Ponant D, 75015 Paris, France. + +Centre National de la Recherche Scientifique - CNRS, a public scientific +and technological establishment, having its principal place of business +at 3 rue Michel-Ange, 75794 Paris cedex 16, France. + +Institut National de Recherche en Informatique et en Automatique - +Inria, a public scientific and technological establishment, having its +principal place of business at Domaine de Voluceau, Rocquencourt, BP +105, 78153 Le Chesnay cedex, France. + + + Preamble + +The purpose of this Free Software license agreement is to grant users +the right to modify and redistribute the software governed by this +license within the framework of an open source distribution model. + +The exercising of this right is conditional upon certain obligations for +users so as to preserve this status for all subsequent redistributions. + +In consideration of access to the source code and the rights to copy, +modify and redistribute granted by the license, users are provided only +with a limited warranty and the software's author, the holder of the +economic rights, and the successive licensors only have limited liability. + +In this respect, the risks associated with loading, using, modifying +and/or developing or reproducing the software by the user are brought to +the user's attention, given its Free Software status, which may make it +complicated to use, with the result that its use is reserved for +developers and experienced professionals having in-depth computer +knowledge. Users are therefore encouraged to load and test the +suitability of the software as regards their requirements in conditions +enabling the security of their systems and/or data to be ensured and, +more generally, to use and operate it in the same conditions of +security. This Agreement may be freely reproduced and published, +provided it is not altered, and that no provisions are either added or +removed herefrom. + +This Agreement may apply to any or all software for which the holder of +the economic rights decides to submit the use thereof to its provisions. + +Frequently asked questions can be found on the official website of the +CeCILL licenses family (http://www.cecill.info/index.en.html) for any +necessary clarification. + + + Article 1 - DEFINITIONS + +For the purpose of this Agreement, when the following expressions +commence with a capital letter, they shall have the following meaning: + +Agreement: means this license agreement, and its possible subsequent +versions and annexes. + +Software: means the software in its Object Code and/or Source Code form +and, where applicable, its documentation, "as is" when the Licensee +accepts the Agreement. + +Initial Software: means the Software in its Source Code and possibly its +Object Code form and, where applicable, its documentation, "as is" when +it is first distributed under the terms and conditions of the Agreement. + +Modified Software: means the Software modified by at least one +Contribution. + +Source Code: means all the Software's instructions and program lines to +which access is required so as to modify the Software. + +Object Code: means the binary files originating from the compilation of +the Source Code. + +Holder: means the holder(s) of the economic rights over the Initial +Software. + +Licensee: means the Software user(s) having accepted the Agreement. + +Contributor: means a Licensee having made at least one Contribution. + +Licensor: means the Holder, or any other individual or legal entity, who +distributes the Software under the Agreement. + +Contribution: means any or all modifications, corrections, translations, +adaptations and/or new functions integrated into the Software by any or +all Contributors, as well as any or all Internal Modules. + +Module: means a set of sources files including their documentation that +enables supplementary functions or services in addition to those offered +by the Software. + +External Module: means any or all Modules, not derived from the +Software, so that this Module and the Software run in separate address +spaces, with one calling the other when they are run. + +Internal Module: means any or all Module, connected to the Software so +that they both execute in the same address space. + +GNU GPL: means the GNU General Public License version 2 or any +subsequent version, as published by the Free Software Foundation Inc. + +GNU Affero GPL: means the GNU Affero General Public License version 3 or +any subsequent version, as published by the Free Software Foundation Inc. + +EUPL: means the European Union Public License version 1.1 or any +subsequent version, as published by the European Commission. + +Parties: mean both the Licensee and the Licensor. + +These expressions may be used both in singular and plural form. + + + Article 2 - PURPOSE + +The purpose of the Agreement is the grant by the Licensor to the +Licensee of a non-exclusive, transferable and worldwide license for the +Software as set forth in Article 5 <#scope> hereinafter for the whole +term of the protection granted by the rights over said Software. + + + Article 3 - ACCEPTANCE + +3.1 The Licensee shall be deemed as having accepted the terms and +conditions of this Agreement upon the occurrence of the first of the +following events: + + * (i) loading the Software by any or all means, notably, by + downloading from a remote server, or by loading from a physical medium; + * (ii) the first time the Licensee exercises any of the rights granted + hereunder. + +3.2 One copy of the Agreement, containing a notice relating to the +characteristics of the Software, to the limited warranty, and to the +fact that its use is restricted to experienced users has been provided +to the Licensee prior to its acceptance as set forth in Article 3.1 +<#accepting> hereinabove, and the Licensee hereby acknowledges that it +has read and understood it. + + + Article 4 - EFFECTIVE DATE AND TERM + + + 4.1 EFFECTIVE DATE + +The Agreement shall become effective on the date when it is accepted by +the Licensee as set forth in Article 3.1 <#accepting>. + + + 4.2 TERM + +The Agreement shall remain in force for the entire legal term of +protection of the economic rights over the Software. + + + Article 5 - SCOPE OF RIGHTS GRANTED + +The Licensor hereby grants to the Licensee, who accepts, the following +rights over the Software for any or all use, and for the term of the +Agreement, on the basis of the terms and conditions set forth hereinafter. + +Besides, if the Licensor owns or comes to own one or more patents +protecting all or part of the functions of the Software or of its +components, the Licensor undertakes not to enforce the rights granted by +these patents against successive Licensees using, exploiting or +modifying the Software. If these patents are transferred, the Licensor +undertakes to have the transferees subscribe to the obligations set +forth in this paragraph. + + + 5.1 RIGHT OF USE + +The Licensee is authorized to use the Software, without any limitation +as to its fields of application, with it being hereinafter specified +that this comprises: + + 1. permanent or temporary reproduction of all or part of the Software + by any or all means and in any or all form. + + 2. loading, displaying, running, or storing the Software on any or all + medium. + + 3. entitlement to observe, study or test its operation so as to + determine the ideas and principles behind any or all constituent + elements of said Software. This shall apply when the Licensee + carries out any or all loading, displaying, running, transmission or + storage operation as regards the Software, that it is entitled to + carry out hereunder. + + + 5.2 ENTITLEMENT TO MAKE CONTRIBUTIONS + +The right to make Contributions includes the right to translate, adapt, +arrange, or make any or all modifications to the Software, and the right +to reproduce the resulting software. + +The Licensee is authorized to make any or all Contributions to the +Software provided that it includes an explicit notice that it is the +author of said Contribution and indicates the date of the creation thereof. + + + 5.3 RIGHT OF DISTRIBUTION + +In particular, the right of distribution includes the right to publish, +transmit and communicate the Software to the general public on any or +all medium, and by any or all means, and the right to market, either in +consideration of a fee, or free of charge, one or more copies of the +Software by any means. + +The Licensee is further authorized to distribute copies of the modified +or unmodified Software to third parties according to the terms and +conditions set forth hereinafter. + + + 5.3.1 DISTRIBUTION OF SOFTWARE WITHOUT MODIFICATION + +The Licensee is authorized to distribute true copies of the Software in +Source Code or Object Code form, provided that said distribution +complies with all the provisions of the Agreement and is accompanied by: + + 1. a copy of the Agreement, + + 2. a notice relating to the limitation of both the Licensor's warranty + and liability as set forth in Articles 8 and 9, + +and that, in the event that only the Object Code of the Software is +redistributed, the Licensee allows effective access to the full Source +Code of the Software for a period of at least three years from the +distribution of the Software, it being understood that the additional +acquisition cost of the Source Code shall not exceed the cost of the +data transfer. + + + 5.3.2 DISTRIBUTION OF MODIFIED SOFTWARE + +When the Licensee makes a Contribution to the Software, the terms and +conditions for the distribution of the resulting Modified Software +become subject to all the provisions of this Agreement. + +The Licensee is authorized to distribute the Modified Software, in +source code or object code form, provided that said distribution +complies with all the provisions of the Agreement and is accompanied by: + + 1. a copy of the Agreement, + + 2. a notice relating to the limitation of both the Licensor's warranty + and liability as set forth in Articles 8 and 9, + +and, in the event that only the object code of the Modified Software is +redistributed, + + 3. a note stating the conditions of effective access to the full source + code of the Modified Software for a period of at least three years + from the distribution of the Modified Software, it being understood + that the additional acquisition cost of the source code shall not + exceed the cost of the data transfer. + + + 5.3.3 DISTRIBUTION OF EXTERNAL MODULES + +When the Licensee has developed an External Module, the terms and +conditions of this Agreement do not apply to said External Module, that +may be distributed under a separate license agreement. + + + 5.3.4 COMPATIBILITY WITH OTHER LICENSES + +The Licensee can include a code that is subject to the provisions of one +of the versions of the GNU GPL, GNU Affero GPL and/or EUPL in the +Modified or unmodified Software, and distribute that entire code under +the terms of the same version of the GNU GPL, GNU Affero GPL and/or EUPL. + +The Licensee can include the Modified or unmodified Software in a code +that is subject to the provisions of one of the versions of the GNU GPL, +GNU Affero GPL and/or EUPL and distribute that entire code under the +terms of the same version of the GNU GPL, GNU Affero GPL and/or EUPL. + + + Article 6 - INTELLECTUAL PROPERTY + + + 6.1 OVER THE INITIAL SOFTWARE + +The Holder owns the economic rights over the Initial Software. Any or +all use of the Initial Software is subject to compliance with the terms +and conditions under which the Holder has elected to distribute its work +and no one shall be entitled to modify the terms and conditions for the +distribution of said Initial Software. + +The Holder undertakes that the Initial Software will remain ruled at +least by this Agreement, for the duration set forth in Article 4.2 <#term>. + + + 6.2 OVER THE CONTRIBUTIONS + +The Licensee who develops a Contribution is the owner of the +intellectual property rights over this Contribution as defined by +applicable law. + + + 6.3 OVER THE EXTERNAL MODULES + +The Licensee who develops an External Module is the owner of the +intellectual property rights over this External Module as defined by +applicable law and is free to choose the type of agreement that shall +govern its distribution. + + + 6.4 JOINT PROVISIONS + +The Licensee expressly undertakes: + + 1. not to remove, or modify, in any manner, the intellectual property + notices attached to the Software; + + 2. to reproduce said notices, in an identical manner, in the copies of + the Software modified or not. + +The Licensee undertakes not to directly or indirectly infringe the +intellectual property rights on the Software of the Holder and/or +Contributors, and to take, where applicable, vis-à-vis its staff, any +and all measures required to ensure respect of said intellectual +property rights of the Holder and/or Contributors. + + + Article 7 - RELATED SERVICES + +7.1 Under no circumstances shall the Agreement oblige the Licensor to +provide technical assistance or maintenance services for the Software. + +However, the Licensor is entitled to offer this type of services. The +terms and conditions of such technical assistance, and/or such +maintenance, shall be set forth in a separate instrument. Only the +Licensor offering said maintenance and/or technical assistance services +shall incur liability therefor. + +7.2 Similarly, any Licensor is entitled to offer to its licensees, under +its sole responsibility, a warranty, that shall only be binding upon +itself, for the redistribution of the Software and/or the Modified +Software, under terms and conditions that it is free to decide. Said +warranty, and the financial terms and conditions of its application, +shall be subject of a separate instrument executed between the Licensor +and the Licensee. + + + Article 8 - LIABILITY + +8.1 Subject to the provisions of Article 8.2, the Licensee shall be +entitled to claim compensation for any direct loss it may have suffered +from the Software as a result of a fault on the part of the relevant +Licensor, subject to providing evidence thereof. + +8.2 The Licensor's liability is limited to the commitments made under +this Agreement and shall not be incurred as a result of in particular: +(i) loss due the Licensee's total or partial failure to fulfill its +obligations, (ii) direct or consequential loss that is suffered by the +Licensee due to the use or performance of the Software, and (iii) more +generally, any consequential loss. In particular the Parties expressly +agree that any or all pecuniary or business loss (i.e. loss of data, +loss of profits, operating loss, loss of customers or orders, +opportunity cost, any disturbance to business activities) or any or all +legal proceedings instituted against the Licensee by a third party, +shall constitute consequential loss and shall not provide entitlement to +any or all compensation from the Licensor. + + + Article 9 - WARRANTY + +9.1 The Licensee acknowledges that the scientific and technical +state-of-the-art when the Software was distributed did not enable all +possible uses to be tested and verified, nor for the presence of +possible defects to be detected. In this respect, the Licensee's +attention has been drawn to the risks associated with loading, using, +modifying and/or developing and reproducing the Software which are +reserved for experienced users. + +The Licensee shall be responsible for verifying, by any or all means, +the suitability of the product for its requirements, its good working +order, and for ensuring that it shall not cause damage to either persons +or properties. + +9.2 The Licensor hereby represents, in good faith, that it is entitled +to grant all the rights over the Software (including in particular the +rights set forth in Article 5 <#scope>). + +9.3 The Licensee acknowledges that the Software is supplied "as is" by +the Licensor without any other express or tacit warranty, other than +that provided for in Article 9.2 <#good-faith> and, in particular, +without any warranty as to its commercial value, its secured, safe, +innovative or relevant nature. + +Specifically, the Licensor does not warrant that the Software is free +from any error, that it will operate without interruption, that it will +be compatible with the Licensee's own equipment and software +configuration, nor that it will meet the Licensee's requirements. + +9.4 The Licensor does not either expressly or tacitly warrant that the +Software does not infringe any third party intellectual property right +relating to a patent, software or any other property right. Therefore, +the Licensor disclaims any and all liability towards the Licensee +arising out of any or all proceedings for infringement that may be +instituted in respect of the use, modification and redistribution of the +Software. Nevertheless, should such proceedings be instituted against +the Licensee, the Licensor shall provide it with technical and legal +expertise for its defense. Such technical and legal expertise shall be +decided on a case-by-case basis between the relevant Licensor and the +Licensee pursuant to a memorandum of understanding. The Licensor +disclaims any and all liability as regards the Licensee's use of the +name of the Software. No warranty is given as regards the existence of +prior rights over the name of the Software or as regards the existence +of a trademark. + + + Article 10 - TERMINATION + +10.1 In the event of a breach by the Licensee of its obligations +hereunder, the Licensor may automatically terminate this Agreement +thirty (30) days after notice has been sent to the Licensee and has +remained ineffective. + +10.2 A Licensee whose Agreement is terminated shall no longer be +authorized to use, modify or distribute the Software. However, any +licenses that it may have granted prior to termination of the Agreement +shall remain valid subject to their having been granted in compliance +with the terms and conditions hereof. + + + Article 11 - MISCELLANEOUS + + + 11.1 EXCUSABLE EVENTS + +Neither Party shall be liable for any or all delay, or failure to +perform the Agreement, that may be attributable to an event of force +majeure, an act of God or an outside cause, such as defective +functioning or interruptions of the electricity or telecommunications +networks, network paralysis following a virus attack, intervention by +government authorities, natural disasters, water damage, earthquakes, +fire, explosions, strikes and labor unrest, war, etc. + +11.2 Any failure by either Party, on one or more occasions, to invoke +one or more of the provisions hereof, shall under no circumstances be +interpreted as being a waiver by the interested Party of its right to +invoke said provision(s) subsequently. + +11.3 The Agreement cancels and replaces any or all previous agreements, +whether written or oral, between the Parties and having the same +purpose, and constitutes the entirety of the agreement between said +Parties concerning said purpose. No supplement or modification to the +terms and conditions hereof shall be effective as between the Parties +unless it is made in writing and signed by their duly authorized +representatives. + +11.4 In the event that one or more of the provisions hereof were to +conflict with a current or future applicable act or legislative text, +said act or legislative text shall prevail, and the Parties shall make +the necessary amendments so as to comply with said act or legislative +text. All other provisions shall remain effective. Similarly, invalidity +of a provision of the Agreement, for any reason whatsoever, shall not +cause the Agreement as a whole to be invalid. + + + 11.5 LANGUAGE + +The Agreement is drafted in both French and English and both versions +are deemed authentic. + + + Article 12 - NEW VERSIONS OF THE AGREEMENT + +12.1 Any person is authorized to duplicate and distribute copies of this +Agreement. + +12.2 So as to ensure coherence, the wording of this Agreement is +protected and may only be modified by the authors of the License, who +reserve the right to periodically publish updates or new versions of the +Agreement, each with a separate number. These subsequent versions may +address new issues encountered by Free Software. + +12.3 Any Software distributed under a given version of the Agreement may +only be subsequently distributed under the same version of the Agreement +or a subsequent version, subject to the provisions of Article 5.3.4 +<#compatibility>. + + + Article 13 - GOVERNING LAW AND JURISDICTION + +13.1 The Agreement is governed by French law. The Parties agree to +endeavor to seek an amicable solution to any disagreements or disputes +that may arise during the performance of the Agreement. + +13.2 Failing an amicable solution within two (2) months as from their +occurrence, and unless emergency proceedings are necessary, the +disagreements or disputes shall be referred to the Paris Courts having +jurisdiction, by the more diligent Party. diff --git a/LICENSE.fr b/LICENSE.fr new file mode 100644 index 0000000..64e1d71 --- /dev/null +++ b/LICENSE.fr @@ -0,0 +1,547 @@ + CONTRAT DE LICENCE DE LOGICIEL LIBRE CeCILL + +Version 2.1 du 2013-06-21 + + + Avertissement + +Ce contrat est une licence de logiciel libre issue d'une concertation +entre ses auteurs afin que le respect de deux grands principes préside à +sa rédaction: + + * d'une part, le respect des principes de diffusion des logiciels + libres: accès au code source, droits étendus conférés aux utilisateurs, + * d'autre part, la désignation d'un droit applicable, le droit + français, auquel elle est conforme, tant au regard du droit de la + responsabilité civile que du droit de la propriété intellectuelle et + de la protection qu'il offre aux auteurs et titulaires des droits + patrimoniaux sur un logiciel. + +Les auteurs de la licence CeCILL (Ce[a] C[nrs] I[nria] L[ogiciel] L[ibre]) +sont: + +Commissariat à l'énergie atomique et aux énergies alternatives - CEA, +établissement public de recherche à caractère scientifique, technique et +industriel, dont le siège est situé 25 rue Leblanc, immeuble Le Ponant +D, 75015 Paris. + +Centre National de la Recherche Scientifique - CNRS, établissement +public à caractère scientifique et technologique, dont le siège est +situé 3 rue Michel-Ange, 75794 Paris cedex 16. + +Institut National de Recherche en Informatique et en Automatique - +Inria, établissement public à caractère scientifique et technologique, +dont le siège est situé Domaine de Voluceau, Rocquencourt, BP 105, 78153 +Le Chesnay cedex. + + + Préambule + +Ce contrat est une licence de logiciel libre dont l'objectif est de +conférer aux utilisateurs la liberté de modification et de +redistribution du logiciel régi par cette licence dans le cadre d'un +modèle de diffusion en logiciel libre. + +L'exercice de ces libertés est assorti de certains devoirs à la charge +des utilisateurs afin de préserver ce statut au cours des +redistributions ultérieures. + +L'accessibilité au code source et les droits de copie, de modification +et de redistribution qui en découlent ont pour contrepartie de n'offrir +aux utilisateurs qu'une garantie limitée et de ne faire peser sur +l'auteur du logiciel, le titulaire des droits patrimoniaux et les +concédants successifs qu'une responsabilité restreinte. + +A cet égard l'attention de l'utilisateur est attirée sur les risques +associés au chargement, à l'utilisation, à la modification et/ou au +développement et à la reproduction du logiciel par l'utilisateur étant +donné sa spécificité de logiciel libre, qui peut le rendre complexe à +manipuler et qui le réserve donc à des développeurs ou des +professionnels avertis possédant des connaissances informatiques +approfondies. Les utilisateurs sont donc invités à charger et tester +l'adéquation du logiciel à leurs besoins dans des conditions permettant +d'assurer la sécurité de leurs systèmes et/ou de leurs données et, plus +généralement, à l'utiliser et l'exploiter dans les mêmes conditions de +sécurité. Ce contrat peut être reproduit et diffusé librement, sous +réserve de le conserver en l'état, sans ajout ni suppression de clauses. + +Ce contrat est susceptible de s'appliquer à tout logiciel dont le +titulaire des droits patrimoniaux décide de soumettre l'exploitation aux +dispositions qu'il contient. + +Une liste de questions fréquemment posées se trouve sur le site web +officiel de la famille des licences CeCILL +(http://www.cecill.info/index.fr.html) pour toute clarification qui +serait nécessaire. + + + Article 1 - DEFINITIONS + +Dans ce contrat, les termes suivants, lorsqu'ils seront écrits avec une +lettre capitale, auront la signification suivante: + +Contrat: désigne le présent contrat de licence, ses éventuelles versions +postérieures et annexes. + +Logiciel: désigne le logiciel sous sa forme de Code Objet et/ou de Code +Source et le cas échéant sa documentation, dans leur état au moment de +l'acceptation du Contrat par le Licencié. + +Logiciel Initial: désigne le Logiciel sous sa forme de Code Source et +éventuellement de Code Objet et le cas échéant sa documentation, dans +leur état au moment de leur première diffusion sous les termes du Contrat. + +Logiciel Modifié: désigne le Logiciel modifié par au moins une +Contribution. + +Code Source: désigne l'ensemble des instructions et des lignes de +programme du Logiciel et auquel l'accès est nécessaire en vue de +modifier le Logiciel. + +Code Objet: désigne les fichiers binaires issus de la compilation du +Code Source. + +Titulaire: désigne le ou les détenteurs des droits patrimoniaux d'auteur +sur le Logiciel Initial. + +Licencié: désigne le ou les utilisateurs du Logiciel ayant accepté le +Contrat. + +Contributeur: désigne le Licencié auteur d'au moins une Contribution. + +Concédant: désigne le Titulaire ou toute personne physique ou morale +distribuant le Logiciel sous le Contrat. + +Contribution: désigne l'ensemble des modifications, corrections, +traductions, adaptations et/ou nouvelles fonctionnalités intégrées dans +le Logiciel par tout Contributeur, ainsi que tout Module Interne. + +Module: désigne un ensemble de fichiers sources y compris leur +documentation qui permet de réaliser des fonctionnalités ou services +supplémentaires à ceux fournis par le Logiciel. + +Module Externe: désigne tout Module, non dérivé du Logiciel, tel que ce +Module et le Logiciel s'exécutent dans des espaces d'adressage +différents, l'un appelant l'autre au moment de leur exécution. + +Module Interne: désigne tout Module lié au Logiciel de telle sorte +qu'ils s'exécutent dans le même espace d'adressage. + +GNU GPL: désigne la GNU General Public License dans sa version 2 ou +toute version ultérieure, telle que publiée par Free Software Foundation +Inc. + +GNU Affero GPL: désigne la GNU Affero General Public License dans sa +version 3 ou toute version ultérieure, telle que publiée par Free +Software Foundation Inc. + +EUPL: désigne la Licence Publique de l'Union européenne dans sa version +1.1 ou toute version ultérieure, telle que publiée par la Commission +Européenne. + +Parties: désigne collectivement le Licencié et le Concédant. + +Ces termes s'entendent au singulier comme au pluriel. + + + Article 2 - OBJET + +Le Contrat a pour objet la concession par le Concédant au Licencié d'une +licence non exclusive, cessible et mondiale du Logiciel telle que +définie ci-après à l'article 5 <#etendue> pour toute la durée de +protection des droits portant sur ce Logiciel. + + + Article 3 - ACCEPTATION + +3.1 L'acceptation par le Licencié des termes du Contrat est réputée +acquise du fait du premier des faits suivants: + + * (i) le chargement du Logiciel par tout moyen notamment par + téléchargement à partir d'un serveur distant ou par chargement à + partir d'un support physique; + * (ii) le premier exercice par le Licencié de l'un quelconque des + droits concédés par le Contrat. + +3.2 Un exemplaire du Contrat, contenant notamment un avertissement +relatif aux spécificités du Logiciel, à la restriction de garantie et à +la limitation à un usage par des utilisateurs expérimentés a été mis à +disposition du Licencié préalablement à son acceptation telle que +définie à l'article 3.1 <#acceptation-acquise> ci dessus et le Licencié +reconnaît en avoir pris connaissance. + + + Article 4 - ENTREE EN VIGUEUR ET DUREE + + + 4.1 ENTREE EN VIGUEUR + +Le Contrat entre en vigueur à la date de son acceptation par le Licencié +telle que définie en 3.1 <#acceptation-acquise>. + + + 4.2 DUREE + +Le Contrat produira ses effets pendant toute la durée légale de +protection des droits patrimoniaux portant sur le Logiciel. + + + Article 5 - ETENDUE DES DROITS CONCEDES + +Le Concédant concède au Licencié, qui accepte, les droits suivants sur +le Logiciel pour toutes destinations et pour la durée du Contrat dans +les conditions ci-après détaillées. + +Par ailleurs, si le Concédant détient ou venait à détenir un ou +plusieurs brevets d'invention protégeant tout ou partie des +fonctionnalités du Logiciel ou de ses composants, il s'engage à ne pas +opposer les éventuels droits conférés par ces brevets aux Licenciés +successifs qui utiliseraient, exploiteraient ou modifieraient le +Logiciel. En cas de cession de ces brevets, le Concédant s'engage à +faire reprendre les obligations du présent alinéa aux cessionnaires. + + + 5.1 DROIT D'UTILISATION + +Le Licencié est autorisé à utiliser le Logiciel, sans restriction quant +aux domaines d'application, étant ci-après précisé que cela comporte: + + 1. + + la reproduction permanente ou provisoire du Logiciel en tout ou + partie par tout moyen et sous toute forme. + + 2. + + le chargement, l'affichage, l'exécution, ou le stockage du Logiciel + sur tout support. + + 3. + + la possibilité d'en observer, d'en étudier, ou d'en tester le + fonctionnement afin de déterminer les idées et principes qui sont à + la base de n'importe quel élément de ce Logiciel; et ceci, lorsque + le Licencié effectue toute opération de chargement, d'affichage, + d'exécution, de transmission ou de stockage du Logiciel qu'il est en + droit d'effectuer en vertu du Contrat. + + + 5.2 DROIT D'APPORTER DES CONTRIBUTIONS + +Le droit d'apporter des Contributions comporte le droit de traduire, +d'adapter, d'arranger ou d'apporter toute autre modification au Logiciel +et le droit de reproduire le logiciel en résultant. + +Le Licencié est autorisé à apporter toute Contribution au Logiciel sous +réserve de mentionner, de façon explicite, son nom en tant qu'auteur de +cette Contribution et la date de création de celle-ci. + + + 5.3 DROIT DE DISTRIBUTION + +Le droit de distribution comporte notamment le droit de diffuser, de +transmettre et de communiquer le Logiciel au public sur tout support et +par tout moyen ainsi que le droit de mettre sur le marché à titre +onéreux ou gratuit, un ou des exemplaires du Logiciel par tout procédé. + +Le Licencié est autorisé à distribuer des copies du Logiciel, modifié ou +non, à des tiers dans les conditions ci-après détaillées. + + + 5.3.1 DISTRIBUTION DU LOGICIEL SANS MODIFICATION + +Le Licencié est autorisé à distribuer des copies conformes du Logiciel, +sous forme de Code Source ou de Code Objet, à condition que cette +distribution respecte les dispositions du Contrat dans leur totalité et +soit accompagnée: + + 1. + + d'un exemplaire du Contrat, + + 2. + + d'un avertissement relatif à la restriction de garantie et de + responsabilité du Concédant telle que prévue aux articles 8 + <#responsabilite> et 9 <#garantie>, + +et que, dans le cas où seul le Code Objet du Logiciel est redistribué, +le Licencié permette un accès effectif au Code Source complet du +Logiciel pour une durée d'au moins 3 ans à compter de la distribution du +logiciel, étant entendu que le coût additionnel d'acquisition du Code +Source ne devra pas excéder le simple coût de transfert des données. + + + 5.3.2 DISTRIBUTION DU LOGICIEL MODIFIE + +Lorsque le Licencié apporte une Contribution au Logiciel, les conditions +de distribution du Logiciel Modifié en résultant sont alors soumises à +l'intégralité des dispositions du Contrat. + +Le Licencié est autorisé à distribuer le Logiciel Modifié, sous forme de +code source ou de code objet, à condition que cette distribution +respecte les dispositions du Contrat dans leur totalité et soit +accompagnée: + + 1. + + d'un exemplaire du Contrat, + + 2. + + d'un avertissement relatif à la restriction de garantie et de + responsabilité du Concédant telle que prévue aux articles 8 + <#responsabilite> et 9 <#garantie>, + +et, dans le cas où seul le code objet du Logiciel Modifié est redistribué, + + 3. + + d'une note précisant les conditions d'accès effectif au code source + complet du Logiciel Modifié, pendant une période d'au moins 3 ans à + compter de la distribution du Logiciel Modifié, étant entendu que le + coût additionnel d'acquisition du code source ne devra pas excéder + le simple coût de transfert des données. + + + 5.3.3 DISTRIBUTION DES MODULES EXTERNES + +Lorsque le Licencié a développé un Module Externe les conditions du +Contrat ne s'appliquent pas à ce Module Externe, qui peut être distribué +sous un contrat de licence différent. + + + 5.3.4 COMPATIBILITE AVEC D'AUTRES LICENCES + +Le Licencié peut inclure un code soumis aux dispositions d'une des +versions de la licence GNU GPL, GNU Affero GPL et/ou EUPL dans le +Logiciel modifié ou non et distribuer l'ensemble sous les conditions de +la même version de la licence GNU GPL, GNU Affero GPL et/ou EUPL. + +Le Licencié peut inclure le Logiciel modifié ou non dans un code soumis +aux dispositions d'une des versions de la licence GNU GPL, GNU Affero +GPL et/ou EUPL et distribuer l'ensemble sous les conditions de la même +version de la licence GNU GPL, GNU Affero GPL et/ou EUPL. + + + Article 6 - PROPRIETE INTELLECTUELLE + + + 6.1 SUR LE LOGICIEL INITIAL + +Le Titulaire est détenteur des droits patrimoniaux sur le Logiciel +Initial. Toute utilisation du Logiciel Initial est soumise au respect +des conditions dans lesquelles le Titulaire a choisi de diffuser son +oeuvre et nul autre n'a la faculté de modifier les conditions de +diffusion de ce Logiciel Initial. + +Le Titulaire s'engage à ce que le Logiciel Initial reste au moins régi +par le Contrat et ce, pour la durée visée à l'article 4.2 <#duree>. + + + 6.2 SUR LES CONTRIBUTIONS + +Le Licencié qui a développé une Contribution est titulaire sur celle-ci +des droits de propriété intellectuelle dans les conditions définies par +la législation applicable. + + + 6.3 SUR LES MODULES EXTERNES + +Le Licencié qui a développé un Module Externe est titulaire sur celui-ci +des droits de propriété intellectuelle dans les conditions définies par +la législation applicable et reste libre du choix du contrat régissant +sa diffusion. + + + 6.4 DISPOSITIONS COMMUNES + +Le Licencié s'engage expressément: + + 1. + + à ne pas supprimer ou modifier de quelque manière que ce soit les + mentions de propriété intellectuelle apposées sur le Logiciel; + + 2. + + à reproduire à l'identique lesdites mentions de propriété + intellectuelle sur les copies du Logiciel modifié ou non. + +Le Licencié s'engage à ne pas porter atteinte, directement ou +indirectement, aux droits de propriété intellectuelle du Titulaire et/ou +des Contributeurs sur le Logiciel et à prendre, le cas échéant, à +l'égard de son personnel toutes les mesures nécessaires pour assurer le +respect des dits droits de propriété intellectuelle du Titulaire et/ou +des Contributeurs. + + + Article 7 - SERVICES ASSOCIES + +7.1 Le Contrat n'oblige en aucun cas le Concédant à la réalisation de +prestations d'assistance technique ou de maintenance du Logiciel. + +Cependant le Concédant reste libre de proposer ce type de services. Les +termes et conditions d'une telle assistance technique et/ou d'une telle +maintenance seront alors déterminés dans un acte séparé. Ces actes de +maintenance et/ou assistance technique n'engageront que la seule +responsabilité du Concédant qui les propose. + +7.2 De même, tout Concédant est libre de proposer, sous sa seule +responsabilité, à ses licenciés une garantie, qui n'engagera que lui, +lors de la redistribution du Logiciel et/ou du Logiciel Modifié et ce, +dans les conditions qu'il souhaite. Cette garantie et les modalités +financières de son application feront l'objet d'un acte séparé entre le +Concédant et le Licencié. + + + Article 8 - RESPONSABILITE + +8.1 Sous réserve des dispositions de l'article 8.2 +<#limite-responsabilite>, le Licencié a la faculté, sous réserve de +prouver la faute du Concédant concerné, de solliciter la réparation du +préjudice direct qu'il subirait du fait du Logiciel et dont il apportera +la preuve. + +8.2 La responsabilité du Concédant est limitée aux engagements pris en +application du Contrat et ne saurait être engagée en raison notamment: +(i) des dommages dus à l'inexécution, totale ou partielle, de ses +obligations par le Licencié, (ii) des dommages directs ou indirects +découlant de l'utilisation ou des performances du Logiciel subis par le +Licencié et (iii) plus généralement d'un quelconque dommage indirect. En +particulier, les Parties conviennent expressément que tout préjudice +financier ou commercial (par exemple perte de données, perte de +bénéfices, perte d'exploitation, perte de clientèle ou de commandes, +manque à gagner, trouble commercial quelconque) ou toute action dirigée +contre le Licencié par un tiers, constitue un dommage indirect et +n'ouvre pas droit à réparation par le Concédant. + + + Article 9 - GARANTIE + +9.1 Le Licencié reconnaît que l'état actuel des connaissances +scientifiques et techniques au moment de la mise en circulation du +Logiciel ne permet pas d'en tester et d'en vérifier toutes les +utilisations ni de détecter l'existence d'éventuels défauts. L'attention +du Licencié a été attirée sur ce point sur les risques associés au +chargement, à l'utilisation, la modification et/ou au développement et à +la reproduction du Logiciel qui sont réservés à des utilisateurs avertis. + +Il relève de la responsabilité du Licencié de contrôler, par tous +moyens, l'adéquation du produit à ses besoins, son bon fonctionnement et +de s'assurer qu'il ne causera pas de dommages aux personnes et aux biens. + +9.2 Le Concédant déclare de bonne foi être en droit de concéder +l'ensemble des droits attachés au Logiciel (comprenant notamment les +droits visés à l'article 5 <#etendue>). + +9.3 Le Licencié reconnaît que le Logiciel est fourni "en l'état" par le +Concédant sans autre garantie, expresse ou tacite, que celle prévue à +l'article 9.2 <#bonne-foi> et notamment sans aucune garantie sur sa +valeur commerciale, son caractère sécurisé, innovant ou pertinent. + +En particulier, le Concédant ne garantit pas que le Logiciel est exempt +d'erreur, qu'il fonctionnera sans interruption, qu'il sera compatible +avec l'équipement du Licencié et sa configuration logicielle ni qu'il +remplira les besoins du Licencié. + +9.4 Le Concédant ne garantit pas, de manière expresse ou tacite, que le +Logiciel ne porte pas atteinte à un quelconque droit de propriété +intellectuelle d'un tiers portant sur un brevet, un logiciel ou sur tout +autre droit de propriété. Ainsi, le Concédant exclut toute garantie au +profit du Licencié contre les actions en contrefaçon qui pourraient être +diligentées au titre de l'utilisation, de la modification, et de la +redistribution du Logiciel. Néanmoins, si de telles actions sont +exercées contre le Licencié, le Concédant lui apportera son expertise +technique et juridique pour sa défense. Cette expertise technique et +juridique est déterminée au cas par cas entre le Concédant concerné et +le Licencié dans le cadre d'un protocole d'accord. Le Concédant dégage +toute responsabilité quant à l'utilisation de la dénomination du +Logiciel par le Licencié. Aucune garantie n'est apportée quant à +l'existence de droits antérieurs sur le nom du Logiciel et sur +l'existence d'une marque. + + + Article 10 - RESILIATION + +10.1 En cas de manquement par le Licencié aux obligations mises à sa +charge par le Contrat, le Concédant pourra résilier de plein droit le +Contrat trente (30) jours après notification adressée au Licencié et +restée sans effet. + +10.2 Le Licencié dont le Contrat est résilié n'est plus autorisé à +utiliser, modifier ou distribuer le Logiciel. Cependant, toutes les +licences qu'il aura concédées antérieurement à la résiliation du Contrat +resteront valides sous réserve qu'elles aient été effectuées en +conformité avec le Contrat. + + + Article 11 - DISPOSITIONS DIVERSES + + + 11.1 CAUSE EXTERIEURE + +Aucune des Parties ne sera responsable d'un retard ou d'une défaillance +d'exécution du Contrat qui serait dû à un cas de force majeure, un cas +fortuit ou une cause extérieure, telle que, notamment, le mauvais +fonctionnement ou les interruptions du réseau électrique ou de +télécommunication, la paralysie du réseau liée à une attaque +informatique, l'intervention des autorités gouvernementales, les +catastrophes naturelles, les dégâts des eaux, les tremblements de terre, +le feu, les explosions, les grèves et les conflits sociaux, l'état de +guerre... + +11.2 Le fait, par l'une ou l'autre des Parties, d'omettre en une ou +plusieurs occasions de se prévaloir d'une ou plusieurs dispositions du +Contrat, ne pourra en aucun cas impliquer renonciation par la Partie +intéressée à s'en prévaloir ultérieurement. + +11.3 Le Contrat annule et remplace toute convention antérieure, écrite +ou orale, entre les Parties sur le même objet et constitue l'accord +entier entre les Parties sur cet objet. Aucune addition ou modification +aux termes du Contrat n'aura d'effet à l'égard des Parties à moins +d'être faite par écrit et signée par leurs représentants dûment habilités. + +11.4 Dans l'hypothèse où une ou plusieurs des dispositions du Contrat +s'avèrerait contraire à une loi ou à un texte applicable, existants ou +futurs, cette loi ou ce texte prévaudrait, et les Parties feraient les +amendements nécessaires pour se conformer à cette loi ou à ce texte. +Toutes les autres dispositions resteront en vigueur. De même, la +nullité, pour quelque raison que ce soit, d'une des dispositions du +Contrat ne saurait entraîner la nullité de l'ensemble du Contrat. + + + 11.5 LANGUE + +Le Contrat est rédigé en langue française et en langue anglaise, ces +deux versions faisant également foi. + + + Article 12 - NOUVELLES VERSIONS DU CONTRAT + +12.1 Toute personne est autorisée à copier et distribuer des copies de +ce Contrat. + +12.2 Afin d'en préserver la cohérence, le texte du Contrat est protégé +et ne peut être modifié que par les auteurs de la licence, lesquels se +réservent le droit de publier périodiquement des mises à jour ou de +nouvelles versions du Contrat, qui posséderont chacune un numéro +distinct. Ces versions ultérieures seront susceptibles de prendre en +compte de nouvelles problématiques rencontrées par les logiciels libres. + +12.3 Tout Logiciel diffusé sous une version donnée du Contrat ne pourra +faire l'objet d'une diffusion ultérieure que sous la même version du +Contrat ou une version postérieure, sous réserve des dispositions de +l'article 5.3.4 <#compatibilite>. + + + Article 13 - LOI APPLICABLE ET COMPETENCE TERRITORIALE + +13.1 Le Contrat est régi par la loi française. Les Parties conviennent +de tenter de régler à l'amiable les différends ou litiges qui +viendraient à se produire par suite ou à l'occasion du Contrat. + +13.2 A défaut d'accord amiable dans un délai de deux (2) mois à compter +de leur survenance et sauf situation relevant d'une procédure d'urgence, +les différends ou litiges seront portés par la Partie la plus diligente +devant les Tribunaux compétents de Paris. diff --git a/README.md b/README.md new file mode 100644 index 0000000..61d12bc --- /dev/null +++ b/README.md @@ -0,0 +1,85 @@ +Python async HTTP client for [awtrix-light](https://github.com/Blueforcer/awtrix-light) + +[![PyPI](https://img.shields.io/pypi/v/awtrix-light-client.svg)](https://pypi.python.org/pypi/awtrix-light-client) +[![PyPI versions](https://img.shields.io/pypi/pyversions/awtrix-light-client.svg)](https://pypi.python.org/pypi/awtrix-light-client) +[![Python test](https://github.com/napalm-automation-community/awtrix-light-client/actions/workflows/test.yml/badge.svg)](https://github.com/napalm-automation-community/awtrix-light-client/actions/workflows/test.yml) +[![Code Style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) + +# Warning +This client has been tested with awtrix-light v0.90 use with caution as official dev documentation is not fully documented and can cause crash + +# Install +``` +pip install awtrix-light-client +``` + +# Dev +Install [Poetry](https://python-poetry.org/docs/master/#installing-with-the-official-installer) + +Install and setup dependencies +``` +poetry install +poetry shell +``` + +# Usage +Official project documentation : https://blueforcer.github.io/awtrix-light/#/api + +Available environment variables +``` +AWTRIX_HTTP_CLIENT_AWTRIX="" +``` + +`` is in JSON and looks like this : +```json +{ + "base_url": "http://192.168.0.1", + "username": "admin", + "password": "password", + "verify_ssl": false, +} +``` +`verify_ssl` used to verify https config (if accessing behind an HTTPS reverse proxy), can be `true`, `false`, or can point to a local ca bundle PEM encoded to validate local CA + +Example script +```py +import asyncio + +from awtrix_light_client.http_client import get_awtrix_http_client, AwtrixLightHttpClientError + + +async def main(): + try: + async with get_awtrix_http_client() as client: + stats = await client.get_stats() + print(stats) + except AwtrixLightHttpClientError as e: + print(f"HTTP code: {e.status_code}, error content: {e.content}") + + +asyncio.run(main()) +``` + +### Run unit test +``` +poetry run pytest --cov +``` + +### Run black +``` +poetry run black . +``` + +# Licence + +The code is under CeCILL license. + +You can find all details here: https://cecill.info/licences/Licence_CeCILL_V2.1-en.html + +# Credits + +Copyright © Ludovic Ortega, 2023 + +Contributor(s): + +-Ortega Ludovic - ludovic.ortega@adminafk.fr \ No newline at end of file diff --git a/awtrix_light_client/__init__.py b/awtrix_light_client/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/awtrix_light_client/http_client.py b/awtrix_light_client/http_client.py new file mode 100644 index 0000000..71c75ed --- /dev/null +++ b/awtrix_light_client/http_client.py @@ -0,0 +1,267 @@ +from typing import Dict, Any, List, Union, Literal +from contextlib import asynccontextmanager +from typing import AsyncIterator + +from httpx import AsyncClient +from pydantic_extra_types.color import Color + +from .http_settings import AwtrixLightHttpClientSettings +from .models.stat import Stats +from .models.effect import EffectType +from .models.transition import TransitionType +from .models.loop import Loop +from .models.screen import Screen +from .models.moodlight import Moodlight +from .models.application import CustomApplication, Notification +from .models.setting import Settings + +from .models.utils import as_hex + + +class AwtrixLightHttpClientError(BaseException): + def __init__(self, status_code: int, content: str, *args: object) -> None: + super().__init__(*args) + self.status_code = status_code + self.content = content + + +class AwtrixLightHttpClient: + def __init__(self, client: AsyncClient) -> None: + self._client = client + + async def _make_request( + self, + method: str, + url: str, + params: Dict[Any, Any] = None, + data: Dict[Any, Any] = None, + json_data: Dict[Any, Any] = None, + ): + r = await self._client.request( + method, url, params=params, data=data, json=json_data + ) + + if not r.is_success: + raise AwtrixLightHttpClientError( + status_code=r.status_code, content=r.content + ) + return r + + async def get_stats(self) -> Stats: + """ + General device stats (e.g., battery, RAM) + """ + response = (await self._make_request("GET", "stats")).json() + + return Stats(**response) + + async def get_effects(self) -> List[EffectType]: + """ + List of all effects + """ + response = (await self._make_request("GET", "effects")).json() + + return [EffectType(e) for e in response] + + async def get_transitions(self) -> List[TransitionType]: + """ + List of all transition effects + """ + response = (await self._make_request("GET", "transitions")).json() + + return [(TransitionType[t.upper()]) for t in response] + + async def get_loops(self) -> Loop: + """ + List of all apps in the loop + """ + response = (await self._make_request("GET", "loop")).json() + + sorted_apps = dict(sorted(response.items(), key=lambda item: item[1])) + + return Loop(loops=[app for app in sorted_apps.keys()]) + + async def get_screen(self) -> Screen: + """ + Retrieve the current matrix screen as an array of 24 bit colors + """ + response = (await self._make_request("GET", "screen")).json() + + return Screen(matrix=response) + + async def set_power(self, power: bool) -> None: + """ + Toggle the matrix on or off + """ + await self._make_request("POST", "power", json_data={"power": power}) + + async def set_sleep(self, seconds: int) -> None: + """ + Send the board in deep sleep mode (turns off the matrix as well), good for saving battery life + """ + await self._make_request("POST", "sleep", json_data={"sleep": seconds}) + + async def set_sound(self, sound: str) -> None: + """ + Play a RTTTL sound from the MELODIES folder + """ + await self._make_request("POST", "sound", json_data={"sound": sound}) + + async def set_rtttl(self, rtttl: str) -> None: + """ + Play a RTTTL sound from a given RTTTL string + """ + await self._make_request("POST", "rtttl", json_data={"rtttl": rtttl}) + + async def set_moodlight(self, moodlight: Moodlight) -> None: + """ + Set the entire matrix to a custom color or temperature + To disable moodlight pass `Moodlight()` + """ + await self._make_request( + "POST", "moodlight", json_data=moodlight.model_dump(exclude_none=True) + ) + + async def set_indicator( + self, + indicator: Literal[1, 2, 3], + color: Color, + blink: int = None, + fade: int = None, + ) -> None: + """ + Colored indicators serve as small notification signs displayed on specific areas of the screen: + Upper right corner: Indicator 1 + Right side: Indicator 2 + Lower right corner: Indicator 3 + To hide the indicators pass black as Color + Blink is in milliseconds + Fade is in milliseconds + """ + if blink and fade: + raise ValueError("fade and blink can't be set together") + + json_data = {"color": as_hex(color, format="long").upper()} + + if blink: + json_data["blink"] = blink + + if fade: + json_data["fade"] = fade + + await self._make_request( + "POST", + f"indicator{indicator}", + json_data=json_data, + ) + + async def set_custom_application( + self, + name: str, + custom_application: Union[CustomApplication, List[CustomApplication], None], + ) -> None: + """ + Set custom app or a list of custom app + When erasing apps, AWTRIX doesn't match the exact app name. Instead, it identifies apps that begin with the specified name. + To expunge all associated apps, send application=None. For example for name=test. This action will remove test0, test1, and so on. + To eradicate a single app, direct the command to, for instance, test1 + """ + if isinstance(custom_application, CustomApplication): + json_data = custom_application.model_dump(exclude_none=True) + else: + json_data = [ + app.model_dump(exclude_none=True) for app in custom_application + ] + + await self._make_request( + "POST", "custom", params={"name": name}, json_data=json_data + ) + + async def notify(self, notification: Notification) -> None: + """ + One-Time Notification + """ + await self._make_request( + "POST", "notify", json_data=notification.model_dump(exclude_none=True) + ) + + async def dismiss_notification(self) -> None: + """ + Easily dismiss a notification that was configured with "hold": true + """ + await self._make_request("POST", "notify/dismiss") + + async def next_app(self) -> None: + """ + Navigate to the next app + """ + await self._make_request("POST", "nextapp") + + async def previous_app(self) -> None: + """ + Navigate to the previous app + """ + await self._make_request("POST", "previousapp") + + async def switch_app(self, name: str) -> None: + """ + Directly transition to a desired app using its name + """ + await self._make_request("POST", "switch", json_data={"name": name}) + + async def get_settings(self) -> Settings: + """ + You can initiate the firmware update either through the update button in HA or using the following + """ + return Settings(**(await self._make_request("GET", "settings")).json()) + + async def set_settings(self, s: Settings) -> None: + """ + You can initiate the firmware update either through the update button in HA or using the following + """ + await self._make_request( + "POST", "settings", json_data=s.model_dump(exclude_none=True) + ) + + async def update(self) -> None: + """ + You can initiate the firmware update either through the update button in HA or using the following + """ + await self._make_request("POST", "doupdate") + + async def reboot(self) -> None: + """ + If you need to restart the Awtrix + """ + await self._make_request("POST", "reboot") + + async def erase(self) -> None: + """ + WARNING: This action will format the flash memory and EEPROM but will not modify the WiFi Settings. It essentially serves as a factory reset. + """ + await self._make_request("POST", "erase") + + async def reset_settings(self) -> None: + """ + WARNING: This action will reset all settings from the settings API. It does not reset the flash files and WiFi Settings. + """ + await self._make_request("POST", "resetSettings") + + +@asynccontextmanager +async def get_awtrix_http_client() -> AsyncIterator[AwtrixLightHttpClient]: + settings = AwtrixLightHttpClientSettings() + + auth = None + if settings.awtrix.username and settings.awtrix.password: + auth = ( + settings.awtrix.username, + settings.awtrix.password, + ) + + async with AsyncClient( + base_url=f"{settings.awtrix.base_url}api", + auth=auth, + verify=settings.awtrix.verify_ssl, + ) as client: + yield AwtrixLightHttpClient(client) diff --git a/awtrix_light_client/http_settings.py b/awtrix_light_client/http_settings.py new file mode 100644 index 0000000..3e4d09c --- /dev/null +++ b/awtrix_light_client/http_settings.py @@ -0,0 +1,19 @@ +from typing import Union + +from pydantic import BaseModel, AnyHttpUrl, FilePath +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class AwtrixHttpConfig(BaseModel): + base_url: AnyHttpUrl + username: str = None + password: str = None + verify_ssl: Union[bool, FilePath] = False + + +class AwtrixLightHttpClientSettings(BaseSettings): + awtrix: AwtrixHttpConfig + + model_config = SettingsConfigDict( + env_prefix="AWTRIX_HTTP_CLIENT_", env_file=".env", env_file_encoding="utf-8" + ) diff --git a/awtrix_light_client/models/application.py b/awtrix_light_client/models/application.py new file mode 100644 index 0000000..b44b929 --- /dev/null +++ b/awtrix_light_client/models/application.py @@ -0,0 +1,270 @@ +from typing import List, Union, Any, Optional +from enum import IntEnum + +from pydantic import BaseModel, Field, model_validator, field_serializer +from pydantic.networks import Annotated, Url, UrlConstraints +from pydantic_extra_types.color import Color + +from .effect import EffectType, EffectSetting +from .utils import convert_color_to_hex, convert_colors_to_hex + + +class TextCase(IntEnum): + GLOBAL = 0 + FORCE_UPPERCASE = 1 + SHOW_AS_IT_SEND = 2 + + +class PushIcon(IntEnum): + NOT_MOVING = 0 + MOVING_ONE_TIME = 1 + MOVING = 2 + + +class Dp(BaseModel): + x: int + y: int + cl: Color + + @field_serializer( + "cl", + ) + def convert_color_to_int(v: Color) -> str: + if v: + return convert_color_to_hex(v) + else: + return v + + +class Dl(BaseModel): + x0: int + y0: int + x1: int + y1: int + cl: Color + + @field_serializer( + "cl", + ) + def convert_color_to_int(v: Color) -> str: + if v: + return convert_color_to_hex(v) + else: + return v + + +class Dr(BaseModel): + x: int + y: int + w: int + h: int + cl: Color + + @field_serializer( + "cl", + ) + def convert_color_to_int(v: Color) -> str: + if v: + return convert_color_to_hex(v) + else: + return v + + +class Df(BaseModel): + x: int + y: int + w: int + h: int + cl: Color + + @field_serializer( + "cl", + ) + def convert_color_to_int(v: Color) -> str: + if v: + return convert_color_to_hex(v) + else: + return v + + +class Dc(BaseModel): + x: int + y: int + r: int + cl: Color + + @field_serializer( + "cl", + ) + def convert_color_to_int(v: Color) -> str: + if v: + return convert_color_to_hex(v) + else: + return v + + +class Dfc(BaseModel): + x: int + y: int + r: int + cl: Color + + @field_serializer( + "cl", + ) + def convert_color_to_int(v: Color) -> str: + if v: + return convert_color_to_hex(v) + else: + return v + + +class Dt(BaseModel): + x: int + y: int + t: int + cl: Color + + @field_serializer( + "cl", + ) + def convert_color_to_int(v: Color) -> str: + if v: + return convert_color_to_hex(v) + else: + return v + + +class Db(BaseModel): + x: int + y: int + r: int + cl: Any + + # cl: Union[bytes, bytearray, memoryview] need to generate pydantic shema, leaving it for now + @field_serializer( + "cl", + ) + def convert_color_to_int(v: Color) -> str: + if v: + return convert_color_to_hex(v) + else: + return v + + +class LifeTimeMode(IntEnum): + DELETE = 0 + STALE = 1 + + +class Fragment(BaseModel): + t: str + c: Color + + @field_serializer( + "c", + ) + def convert_color_to_int(v: Color) -> str: + if v: + return convert_color_to_hex(v) + else: + return v + + +class BaseApplication(BaseModel): + text: Optional[Union[str, List[Fragment]]] = None + textCase: Optional[TextCase] = None + topText: Optional[bool] = None + textOffset: Optional[int] = Field(default=None, ge=0) + center: Optional[bool] = None + color: Optional[Color] = None + gradient: Optional[List[Color]] = Field(default=None, min_length=2, max_length=2) + blinkText: Optional[int] = None + fadeText: Optional[int] = None + background: Optional[Color] = None + rainbow: Optional[bool] = None + icon: Optional[str] = None + pushIcon: Optional[PushIcon] = None + repeat: Optional[int] = Field(default=None, ge=-1) + duration: Optional[int] = Field(default=None, ge=1) + bar: Optional[List[int]] = Field(default=None, max_length=16) + line: Optional[List[int]] = Field(default=None, max_length=16) + autoscale: Optional[bool] = None + progress: Optional[int] = Field(default=None, ge=-1, le=100) + progressC: Optional[Color] = None + progressBC: Optional[Color] = None + draw: Optional[List[Union[Dp, Dl, Dr, Df, Dc, Dfc, Dt, Db]]] = None + noScroll: Optional[bool] = None + scrollSpeed: Optional[int] = Field(default=None, ge=0, le=100) + effect: Optional[EffectType] = None + effectSettings: Optional[EffectSetting] = None + + class ConfigDict: + use_enum_values = True + + @model_validator(mode="after") + def check_constraint_blink_text(self) -> "BaseApplication": + if self.blinkText and (self.gradient or self.rainbow): + raise ValueError( + "blink text can be set only if gradient and rainbow are not used" + ) + return self + + @model_validator(mode="after") + def check_constraint_fade_text(self) -> "BaseApplication": + if self.fadeText and (self.gradient or self.rainbow): + raise ValueError( + "fade text can be set only if gradient and rainbow are not used" + ) + return self + + @model_validator(mode="after") + def check_constraint_bar(self) -> "BaseApplication": + if self.icon and self.bar and len(self.bar) > 11: + raise ValueError("bar can have only 11 values with icon") + return self + + @model_validator(mode="after") + def check_constraint_line(self) -> "BaseApplication": + if self.icon and self.line and len(self.line) > 11: + raise ValueError("line can have only 11 values with icon") + return self + + @field_serializer("color", "gradient", "background", "progressC", "progressBC") + def convert_color_to_hex(v: Union[List[Color], Color]) -> Union[List, str]: + if isinstance(v, List): + return convert_colors_to_hex(v) + else: + return convert_color_to_hex(v) + + +class CustomApplication(BaseApplication): + pos: Optional[int] = Field(default=None, ge=0) + lifetime: Optional[int] = Field(default=None, ge=0) + lifetimeMode: Optional[LifeTimeMode] = None + save: Optional[bool] = None + + +CLIENT_TYPE = Annotated[ + Url, UrlConstraints(allowed_schemes=["http", "https", "mqtt", "mqtts"]) +] + + +class Notification(BaseApplication): + hold: Optional[bool] = None + sound: Optional[str] = None + rtttl: Optional[str] = None + loopSound: Optional[bool] = None + stack: Optional[bool] = None + wakeup: Optional[bool] = None + clients: Optional[List[CLIENT_TYPE]] = None + + @model_validator(mode="after") + def check_constraint_sound_rtttl(self) -> "Notification": + if self.sound and self.rtttl: + raise ValueError("sound and rtttl can't be set together") + return self + + @field_serializer("clients") + def convert_url_to_str(clients: CLIENT_TYPE) -> List[str]: + return [str(client) for client in clients] diff --git a/awtrix_light_client/models/effect.py b/awtrix_light_client/models/effect.py new file mode 100644 index 0000000..c8e5797 --- /dev/null +++ b/awtrix_light_client/models/effect.py @@ -0,0 +1,41 @@ +from pydantic import BaseModel +from enum import Enum + + +class Palette(str, Enum): + CLOUD = "Cloud" + LAVA = "Lava" + OCEAN = "Ocean" + FOREST = "Forest" + STRIPE = "Stripe" + PARTY = "Party" + HEAT = "Heat" + RAINBOW = "Rainbow" + + +class EffectType(str, Enum): + FADE = "Fade" + MOVINGLINE = "MovingLine" + BRICKBREAKER = "BrickBreaker" + PINGPONG = "PingPong" + RADAR = "Radar" + CHECKERBOARD = "Checkerboard" + FIREWORKS = "Fireworks" + PLASMACLOUD = "PlasmaCloud" + RIPPLE = "Ripple" + SNAKE = "Snake" + PACIFICA = "Pacifica" + THEATERCHASE = "TheaterChase" + PLASMA = "Plasma" + MATRIX = "Matrix" + SWIRLIN = "SwirlIn" + SWIRLOUT = "SwirlOut" + LOOKINGEYES = "LookingEyes" + TWINKLINGSTARS = "TwinklingStars" + COLORWAVES = "ColorWaves" + + +class EffectSetting(BaseModel): + speed: int + palette: Palette + blend: bool diff --git a/awtrix_light_client/models/loop.py b/awtrix_light_client/models/loop.py new file mode 100644 index 0000000..d15722a --- /dev/null +++ b/awtrix_light_client/models/loop.py @@ -0,0 +1,6 @@ +from typing import List +from pydantic import BaseModel + + +class Loop(BaseModel): + loops: List[str] diff --git a/awtrix_light_client/models/moodlight.py b/awtrix_light_client/models/moodlight.py new file mode 100644 index 0000000..e24a305 --- /dev/null +++ b/awtrix_light_client/models/moodlight.py @@ -0,0 +1,22 @@ +from pydantic import BaseModel, model_validator, field_serializer +from pydantic_extra_types.color import Color + +from .utils import convert_color_to_hex + + +class Moodlight(BaseModel): + brightness: int = None + kelvin: int = None + color: Color = None + + @model_validator(mode="after") + def check_constraint_blink_text(self) -> "Moodlight": + if self.kelvin and self.color: + raise ValueError("kelvin and color can't be set together") + return self + + @field_serializer("color") + def convert_color_to_hex(v: Color) -> str: + if v: + return convert_color_to_hex(v) + return v diff --git a/awtrix_light_client/models/screen.py b/awtrix_light_client/models/screen.py new file mode 100644 index 0000000..77ac268 --- /dev/null +++ b/awtrix_light_client/models/screen.py @@ -0,0 +1,6 @@ +from typing import List +from pydantic import BaseModel, Field + + +class Screen(BaseModel): + matrix: List[int] = Field(min_length=256, max_length=256) diff --git a/awtrix_light_client/models/setting.py b/awtrix_light_client/models/setting.py new file mode 100644 index 0000000..820202a --- /dev/null +++ b/awtrix_light_client/models/setting.py @@ -0,0 +1,112 @@ +from typing import Any +from typing import Literal, Union + +from pydantic_extra_types.color import Color +from pydantic import BaseModel, Field, field_validator, field_serializer + +from .transition import TransitionType +from .utils import convert_color_to_hex, as_hex + + +class Settings(BaseModel): + ATIME: int = Field(default=None, ge=0) + TEFF: TransitionType = None + TSPEED: int = Field(default=None, ge=0) + TCOL: Union[Color, int] = None + TMODE: int = Field(default=None, ge=0, le=4) + CHCOL: Union[Color, int] = None + CBCOL: Union[Color, int] = None + CTCOL: Union[Color, int] = None + WD: bool = None + WDCA: Union[Color, int] = None + WDCI: Union[Color, int] = None + BRI: int = Field(default=0, ge=0, le=255) + ABRI: bool = None + ATRANS: bool = None + CCORRECTION: Union[Color, str] = None + CTEMP: Union[Color, str] = None + TFORMAT: Literal[ + "%H:%M:%S", + "%l:%M:%S", + "%H:%M", + "%H %M", + "%l:%M", + "%l %M", + "%l:%M %p", + "%l %M %p", + ] = None + DFORMAT: Literal[ + "%d.%m.%y", + "%d.%m", + "%y-%m-%d", + "%m-%d", + "%m/%d/%y", + "%m/%d", + "%d/%m/%y", + "%d/%m", + "%m-%d-%y", + ] = None + SOM: bool = None + CEL: bool = None + MAT: int = None + SOUND: bool = None + GAMMA: float = None + BLOCKN: bool = None + UPPERCASE: bool = None + TIME_COL: Union[Color, Literal[0]] = None + DATE_COL: Union[Color, Literal[0]] = None + TEMP_COL: Union[Color, Literal[0]] = None + HUM_COL: Union[Color, Literal[0]] = None + BAT_COL: Union[Color, Literal[0]] = None + SSPEED: int = Field(default=None, ge=0, le=100) + TIM: bool = None + DAT: bool = None + HUM: bool = None + TEMP: bool = None + BAT: bool = None + MATP: bool = None + + class ConfigDict: + use_enum_values = True + + @field_validator("CCORRECTION", "CTEMP", mode="before") + @classmethod + def convert_str_to_color(cls, v: Union[Color, str]) -> Color: + if isinstance(v, str): + return Color(v) + else: + return v + + @field_validator("TCOL", "CHCOL", "CBCOL", "CTCOL", "WDCA", "WDCI", mode="before") + @classmethod + def convert_integer_to_color(cls, v: Union[Color, int]) -> Color: + if isinstance(v, int): + return Color("{0:06X}".format(v)) + else: + return v + + @field_serializer("CCORRECTION", "CTEMP") + def convert_color_to_hex(v: Union[Color, str]) -> str: + if isinstance(v, Color): + return convert_color_to_hex(v) + else: + return v + + @field_serializer( + "TCOL", + "CHCOL", + "CBCOL", + "CTCOL", + "WDCA", + "WDCI", + "TIME_COL", + "DATE_COL", + "TEMP_COL", + "HUM_COL", + "BAT_COL", + ) + def convert_color_to_int(v: Any) -> int: + if isinstance(v, Color): + return int(as_hex(v, format="long")[1:], 16) + else: + return v diff --git a/awtrix_light_client/models/stat.py b/awtrix_light_client/models/stat.py new file mode 100644 index 0000000..e1c7992 --- /dev/null +++ b/awtrix_light_client/models/stat.py @@ -0,0 +1,23 @@ +from pydantic import BaseModel + + +class Stats(BaseModel): + bat: int + bat_raw: int + type: int + lux: int + ldr_raw: int + ram: int + bri: int + temp: int + hum: int + uptime: int + wifi_signal: int + messages: int + version: str + indicator1: bool + indicator2: bool + indicator3: bool + app: str + uid: str + matrix: bool diff --git a/awtrix_light_client/models/transition.py b/awtrix_light_client/models/transition.py new file mode 100644 index 0000000..2c4187a --- /dev/null +++ b/awtrix_light_client/models/transition.py @@ -0,0 +1,15 @@ +from enum import IntEnum + + +class TransitionType(IntEnum): + RANDOM = 0 + SLIDE = 1 + DIM = 2 + ZOOM = 3 + ROTATE = 4 + PIXELATE = 5 + CURTAIN = 6 + RIPPLE = 7 + BLINK = 8 + RELOAD = 9 + FADE = 10 diff --git a/awtrix_light_client/models/utils.py b/awtrix_light_client/models/utils.py new file mode 100644 index 0000000..4327a12 --- /dev/null +++ b/awtrix_light_client/models/utils.py @@ -0,0 +1,28 @@ +from typing import List, Literal + +from pydantic_extra_types.color import Color + + +def convert_color_to_hex(color: Color) -> str: + return as_hex(color, format="long").upper() + + +def convert_colors_to_hex(colors: List[Color]) -> str: + return [convert_color_to_hex(color) for color in colors] + + +def as_hex(c: Color, format: Literal["short", "long"] = "short") -> str: + """ + # https://github.com/pydantic/pydantic-extra-types/blob/main/pydantic_extra_types/color.py#L141 + Will be removed when pydantic-extra-type will release a new version + """ + values = [round(c * 255) for c in c._rgba[:3]] + if c._rgba.alpha is not None: + values.append(round(c._rgba.alpha * 255)) + + as_hex = "".join(f"{v:02x}" for v in values) + if format == "short" and all( + c in {int(c * 2, 16) for c in "0123456789abcdef"} for c in values + ): + as_hex = "".join(as_hex[c] for c in range(0, len(as_hex), 2)) + return "#" + as_hex diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..d7545ad --- /dev/null +++ b/poetry.lock @@ -0,0 +1,633 @@ +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. + +[[package]] +name = "annotated-types" +version = "0.6.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, + {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, +] + +[[package]] +name = "anyio" +version = "4.0.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.8" +files = [ + {file = "anyio-4.0.0-py3-none-any.whl", hash = "sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f"}, + {file = "anyio-4.0.0.tar.gz", hash = "sha256:f7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +sniffio = ">=1.1" + +[package.extras] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (>=0.22)"] + +[[package]] +name = "black" +version = "23.10.1" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.8" +files = [ + {file = "black-23.10.1-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:ec3f8e6234c4e46ff9e16d9ae96f4ef69fa328bb4ad08198c8cee45bb1f08c69"}, + {file = "black-23.10.1-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:1b917a2aa020ca600483a7b340c165970b26e9029067f019e3755b56e8dd5916"}, + {file = "black-23.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c74de4c77b849e6359c6f01987e94873c707098322b91490d24296f66d067dc"}, + {file = "black-23.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:7b4d10b0f016616a0d93d24a448100adf1699712fb7a4efd0e2c32bbb219b173"}, + {file = "black-23.10.1-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:b15b75fc53a2fbcac8a87d3e20f69874d161beef13954747e053bca7a1ce53a0"}, + {file = "black-23.10.1-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:e293e4c2f4a992b980032bbd62df07c1bcff82d6964d6c9496f2cd726e246ace"}, + {file = "black-23.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d56124b7a61d092cb52cce34182a5280e160e6aff3137172a68c2c2c4b76bcb"}, + {file = "black-23.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:3f157a8945a7b2d424da3335f7ace89c14a3b0625e6593d21139c2d8214d55ce"}, + {file = "black-23.10.1-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:cfcce6f0a384d0da692119f2d72d79ed07c7159879d0bb1bb32d2e443382bf3a"}, + {file = "black-23.10.1-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:33d40f5b06be80c1bbce17b173cda17994fbad096ce60eb22054da021bf933d1"}, + {file = "black-23.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:840015166dbdfbc47992871325799fd2dc0dcf9395e401ada6d88fe11498abad"}, + {file = "black-23.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:037e9b4664cafda5f025a1728c50a9e9aedb99a759c89f760bd83730e76ba884"}, + {file = "black-23.10.1-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:7cb5936e686e782fddb1c73f8aa6f459e1ad38a6a7b0e54b403f1f05a1507ee9"}, + {file = "black-23.10.1-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:7670242e90dc129c539e9ca17665e39a146a761e681805c54fbd86015c7c84f7"}, + {file = "black-23.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ed45ac9a613fb52dad3b61c8dea2ec9510bf3108d4db88422bacc7d1ba1243d"}, + {file = "black-23.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:6d23d7822140e3fef190734216cefb262521789367fbdc0b3f22af6744058982"}, + {file = "black-23.10.1-py3-none-any.whl", hash = "sha256:d431e6739f727bb2e0495df64a6c7a5310758e87505f5f8cde9ff6c0f2d7e4fe"}, + {file = "black-23.10.1.tar.gz", hash = "sha256:1f8ce316753428ff68749c65a5f7844631aa18c8679dfd3ca9dc1a289979c258"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "certifi" +version = "2023.7.22" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, + {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, +] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[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 = "coverage" +version = "7.3.2" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "coverage-7.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d872145f3a3231a5f20fd48500274d7df222e291d90baa2026cc5152b7ce86bf"}, + {file = "coverage-7.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:310b3bb9c91ea66d59c53fa4989f57d2436e08f18fb2f421a1b0b6b8cc7fffda"}, + {file = "coverage-7.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f47d39359e2c3779c5331fc740cf4bce6d9d680a7b4b4ead97056a0ae07cb49a"}, + {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa72dbaf2c2068404b9870d93436e6d23addd8bbe9295f49cbca83f6e278179c"}, + {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:beaa5c1b4777f03fc63dfd2a6bd820f73f036bfb10e925fce067b00a340d0f3f"}, + {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:dbc1b46b92186cc8074fee9d9fbb97a9dd06c6cbbef391c2f59d80eabdf0faa6"}, + {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:315a989e861031334d7bee1f9113c8770472db2ac484e5b8c3173428360a9148"}, + {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d1bc430677773397f64a5c88cb522ea43175ff16f8bfcc89d467d974cb2274f9"}, + {file = "coverage-7.3.2-cp310-cp310-win32.whl", hash = "sha256:a889ae02f43aa45032afe364c8ae84ad3c54828c2faa44f3bfcafecb5c96b02f"}, + {file = "coverage-7.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c0ba320de3fb8c6ec16e0be17ee1d3d69adcda99406c43c0409cb5c41788a611"}, + {file = "coverage-7.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ac8c802fa29843a72d32ec56d0ca792ad15a302b28ca6203389afe21f8fa062c"}, + {file = "coverage-7.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:89a937174104339e3a3ffcf9f446c00e3a806c28b1841c63edb2b369310fd074"}, + {file = "coverage-7.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e267e9e2b574a176ddb983399dec325a80dbe161f1a32715c780b5d14b5f583a"}, + {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2443cbda35df0d35dcfb9bf8f3c02c57c1d6111169e3c85fc1fcc05e0c9f39a3"}, + {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4175e10cc8dda0265653e8714b3174430b07c1dca8957f4966cbd6c2b1b8065a"}, + {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0cbf38419fb1a347aaf63481c00f0bdc86889d9fbf3f25109cf96c26b403fda1"}, + {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5c913b556a116b8d5f6ef834038ba983834d887d82187c8f73dec21049abd65c"}, + {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1981f785239e4e39e6444c63a98da3a1db8e971cb9ceb50a945ba6296b43f312"}, + {file = "coverage-7.3.2-cp311-cp311-win32.whl", hash = "sha256:43668cabd5ca8258f5954f27a3aaf78757e6acf13c17604d89648ecc0cc66640"}, + {file = "coverage-7.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10c39c0452bf6e694511c901426d6b5ac005acc0f78ff265dbe36bf81f808a2"}, + {file = "coverage-7.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4cbae1051ab791debecc4a5dcc4a1ff45fc27b91b9aee165c8a27514dd160836"}, + {file = "coverage-7.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12d15ab5833a997716d76f2ac1e4b4d536814fc213c85ca72756c19e5a6b3d63"}, + {file = "coverage-7.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c7bba973ebee5e56fe9251300c00f1579652587a9f4a5ed8404b15a0471f216"}, + {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe494faa90ce6381770746077243231e0b83ff3f17069d748f645617cefe19d4"}, + {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6e9589bd04d0461a417562649522575d8752904d35c12907d8c9dfeba588faf"}, + {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d51ac2a26f71da1b57f2dc81d0e108b6ab177e7d30e774db90675467c847bbdf"}, + {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:99b89d9f76070237975b315b3d5f4d6956ae354a4c92ac2388a5695516e47c84"}, + {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fa28e909776dc69efb6ed975a63691bc8172b64ff357e663a1bb06ff3c9b589a"}, + {file = "coverage-7.3.2-cp312-cp312-win32.whl", hash = "sha256:289fe43bf45a575e3ab10b26d7b6f2ddb9ee2dba447499f5401cfb5ecb8196bb"}, + {file = "coverage-7.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dbc3ed60e8659bc59b6b304b43ff9c3ed858da2839c78b804973f613d3e92ed"}, + {file = "coverage-7.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f94b734214ea6a36fe16e96a70d941af80ff3bfd716c141300d95ebc85339738"}, + {file = "coverage-7.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:af3d828d2c1cbae52d34bdbb22fcd94d1ce715d95f1a012354a75e5913f1bda2"}, + {file = "coverage-7.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:630b13e3036e13c7adc480ca42fa7afc2a5d938081d28e20903cf7fd687872e2"}, + {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9eacf273e885b02a0273bb3a2170f30e2d53a6d53b72dbe02d6701b5296101c"}, + {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8f17966e861ff97305e0801134e69db33b143bbfb36436efb9cfff6ec7b2fd9"}, + {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b4275802d16882cf9c8b3d057a0839acb07ee9379fa2749eca54efbce1535b82"}, + {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:72c0cfa5250f483181e677ebc97133ea1ab3eb68645e494775deb6a7f6f83901"}, + {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cb536f0dcd14149425996821a168f6e269d7dcd2c273a8bff8201e79f5104e76"}, + {file = "coverage-7.3.2-cp38-cp38-win32.whl", hash = "sha256:307adb8bd3abe389a471e649038a71b4eb13bfd6b7dd9a129fa856f5c695cf92"}, + {file = "coverage-7.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:88ed2c30a49ea81ea3b7f172e0269c182a44c236eb394718f976239892c0a27a"}, + {file = "coverage-7.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b631c92dfe601adf8f5ebc7fc13ced6bb6e9609b19d9a8cd59fa47c4186ad1ce"}, + {file = "coverage-7.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d3d9df4051c4a7d13036524b66ecf7a7537d14c18a384043f30a303b146164e9"}, + {file = "coverage-7.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f7363d3b6a1119ef05015959ca24a9afc0ea8a02c687fe7e2d557705375c01f"}, + {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f11cc3c967a09d3695d2a6f03fb3e6236622b93be7a4b5dc09166a861be6d25"}, + {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:149de1d2401ae4655c436a3dced6dd153f4c3309f599c3d4bd97ab172eaf02d9"}, + {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3a4006916aa6fee7cd38db3bfc95aa9c54ebb4ffbfc47c677c8bba949ceba0a6"}, + {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9028a3871280110d6e1aa2df1afd5ef003bab5fb1ef421d6dc748ae1c8ef2ebc"}, + {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9f805d62aec8eb92bab5b61c0f07329275b6f41c97d80e847b03eb894f38d083"}, + {file = "coverage-7.3.2-cp39-cp39-win32.whl", hash = "sha256:d1c88ec1a7ff4ebca0219f5b1ef863451d828cccf889c173e1253aa84b1e07ce"}, + {file = "coverage-7.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b4767da59464bb593c07afceaddea61b154136300881844768037fd5e859353f"}, + {file = "coverage-7.3.2-pp38.pp39.pp310-none-any.whl", hash = "sha256:ae97af89f0fbf373400970c0a21eef5aa941ffeed90aee43650b81f7d7f47637"}, + {file = "coverage-7.3.2.tar.gz", hash = "sha256:be32ad29341b0170e795ca590e1c07e81fc061cb5b10c74ce7203491484404ef"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "exceptiongroup" +version = "1.1.3" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, + {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "httpcore" +version = "0.18.0" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpcore-0.18.0-py3-none-any.whl", hash = "sha256:adc5398ee0a476567bf87467063ee63584a8bce86078bf748e48754f60202ced"}, + {file = "httpcore-0.18.0.tar.gz", hash = "sha256:13b5e5cd1dca1a6636a6aaea212b19f4f85cd88c366a2b82304181b769aab3c9"}, +] + +[package.dependencies] +anyio = ">=3.0,<5.0" +certifi = "*" +h11 = ">=0.13,<0.15" +sniffio = "==1.*" + +[package.extras] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] + +[[package]] +name = "httpx" +version = "0.25.0" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpx-0.25.0-py3-none-any.whl", hash = "sha256:181ea7f8ba3a82578be86ef4171554dd45fec26a02556a744db029a0a27b7100"}, + {file = "httpx-0.25.0.tar.gz", hash = "sha256:47ecda285389cb32bb2691cc6e069e3ab0205956f681c5b2ad2325719751d875"}, +] + +[package.dependencies] +certifi = "*" +httpcore = ">=0.18.0,<0.19.0" +idna = "*" +sniffio = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] + +[[package]] +name = "idna" +version = "3.4" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] + +[[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 = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "packaging" +version = "23.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, +] + +[[package]] +name = "pathspec" +version = "0.11.2" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, + {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, +] + +[[package]] +name = "platformdirs" +version = "3.11.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = ">=3.7" +files = [ + {file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"}, + {file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"}, +] + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] + +[[package]] +name = "pluggy" +version = "1.3.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, + {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pydantic" +version = "2.4.2" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic-2.4.2-py3-none-any.whl", hash = "sha256:bc3ddf669d234f4220e6e1c4d96b061abe0998185a8d7855c0126782b7abc8c1"}, + {file = "pydantic-2.4.2.tar.gz", hash = "sha256:94f336138093a5d7f426aac732dcfe7ab4eb4da243c88f891d65deb4a2556ee7"}, +] + +[package.dependencies] +annotated-types = ">=0.4.0" +pydantic-core = "2.10.1" +typing-extensions = ">=4.6.1" + +[package.extras] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.10.1" +description = "" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic_core-2.10.1-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:d64728ee14e667ba27c66314b7d880b8eeb050e58ffc5fec3b7a109f8cddbd63"}, + {file = "pydantic_core-2.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:48525933fea744a3e7464c19bfede85df4aba79ce90c60b94d8b6e1eddd67096"}, + {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef337945bbd76cce390d1b2496ccf9f90b1c1242a3a7bc242ca4a9fc5993427a"}, + {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1392e0638af203cee360495fd2cfdd6054711f2db5175b6e9c3c461b76f5175"}, + {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0675ba5d22de54d07bccde38997e780044dcfa9a71aac9fd7d4d7a1d2e3e65f7"}, + {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:128552af70a64660f21cb0eb4876cbdadf1a1f9d5de820fed6421fa8de07c893"}, + {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f6e6aed5818c264412ac0598b581a002a9f050cb2637a84979859e70197aa9e"}, + {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ecaac27da855b8d73f92123e5f03612b04c5632fd0a476e469dfc47cd37d6b2e"}, + {file = "pydantic_core-2.10.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b3c01c2fb081fced3bbb3da78510693dc7121bb893a1f0f5f4b48013201f362e"}, + {file = "pydantic_core-2.10.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:92f675fefa977625105708492850bcbc1182bfc3e997f8eecb866d1927c98ae6"}, + {file = "pydantic_core-2.10.1-cp310-none-win32.whl", hash = "sha256:420a692b547736a8d8703c39ea935ab5d8f0d2573f8f123b0a294e49a73f214b"}, + {file = "pydantic_core-2.10.1-cp310-none-win_amd64.whl", hash = "sha256:0880e239827b4b5b3e2ce05e6b766a7414e5f5aedc4523be6b68cfbc7f61c5d0"}, + {file = "pydantic_core-2.10.1-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:073d4a470b195d2b2245d0343569aac7e979d3a0dcce6c7d2af6d8a920ad0bea"}, + {file = "pydantic_core-2.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:600d04a7b342363058b9190d4e929a8e2e715c5682a70cc37d5ded1e0dd370b4"}, + {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39215d809470f4c8d1881758575b2abfb80174a9e8daf8f33b1d4379357e417c"}, + {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eeb3d3d6b399ffe55f9a04e09e635554012f1980696d6b0aca3e6cf42a17a03b"}, + {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a7a7902bf75779bc12ccfc508bfb7a4c47063f748ea3de87135d433a4cca7a2f"}, + {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3625578b6010c65964d177626fde80cf60d7f2e297d56b925cb5cdeda6e9925a"}, + {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:caa48fc31fc7243e50188197b5f0c4228956f97b954f76da157aae7f67269ae8"}, + {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:07ec6d7d929ae9c68f716195ce15e745b3e8fa122fc67698ac6498d802ed0fa4"}, + {file = "pydantic_core-2.10.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e6f31a17acede6a8cd1ae2d123ce04d8cca74056c9d456075f4f6f85de055607"}, + {file = "pydantic_core-2.10.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d8f1ebca515a03e5654f88411420fea6380fc841d1bea08effb28184e3d4899f"}, + {file = "pydantic_core-2.10.1-cp311-none-win32.whl", hash = "sha256:6db2eb9654a85ada248afa5a6db5ff1cf0f7b16043a6b070adc4a5be68c716d6"}, + {file = "pydantic_core-2.10.1-cp311-none-win_amd64.whl", hash = "sha256:4a5be350f922430997f240d25f8219f93b0c81e15f7b30b868b2fddfc2d05f27"}, + {file = "pydantic_core-2.10.1-cp311-none-win_arm64.whl", hash = "sha256:5fdb39f67c779b183b0c853cd6b45f7db84b84e0571b3ef1c89cdb1dfc367325"}, + {file = "pydantic_core-2.10.1-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:b1f22a9ab44de5f082216270552aa54259db20189e68fc12484873d926426921"}, + {file = "pydantic_core-2.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8572cadbf4cfa95fb4187775b5ade2eaa93511f07947b38f4cd67cf10783b118"}, + {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db9a28c063c7c00844ae42a80203eb6d2d6bbb97070cfa00194dff40e6f545ab"}, + {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e2a35baa428181cb2270a15864ec6286822d3576f2ed0f4cd7f0c1708472aff"}, + {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05560ab976012bf40f25d5225a58bfa649bb897b87192a36c6fef1ab132540d7"}, + {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d6495008733c7521a89422d7a68efa0a0122c99a5861f06020ef5b1f51f9ba7c"}, + {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14ac492c686defc8e6133e3a2d9eaf5261b3df26b8ae97450c1647286750b901"}, + {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8282bab177a9a3081fd3d0a0175a07a1e2bfb7fcbbd949519ea0980f8a07144d"}, + {file = "pydantic_core-2.10.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:aafdb89fdeb5fe165043896817eccd6434aee124d5ee9b354f92cd574ba5e78f"}, + {file = "pydantic_core-2.10.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f6defd966ca3b187ec6c366604e9296f585021d922e666b99c47e78738b5666c"}, + {file = "pydantic_core-2.10.1-cp312-none-win32.whl", hash = "sha256:7c4d1894fe112b0864c1fa75dffa045720a194b227bed12f4be7f6045b25209f"}, + {file = "pydantic_core-2.10.1-cp312-none-win_amd64.whl", hash = "sha256:5994985da903d0b8a08e4935c46ed8daf5be1cf217489e673910951dc533d430"}, + {file = "pydantic_core-2.10.1-cp312-none-win_arm64.whl", hash = "sha256:0d8a8adef23d86d8eceed3e32e9cca8879c7481c183f84ed1a8edc7df073af94"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:9badf8d45171d92387410b04639d73811b785b5161ecadabf056ea14d62d4ede"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:ebedb45b9feb7258fac0a268a3f6bec0a2ea4d9558f3d6f813f02ff3a6dc6698"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfe1090245c078720d250d19cb05d67e21a9cd7c257698ef139bc41cf6c27b4f"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e357571bb0efd65fd55f18db0a2fb0ed89d0bb1d41d906b138f088933ae618bb"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b3dcd587b69bbf54fc04ca157c2323b8911033e827fffaecf0cafa5a892a0904"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c120c9ce3b163b985a3b966bb701114beb1da4b0468b9b236fc754783d85aa3"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15d6bca84ffc966cc9976b09a18cf9543ed4d4ecbd97e7086f9ce9327ea48891"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5cabb9710f09d5d2e9e2748c3e3e20d991a4c5f96ed8f1132518f54ab2967221"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:82f55187a5bebae7d81d35b1e9aaea5e169d44819789837cdd4720d768c55d15"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:1d40f55222b233e98e3921df7811c27567f0e1a4411b93d4c5c0f4ce131bc42f"}, + {file = "pydantic_core-2.10.1-cp37-none-win32.whl", hash = "sha256:14e09ff0b8fe6e46b93d36a878f6e4a3a98ba5303c76bb8e716f4878a3bee92c"}, + {file = "pydantic_core-2.10.1-cp37-none-win_amd64.whl", hash = "sha256:1396e81b83516b9d5c9e26a924fa69164156c148c717131f54f586485ac3c15e"}, + {file = "pydantic_core-2.10.1-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:6835451b57c1b467b95ffb03a38bb75b52fb4dc2762bb1d9dbed8de31ea7d0fc"}, + {file = "pydantic_core-2.10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b00bc4619f60c853556b35f83731bd817f989cba3e97dc792bb8c97941b8053a"}, + {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fa467fd300a6f046bdb248d40cd015b21b7576c168a6bb20aa22e595c8ffcdd"}, + {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d99277877daf2efe074eae6338453a4ed54a2d93fb4678ddfe1209a0c93a2468"}, + {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa7db7558607afeccb33c0e4bf1c9a9a835e26599e76af6fe2fcea45904083a6"}, + {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aad7bd686363d1ce4ee930ad39f14e1673248373f4a9d74d2b9554f06199fb58"}, + {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:443fed67d33aa85357464f297e3d26e570267d1af6fef1c21ca50921d2976302"}, + {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:042462d8d6ba707fd3ce9649e7bf268633a41018d6a998fb5fbacb7e928a183e"}, + {file = "pydantic_core-2.10.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ecdbde46235f3d560b18be0cb706c8e8ad1b965e5c13bbba7450c86064e96561"}, + {file = "pydantic_core-2.10.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ed550ed05540c03f0e69e6d74ad58d026de61b9eaebebbaaf8873e585cbb18de"}, + {file = "pydantic_core-2.10.1-cp38-none-win32.whl", hash = "sha256:8cdbbd92154db2fec4ec973d45c565e767ddc20aa6dbaf50142676484cbff8ee"}, + {file = "pydantic_core-2.10.1-cp38-none-win_amd64.whl", hash = "sha256:9f6f3e2598604956480f6c8aa24a3384dbf6509fe995d97f6ca6103bb8c2534e"}, + {file = "pydantic_core-2.10.1-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:655f8f4c8d6a5963c9a0687793da37b9b681d9ad06f29438a3b2326d4e6b7970"}, + {file = "pydantic_core-2.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e570ffeb2170e116a5b17e83f19911020ac79d19c96f320cbfa1fa96b470185b"}, + {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64322bfa13e44c6c30c518729ef08fda6026b96d5c0be724b3c4ae4da939f875"}, + {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:485a91abe3a07c3a8d1e082ba29254eea3e2bb13cbbd4351ea4e5a21912cc9b0"}, + {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7c2b8eb9fc872e68b46eeaf835e86bccc3a58ba57d0eedc109cbb14177be531"}, + {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5cb87bdc2e5f620693148b5f8f842d293cae46c5f15a1b1bf7ceeed324a740c"}, + {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25bd966103890ccfa028841a8f30cebcf5875eeac8c4bde4fe221364c92f0c9a"}, + {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f323306d0556351735b54acbf82904fe30a27b6a7147153cbe6e19aaaa2aa429"}, + {file = "pydantic_core-2.10.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0c27f38dc4fbf07b358b2bc90edf35e82d1703e22ff2efa4af4ad5de1b3833e7"}, + {file = "pydantic_core-2.10.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f1365e032a477c1430cfe0cf2856679529a2331426f8081172c4a74186f1d595"}, + {file = "pydantic_core-2.10.1-cp39-none-win32.whl", hash = "sha256:a1c311fd06ab3b10805abb72109f01a134019739bd3286b8ae1bc2fc4e50c07a"}, + {file = "pydantic_core-2.10.1-cp39-none-win_amd64.whl", hash = "sha256:ae8a8843b11dc0b03b57b52793e391f0122e740de3df1474814c700d2622950a"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:d43002441932f9a9ea5d6f9efaa2e21458221a3a4b417a14027a1d530201ef1b"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fcb83175cc4936a5425dde3356f079ae03c0802bbdf8ff82c035f8a54b333521"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:962ed72424bf1f72334e2f1e61b68f16c0e596f024ca7ac5daf229f7c26e4208"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2cf5bb4dd67f20f3bbc1209ef572a259027c49e5ff694fa56bed62959b41e1f9"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e544246b859f17373bed915182ab841b80849ed9cf23f1f07b73b7c58baee5fb"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c0877239307b7e69d025b73774e88e86ce82f6ba6adf98f41069d5b0b78bd1bf"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:53df009d1e1ba40f696f8995683e067e3967101d4bb4ea6f667931b7d4a01357"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a1254357f7e4c82e77c348dabf2d55f1d14d19d91ff025004775e70a6ef40ada"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:524ff0ca3baea164d6d93a32c58ac79eca9f6cf713586fdc0adb66a8cdeab96a"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f0ac9fb8608dbc6eaf17956bf623c9119b4db7dbb511650910a82e261e6600f"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:320f14bd4542a04ab23747ff2c8a778bde727158b606e2661349557f0770711e"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:63974d168b6233b4ed6a0046296803cb13c56637a7b8106564ab575926572a55"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:417243bf599ba1f1fef2bb8c543ceb918676954734e2dcb82bf162ae9d7bd514"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:dda81e5ec82485155a19d9624cfcca9be88a405e2857354e5b089c2a982144b2"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:14cfbb00959259e15d684505263d5a21732b31248a5dd4941f73a3be233865b9"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:631cb7415225954fdcc2a024119101946793e5923f6c4d73a5914d27eb3d3a05"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:bec7dd208a4182e99c5b6c501ce0b1f49de2802448d4056091f8e630b28e9a52"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:149b8a07712f45b332faee1a2258d8ef1fb4a36f88c0c17cb687f205c5dc6e7d"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d966c47f9dd73c2d32a809d2be529112d509321c5310ebf54076812e6ecd884"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7eb037106f5c6b3b0b864ad226b0b7ab58157124161d48e4b30c4a43fef8bc4b"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:154ea7c52e32dce13065dbb20a4a6f0cc012b4f667ac90d648d36b12007fa9f7"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e562617a45b5a9da5be4abe72b971d4f00bf8555eb29bb91ec2ef2be348cd132"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:f23b55eb5464468f9e0e9a9935ce3ed2a870608d5f534025cd5536bca25b1402"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:e9121b4009339b0f751955baf4543a0bfd6bc3f8188f8056b1a25a2d45099934"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:0523aeb76e03f753b58be33b26540880bac5aa54422e4462404c432230543f33"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e0e2959ef5d5b8dc9ef21e1a305a21a36e254e6a34432d00c72a92fdc5ecda5"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da01bec0a26befab4898ed83b362993c844b9a607a86add78604186297eb047e"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f2e9072d71c1f6cfc79a36d4484c82823c560e6f5599c43c1ca6b5cdbd54f881"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f36a3489d9e28fe4b67be9992a23029c3cec0babc3bd9afb39f49844a8c721c5"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f64f82cc3443149292b32387086d02a6c7fb39b8781563e0ca7b8d7d9cf72bd7"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b4a6db486ac8e99ae696e09efc8b2b9fea67b63c8f88ba7a1a16c24a057a0776"}, + {file = "pydantic_core-2.10.1.tar.gz", hash = "sha256:0f8682dbdd2f67f8e1edddcbffcc29f60a6182b4901c367fc8c1c40d30bb0a82"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "pydantic-extra-types" +version = "2.1.0" +description = "Extra Pydantic types." +optional = false +python-versions = ">=3.7" +files = [] +develop = false + +[package.dependencies] +pydantic = ">=2.0.3" + +[package.extras] +all = ["phonenumbers (>=8,<9)", "pycountry (>=22,<23)", "python-ulid (>=1,<2)"] + +[package.source] +type = "git" +url = "https://github.com/pydantic/pydantic-extra-types" +reference = "a973b79" +resolved_reference = "a973b7942112df731e2618336e55e3343a2e1c32" + +[[package]] +name = "pydantic-settings" +version = "2.0.3" +description = "Settings management using Pydantic" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic_settings-2.0.3-py3-none-any.whl", hash = "sha256:ddd907b066622bd67603b75e2ff791875540dc485b7307c4fffc015719da8625"}, + {file = "pydantic_settings-2.0.3.tar.gz", hash = "sha256:962dc3672495aad6ae96a4390fac7e593591e144625e5112d359f8f67fb75945"}, +] + +[package.dependencies] +pydantic = ">=2.0.1" +python-dotenv = ">=0.21.0" + +[[package]] +name = "pytest" +version = "7.4.3" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, + {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "0.21.1" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-asyncio-0.21.1.tar.gz", hash = "sha256:40a7eae6dded22c7b604986855ea48400ab15b069ae38116e8c01238e9eeb64d"}, + {file = "pytest_asyncio-0.21.1-py3-none-any.whl", hash = "sha256:8666c1c8ac02631d7c51ba282e0c69a8a452b211ffedf2599099845da5c5c37b"}, +] + +[package.dependencies] +pytest = ">=7.0.0" + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] + +[[package]] +name = "pytest-cov" +version = "4.1.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, + {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] + +[[package]] +name = "pytest-httpx" +version = "0.26.0" +description = "Send responses to httpx." +optional = false +python-versions = ">=3.9" +files = [ + {file = "pytest_httpx-0.26.0-py3-none-any.whl", hash = "sha256:ca372b94c569c0aca2f06240f6f78cc223dfbc3ab97b5700d4e14c9a73eab17a"}, + {file = "pytest_httpx-0.26.0.tar.gz", hash = "sha256:b489c5a7bb847551943eaee601bc35053b35dc4f5961c944305120f14a1d770a"}, +] + +[package.dependencies] +httpx = "==0.25.*" +pytest = "==7.*" + +[package.extras] +testing = ["pytest-asyncio (==0.21.*)", "pytest-cov (==4.*)"] + +[[package]] +name = "python-dotenv" +version = "1.0.0" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.8" +files = [ + {file = "python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba"}, + {file = "python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "sniffio" +version = "1.3.0" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, + {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, +] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "typing-extensions" +version = "4.8.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, + {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, +] + +[metadata] +lock-version = "2.0" +python-versions = ">=3.9" +content-hash = "3e0c8eab642e816d0d04232b312f53cbe40f4524affdeeb10a25722bbc10ff25" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a9e1563 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,45 @@ +[tool.poetry] +name = "awtrix-light-client" +version = "0" +description = "awtrix-light HTTP client to be used with Ulanzi clock" +authors = ["Ludovic Ortega "] +license = "CeCILL" +readme = "README.md" +homepage = "https://github.com/M0NsTeRRR/awtrix-light-client" +repository = "https://github.com/M0NsTeRRR/awtrix-light-client" +keywords = ["awtrix-light", "awtrix"] +classifiers = [ + "Programming Language :: Python", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Operating System :: OS Independent", + "License :: OSI Approved :: CEA CNRS Inria Logiciel Libre License, version 2.1 (CeCILL-2.1)", + "Topic :: Utilities", +] +include = [ + "LICENSE", + "LICENSE.fr", +] + +[tool.poetry.dependencies] +python = ">=3.9" +httpx = "~0.25.0" +pydantic = "~2.4.2" +pydantic-settings = "~2.0.3" +pydantic-extra-types = "~2.1.0" + +[tool.poetry.group.dev.dependencies] +black = "~23.10.1" +pytest = "~7.4.3" +pytest-cov = "~4.1.0" +pytest-asyncio = "~0.21.1" +pytest-httpx = "~0.26.0" + +[tool.pytest.ini_options] +asyncio_mode = "auto" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..15f648b --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,16 @@ +from contextlib import asynccontextmanager +from typing import AsyncIterator +import pytest + +from awtrix_light_client.http_client import ( + AwtrixLightHttpClient, + get_awtrix_http_client, +) + + +@pytest.fixture +@asynccontextmanager +async def awtrix_http_client(monkeypatch) -> AsyncIterator[AwtrixLightHttpClient]: + monkeypatch.setenv("AWTRIX_HTTP_CLIENT_AWTRIX", '{"base_url": "http://test/"}') + async with get_awtrix_http_client() as client: + yield client diff --git a/tests/test_http_client.py b/tests/test_http_client.py new file mode 100644 index 0000000..8e8dd4c --- /dev/null +++ b/tests/test_http_client.py @@ -0,0 +1,1422 @@ +from typing import AsyncIterator + +import pytest +from pydantic import ValidationError +from pydantic_extra_types.color import Color +from pytest_httpx import HTTPXMock + +from awtrix_light_client.http_client import AwtrixLightHttpClient +from awtrix_light_client.models.stat import Stats +from awtrix_light_client.models.effect import EffectSetting, EffectType, Palette +from awtrix_light_client.models.transition import TransitionType +from awtrix_light_client.models.loop import Loop +from awtrix_light_client.models.screen import Screen +from awtrix_light_client.models.moodlight import Moodlight +from awtrix_light_client.models.setting import Settings +from awtrix_light_client.models.application import ( + CustomApplication, + Notification, + Fragment, + TextCase, + PushIcon, + LifeTimeMode, + Dp, + Dl, + Dr, + Df, + Dc, + Dfc, + Dt, + Db, +) + +BASE_URL = "http://test/api/" + + +async def test_get_stats( + awtrix_http_client: AsyncIterator[AwtrixLightHttpClient], httpx_mock: HTTPXMock +): + httpx_mock.add_response( + method="GET", + url=f"{BASE_URL}stats", + json={ + "bat": 52, + "bat_raw": 574, + "type": 0, + "lux": 0, + "ldr_raw": 79, + "ram": 144524, + "bri": 120, + "temp": 26, + "hum": 45, + "uptime": 461, + "wifi_signal": -53, + "messages": 0, + "version": "0.90", + "indicator1": False, + "indicator2": False, + "indicator3": False, + "app": "Time", + "uid": "awtrix_fa9b04", + "matrix": True, + }, + ) + + async with awtrix_http_client as client: + assert await client.get_stats() == Stats( + bat=52, + bat_raw=574, + type=0, + lux=0, + ldr_raw=79, + ram=144524, + bri=120, + temp=26, + hum=45, + uptime=461, + wifi_signal=-53, + messages=0, + version="0.90", + indicator1=False, + indicator2=False, + indicator3=False, + app="Time", + uid="awtrix_fa9b04", + matrix=True, + ) + + +async def test_get_effects( + awtrix_http_client: AsyncIterator[AwtrixLightHttpClient], httpx_mock: HTTPXMock +): + httpx_mock.add_response( + method="GET", + url=f"{BASE_URL}effects", + json=[ + "Fade", + "MovingLine", + "BrickBreaker", + "PingPong", + "Radar", + "Checkerboard", + "Fireworks", + "PlasmaCloud", + "Ripple", + "Snake", + "Pacifica", + "TheaterChase", + "Plasma", + "Matrix", + "SwirlIn", + "SwirlOut", + "LookingEyes", + "TwinklingStars", + "ColorWaves", + ], + ) + + async with awtrix_http_client as client: + assert await client.get_effects() == [ + EffectType("Fade"), + EffectType("MovingLine"), + EffectType("BrickBreaker"), + EffectType("PingPong"), + EffectType("Radar"), + EffectType("Checkerboard"), + EffectType("Fireworks"), + EffectType("PlasmaCloud"), + EffectType("Ripple"), + EffectType("Snake"), + EffectType("Pacifica"), + EffectType("TheaterChase"), + EffectType("Plasma"), + EffectType("Matrix"), + EffectType("SwirlIn"), + EffectType("SwirlOut"), + EffectType("LookingEyes"), + EffectType("TwinklingStars"), + EffectType("ColorWaves"), + ] + + +async def test_get_transitions( + awtrix_http_client: AsyncIterator[AwtrixLightHttpClient], httpx_mock: HTTPXMock +): + httpx_mock.add_response( + method="GET", + url=f"{BASE_URL}transitions", + json=[ + "Random", + "Slide", + "Dim", + "Zoom", + "Rotate", + "Pixelate", + "Curtain", + "Ripple", + "Blink", + "Reload", + "Fade", + ], + ) + + async with awtrix_http_client as client: + assert await client.get_transitions() == [ + TransitionType["RANDOM"], + TransitionType["SLIDE"], + TransitionType["DIM"], + TransitionType["ZOOM"], + TransitionType["ROTATE"], + TransitionType["PIXELATE"], + TransitionType["CURTAIN"], + TransitionType["RIPPLE"], + TransitionType["BLINK"], + TransitionType["RELOAD"], + TransitionType["FADE"], + ] + + +async def test_get_loops( + awtrix_http_client: AsyncIterator[AwtrixLightHttpClient], httpx_mock: HTTPXMock +): + httpx_mock.add_response( + method="GET", + url=f"{BASE_URL}loop", + json={"Time": 0, "Temperature": 1, "Humidity": 2, "Battery": 3}, + ) + + async with awtrix_http_client as client: + assert await client.get_loops() == Loop( + loops=[ + "Time", + "Temperature", + "Humidity", + "Battery", + ] + ) + + +async def test_get_screen( + awtrix_http_client: AsyncIterator[AwtrixLightHttpClient], httpx_mock: HTTPXMock +): + httpx_mock.add_response( + method="GET", + url=f"{BASE_URL}screen", + json=[ + 0, + 0, + 65280, + 65280, + 65280, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 65280, + 65280, + 77826, + 65280, + 65280, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 16777215, + 0, + 16777215, + 0, + 16777215, + 16777215, + 16777215, + 0, + 16777215, + 0, + 16777215, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 65280, + 77826, + 77826, + 77826, + 65280, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 16777215, + 0, + 16777215, + 0, + 0, + 0, + 16777215, + 0, + 0, + 0, + 16777215, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 65280, + 77826, + 77826, + 77826, + 65280, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 16777215, + 16777215, + 16777215, + 0, + 16777215, + 16777215, + 16777215, + 0, + 0, + 16777215, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 65280, + 77826, + 77826, + 77826, + 65280, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 16777215, + 0, + 0, + 0, + 16777215, + 0, + 16777215, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 65280, + 77826, + 77826, + 77826, + 65280, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 16777215, + 0, + 16777215, + 16777215, + 16777215, + 0, + 16777215, + 0, + 16777215, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 65280, + 77826, + 77826, + 77826, + 65280, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 263168, + 65280, + 65280, + 65280, + 65280, + 65280, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + ) + + async with awtrix_http_client as client: + assert await client.get_screen() == Screen( + matrix=[ + 0, + 0, + 65280, + 65280, + 65280, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 65280, + 65280, + 77826, + 65280, + 65280, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 16777215, + 0, + 16777215, + 0, + 16777215, + 16777215, + 16777215, + 0, + 16777215, + 0, + 16777215, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 65280, + 77826, + 77826, + 77826, + 65280, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 16777215, + 0, + 16777215, + 0, + 0, + 0, + 16777215, + 0, + 0, + 0, + 16777215, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 65280, + 77826, + 77826, + 77826, + 65280, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 16777215, + 16777215, + 16777215, + 0, + 16777215, + 16777215, + 16777215, + 0, + 0, + 16777215, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 65280, + 77826, + 77826, + 77826, + 65280, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 16777215, + 0, + 0, + 0, + 16777215, + 0, + 16777215, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 65280, + 77826, + 77826, + 77826, + 65280, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 16777215, + 0, + 16777215, + 16777215, + 16777215, + 0, + 16777215, + 0, + 16777215, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 65280, + 77826, + 77826, + 77826, + 65280, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 263168, + 65280, + 65280, + 65280, + 65280, + 65280, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ] + ) + + +async def test_set_power( + awtrix_http_client: AsyncIterator[AwtrixLightHttpClient], httpx_mock: HTTPXMock +): + httpx_mock.add_response( + method="POST", + url=f"{BASE_URL}power", + match_json={"power": False}, + ) + + async with awtrix_http_client as client: + assert await client.set_power(False) == None + + +async def test_set_sleep( + awtrix_http_client: AsyncIterator[AwtrixLightHttpClient], httpx_mock: HTTPXMock +): + httpx_mock.add_response( + method="POST", + url=f"{BASE_URL}sleep", + match_json={"sleep": 10}, + ) + + async with awtrix_http_client as client: + assert await client.set_sleep(10) == None + + +async def test_set_sound( + awtrix_http_client: AsyncIterator[AwtrixLightHttpClient], httpx_mock: HTTPXMock +): + httpx_mock.add_response( + method="POST", + url=f"{BASE_URL}sound", + match_json={"sound": "alarm"}, + ) + + async with awtrix_http_client as client: + assert await client.set_sound("alarm") == None + + +async def test_set_rtttl( + awtrix_http_client: AsyncIterator[AwtrixLightHttpClient], httpx_mock: HTTPXMock +): + httpx_mock.add_response( + method="POST", + url=f"{BASE_URL}rtttl", + match_json={ + "rtttl": "JingleBell:d=8,o=5,b=112:32p,a,a,4a,a,a,4a,a,c6,f.,16g,2a,a#,a#,a#.,16a#,a#,a,a.,16a,a,g,g,a,4g,4c6" + }, + ) + + async with awtrix_http_client as client: + assert ( + await client.set_rtttl( + "JingleBell:d=8,o=5,b=112:32p,a,a,4a,a,a,4a,a,c6,f.,16g,2a,a#,a#,a#.,16a#,a#,a,a.,16a,a,g,g,a,4g,4c6" + ) + == None + ) + + +async def test_wrong_moodlight(): + with pytest.raises(ValidationError, match="kelvin and color can't be set together"): + Moodlight(brightness=170, kelvin=2300, color=Color("#FF00FF")) + + +async def test_set_moodlight( + awtrix_http_client: AsyncIterator[AwtrixLightHttpClient], httpx_mock: HTTPXMock +): + httpx_mock.add_response( + method="POST", + url=f"{BASE_URL}moodlight", + match_json={"brightness": 170, "color": "#FF00FF"}, + ) + + async with awtrix_http_client as client: + assert ( + await client.set_moodlight( + Moodlight(brightness=170, color=Color("#FF00FF")) + ) + == None + ) + + +async def test_wrong_indicator_param( + awtrix_http_client: AsyncIterator[AwtrixLightHttpClient], +): + with pytest.raises(ValueError, match="fade and blink can't be set together"): + async with awtrix_http_client as client: + assert ( + await client.set_indicator(2, color=Color("#FF00FF"), fade=2, blink=2) + == None + ) + + +async def test_set_indicator( + awtrix_http_client: AsyncIterator[AwtrixLightHttpClient], httpx_mock: HTTPXMock +): + httpx_mock.add_response( + method="POST", + url=f"{BASE_URL}indicator2", + match_json={"color": "#FF00FF", "fade": 2}, + ) + + async with awtrix_http_client as client: + assert await client.set_indicator(2, color=Color("#FF00FF"), fade=2) == None + + +async def test_set_one_custom_application( + awtrix_http_client: AsyncIterator[AwtrixLightHttpClient], httpx_mock: HTTPXMock +): + httpx_mock.add_response( + method="POST", + url=f"{BASE_URL}custom?name=test", + match_json={ + "text": [{"t": "test", "c": "#0000FF"}], + "textCase": 0, + "topText": True, + "textOffset": 2, + "center": True, + "color": "#0000FF", + "gradient": ["#0000FF", "#0000FF"], + "background": "#0000FF", + "rainbow": True, + "icon": "4300", + "pushIcon": 0, + "repeat": 50, + "duration": 50, + "bar": [12], + "line": [12], + "autoscale": True, + "progress": 50, + "progressC": "#0000FF", + "progressBC": "#0000FF", + "pos": 2, + "draw": [ + {"x": 0, "y": 0, "cl": "#0000FF"}, + {"x0": 1, "y0": 1, "x1": 1, "y1": 1, "cl": "#0000FF"}, + {"x": 2, "y": 2, "w": 2, "h": 2, "cl": "#0000FF"}, + {"x": 3, "y": 3, "w": 3, "h": 3, "cl": "#0000FF"}, + {"x": 4, "y": 4, "r": 4, "cl": "#0000FF"}, + {"x": 5, "y": 5, "r": 2, "cl": "#0000FF"}, + {"x": 6, "y": 6, "t": 6, "cl": "#0000FF"}, + {"x": 7, "y": 7, "r": 7, "cl": "#0000FF"}, + ], + "noScroll": True, + "scrollSpeed": 50, + "effect": "Fade", + "effectSettings": {"speed": 1, "palette": "Lava", "blend": True}, + "lifetime": 2, + "lifetimeMode": 1, + "save": True, + }, + ) + + async with awtrix_http_client as client: + assert ( + await client.set_custom_application( + "test", + CustomApplication( + text=[Fragment(t="test", c=Color("blue"))], + textCase=TextCase.GLOBAL, + topText=True, + textOffset=2, + center=True, + color=Color("blue"), + gradient=[Color("blue"), Color("blue")], + background=Color("blue"), + rainbow=True, + icon="4300", + pushIcon=PushIcon.NOT_MOVING, + repeat=50, + duration=50, + bar=[12], + line=[12], + autoscale=True, + progress=50, + progressC=Color("blue"), + progressBC=Color("blue"), + pos=2, + draw=[ + Dp(x=0, y=0, cl=Color("blue")), + Dl(x0=1, y0=1, x1=1, y1=1, cl=Color("blue")), + Dr(x=2, y=2, w=2, h=2, cl=Color("blue")), + Df(x=3, y=3, w=3, h=3, cl=Color("blue")), + Dc(x=4, y=4, r=4, cl=Color("blue")), + Dfc(x=5, y=5, r=2, cl=Color("blue")), + Dt(x=6, y=6, t=6, cl=Color("blue")), + Db(x=7, y=7, r=7, cl=Color("blue")), + ], + lifetime=2, + lifetimeMode=LifeTimeMode.STALE, + noScroll=True, + scrollSpeed=50, + effect=EffectType.FADE, + effectSettings=EffectSetting( + speed=1, palette=Palette.LAVA, blend=True + ), + save=True, + ), + ) + == None + ) + + +async def test_set_multiple_custom_application( + awtrix_http_client: AsyncIterator[AwtrixLightHttpClient], httpx_mock: HTTPXMock +): + httpx_mock.add_response( + method="POST", + url=f"{BASE_URL}custom?name=test", + match_json=[ + { + "text": [{"t": "test", "c": "#0000FF"}], + "textCase": 0, + "topText": True, + "textOffset": 2, + "center": True, + "color": "#0000FF", + "gradient": ["#0000FF", "#0000FF"], + "background": "#0000FF", + "rainbow": True, + "icon": "4300", + "pushIcon": 0, + "repeat": 50, + "duration": 50, + "bar": [12], + "line": [12], + "autoscale": True, + "progress": 50, + "progressC": "#0000FF", + "progressBC": "#0000FF", + "draw": [ + {"x": 0, "y": 0, "cl": "#0000FF"}, + {"x0": 1, "y0": 1, "x1": 1, "y1": 1, "cl": "#0000FF"}, + {"x": 2, "y": 2, "w": 2, "h": 2, "cl": "#0000FF"}, + {"x": 3, "y": 3, "w": 3, "h": 3, "cl": "#0000FF"}, + {"x": 4, "y": 4, "r": 4, "cl": "#0000FF"}, + {"x": 5, "y": 5, "r": 2, "cl": "#0000FF"}, + {"x": 6, "y": 6, "t": 6, "cl": "#0000FF"}, + {"x": 7, "y": 7, "r": 7, "cl": "#0000FF"}, + ], + "noScroll": True, + "scrollSpeed": 50, + "effect": "Fade", + "effectSettings": {"speed": 1, "palette": "Lava", "blend": True}, + "pos": 2, + "lifetime": 2, + "lifetimeMode": 1, + "save": True, + } + ], + ) + + async with awtrix_http_client as client: + assert ( + await client.set_custom_application( + "test", + [ + CustomApplication( + text=[Fragment(t="test", c=Color("blue"))], + textCase=TextCase.GLOBAL, + topText=True, + textOffset=2, + center=True, + color=Color("blue"), + gradient=[Color("blue"), Color("blue")], + background=Color("blue"), + rainbow=True, + icon="4300", + pushIcon=PushIcon.NOT_MOVING, + repeat=50, + duration=50, + bar=[12], + line=[12], + autoscale=True, + progress=50, + progressC=Color("blue"), + progressBC=Color("blue"), + pos=2, + draw=[ + Dp(x=0, y=0, cl=Color("blue")), + Dl(x0=1, y0=1, x1=1, y1=1, cl=Color("blue")), + Dr(x=2, y=2, w=2, h=2, cl=Color("blue")), + Df(x=3, y=3, w=3, h=3, cl=Color("blue")), + Dc(x=4, y=4, r=4, cl=Color("blue")), + Dfc(x=5, y=5, r=2, cl=Color("blue")), + Dt(x=6, y=6, t=6, cl=Color("blue")), + Db(x=7, y=7, r=7, cl=Color("blue")), + ], + lifetime=2, + lifetimeMode=LifeTimeMode.STALE, + noScroll=True, + scrollSpeed=50, + effect=EffectType.FADE, + effectSettings=EffectSetting( + speed=1, palette=Palette.LAVA, blend=True + ), + save=True, + ) + ], + ) + == None + ) + + +async def test_notify( + awtrix_http_client: AsyncIterator[AwtrixLightHttpClient], httpx_mock: HTTPXMock +): + httpx_mock.add_response( + method="POST", + url=f"{BASE_URL}notify", + match_json={ + "text": [{"t": "test", "c": "#0000FF"}], + "textCase": 0, + "topText": True, + "textOffset": 2, + "center": True, + "color": "#0000FF", + "gradient": ["#0000FF", "#0000FF"], + "background": "#0000FF", + "rainbow": True, + "icon": "4300", + "pushIcon": 0, + "repeat": 50, + "duration": 50, + "bar": [12], + "line": [12], + "autoscale": True, + "progress": 50, + "progressC": "#0000FF", + "progressBC": "#0000FF", + "draw": [ + {"x": 0, "y": 0, "cl": "#0000FF"}, + {"x0": 1, "y0": 1, "x1": 1, "y1": 1, "cl": "#0000FF"}, + {"x": 2, "y": 2, "w": 2, "h": 2, "cl": "#0000FF"}, + {"x": 3, "y": 3, "w": 3, "h": 3, "cl": "#0000FF"}, + {"x": 4, "y": 4, "r": 4, "cl": "#0000FF"}, + {"x": 5, "y": 5, "r": 2, "cl": "#0000FF"}, + {"x": 6, "y": 6, "t": 6, "cl": "#0000FF"}, + {"x": 7, "y": 7, "r": 7, "cl": "#0000FF"}, + ], + "noScroll": True, + "scrollSpeed": 50, + "effect": "Fade", + "effectSettings": {"speed": 1, "palette": "Lava", "blend": True}, + "hold": True, + "sound": "test", + "loopSound": True, + "stack": True, + "wakeup": True, + "clients": ["http://test.fr/"], + }, + ) + + async with awtrix_http_client as client: + assert ( + await client.notify( + Notification( + text=[Fragment(t="test", c=Color("blue"))], + textCase=TextCase.GLOBAL, + topText=True, + textOffset=2, + center=True, + color=Color("blue"), + gradient=[Color("blue"), Color("blue")], + background=Color("blue"), + rainbow=True, + icon="4300", + pushIcon=PushIcon.NOT_MOVING, + repeat=50, + duration=50, + bar=[12], + line=[12], + autoscale=True, + progress=50, + progressC=Color("blue"), + progressBC=Color("blue"), + draw=[ + Dp(x=0, y=0, cl=Color("blue")), + Dl(x0=1, y0=1, x1=1, y1=1, cl=Color("blue")), + Dr(x=2, y=2, w=2, h=2, cl=Color("blue")), + Df(x=3, y=3, w=3, h=3, cl=Color("blue")), + Dc(x=4, y=4, r=4, cl=Color("blue")), + Dfc(x=5, y=5, r=2, cl=Color("blue")), + Dt(x=6, y=6, t=6, cl=Color("blue")), + Db(x=7, y=7, r=7, cl=Color("blue")), + ], + noScroll=True, + scrollSpeed=50, + effect=EffectType.FADE, + effectSettings=EffectSetting( + speed=1, palette=Palette.LAVA, blend=True + ), + hold=True, + sound="test", + loopSound=True, + stack=True, + wakeup=True, + clients=["http://test.fr"], + ) + ) + == None + ) + + +async def test_dismiss_notification( + awtrix_http_client: AsyncIterator[AwtrixLightHttpClient], httpx_mock: HTTPXMock +): + httpx_mock.add_response( + method="POST", + url=f"{BASE_URL}notify/dismiss", + ) + + async with awtrix_http_client as client: + assert await client.dismiss_notification() == None + + +async def test_next_app( + awtrix_http_client: AsyncIterator[AwtrixLightHttpClient], httpx_mock: HTTPXMock +): + httpx_mock.add_response( + method="POST", + url=f"{BASE_URL}nextapp", + ) + + async with awtrix_http_client as client: + assert await client.next_app() == None + + +async def test_previous_app( + awtrix_http_client: AsyncIterator[AwtrixLightHttpClient], httpx_mock: HTTPXMock +): + httpx_mock.add_response( + method="POST", + url=f"{BASE_URL}previousapp", + ) + + async with awtrix_http_client as client: + assert await client.previous_app() == None + + +async def test_switch_app( + awtrix_http_client: AsyncIterator[AwtrixLightHttpClient], httpx_mock: HTTPXMock +): + httpx_mock.add_response( + method="POST", + url=f"{BASE_URL}switch", + match_json={"name": "time"}, + ) + + async with awtrix_http_client as client: + assert await client.switch_app("time") == None + + +async def test_get_settings( + awtrix_http_client: AsyncIterator[AwtrixLightHttpClient], httpx_mock: HTTPXMock +): + httpx_mock.add_response( + method="GET", + url=f"{BASE_URL}settings", + json={ + "MATP": True, + "ABRI": False, + "BRI": 120, + "ATRANS": True, + "TCOL": 16777215, + "TEFF": 1, + "TSPEED": 400, + "ATIME": 7, + "TMODE": 1, + "CHCOL": 16711680, + "CTCOL": 0, + "CBCOL": 16777215, + "TFORMAT": "%H %M", + "DFORMAT": "%d.%m.%y", + "SOM": True, + "CEL": True, + "BLOCKN": False, + "MAT": 0, + "SOUND": True, + "GAMMA": 1.899999976, + "UPPERCASE": True, + "CCORRECTION": "#000000", + "CTEMP": "#000000", + "WD": True, + "WDCA": 16777215, + "WDCI": 6710886, + "TIME_COL": 0, + "DATE_COL": 0, + "HUM_COL": 0, + "TEMP_COL": 0, + "BAT_COL": 0, + "SSPEED": 100, + "TIM": True, + "DAT": False, + "HUM": True, + "TEMP": True, + "BAT": True, + }, + ) + + async with awtrix_http_client as client: + assert await client.get_settings() == Settings( + ATIME=7, + TEFF=TransitionType.SLIDE, + TSPEED=400, + TCOL=Color("#FFFFFF"), + TMODE=1, + CHCOL=Color("#FF0000"), + CBCOL=Color("#FFFFFF"), + CTCOL=0, + WD=True, + WDCA=Color("#FFFFFF"), + WDCI=Color("#666666"), + BRI=120, + ABRI=False, + ATRANS=True, + CCORRECTION=Color("#000000"), + CTEMP=Color("#000000"), + TFORMAT="%H %M", + DFORMAT="%d.%m.%y", + SOM=True, + CEL=True, + MAT=0, + SOUND=True, + GAMMA=1.899999976, + BLOCKN=False, + UPPERCASE=True, + TIME_COL=0, + DATE_COL=0, + TEMP_COL=0, + HUM_COL=0, + BAT_COL=0, + SSPEED=100, + TIM=True, + DAT=False, + HUM=True, + TEMP=True, + BAT=True, + MATP=True, + ) + + +async def test_set_settings_all_params_optionnal( + awtrix_http_client: AsyncIterator[AwtrixLightHttpClient], httpx_mock: HTTPXMock +): + httpx_mock.add_response( + method="POST", + url=f"{BASE_URL}notify", + match_json={}, + ) + + async with awtrix_http_client as client: + assert await client.notify(Notification()) == None + + +async def test_set_settings( + awtrix_http_client: AsyncIterator[AwtrixLightHttpClient], httpx_mock: HTTPXMock +): + httpx_mock.add_response( + method="POST", + url=f"{BASE_URL}settings", + match_json={ + "MATP": True, + "ABRI": False, + "BRI": 120, + "ATRANS": True, + "TCOL": 16777215, + "TEFF": 1, + "TSPEED": 400, + "ATIME": 7, + "TMODE": 1, + "CHCOL": 16711680, + "CTCOL": 0, + "CBCOL": 16777215, + "TFORMAT": "%H %M", + "DFORMAT": "%d.%m.%y", + "SOM": True, + "CEL": True, + "BLOCKN": False, + "MAT": 0, + "SOUND": True, + "GAMMA": 1.899999976, + "UPPERCASE": True, + "CCORRECTION": "#000000", + "CTEMP": "#000000", + "WD": True, + "WDCA": 16777215, + "WDCI": 6710886, + "TIME_COL": 0, + "DATE_COL": 0, + "HUM_COL": 0, + "TEMP_COL": 0, + "BAT_COL": 0, + "SSPEED": 100, + "TIM": True, + "DAT": False, + "HUM": True, + "TEMP": True, + "BAT": True, + }, + ) + + async with awtrix_http_client as client: + assert ( + await client.set_settings( + Settings( + ATIME=7, + TEFF=TransitionType.SLIDE, + TSPEED=400, + TCOL=Color("#FFFFFF"), + TMODE=1, + CHCOL=Color("#FF0000"), + CBCOL=Color("#FFFFFF"), + CTCOL=0, + WD=True, + WDCA=Color("#FFFFFF"), + WDCI=Color("#666666"), + BRI=120, + ABRI=False, + ATRANS=True, + CCORRECTION=Color("#000000"), + CTEMP=Color("#000000"), + TFORMAT="%H %M", + DFORMAT="%d.%m.%y", + SOM=True, + CEL=True, + MAT=0, + SOUND=True, + GAMMA=1.899999976, + BLOCKN=False, + UPPERCASE=True, + TIME_COL=0, + DATE_COL=0, + TEMP_COL=0, + HUM_COL=0, + BAT_COL=0, + SSPEED=100, + TIM=True, + DAT=False, + HUM=True, + TEMP=True, + BAT=True, + MATP=True, + ) + ) + == None + ) + + +async def test_update( + awtrix_http_client: AsyncIterator[AwtrixLightHttpClient], httpx_mock: HTTPXMock +): + httpx_mock.add_response( + method="POST", + url=f"{BASE_URL}doupdate", + ) + + async with awtrix_http_client as client: + assert await client.update() == None + + +async def test_reboot( + awtrix_http_client: AsyncIterator[AwtrixLightHttpClient], httpx_mock: HTTPXMock +): + httpx_mock.add_response( + method="POST", + url=f"{BASE_URL}reboot", + ) + + async with awtrix_http_client as client: + assert await client.reboot() == None + + +async def test_erase( + awtrix_http_client: AsyncIterator[AwtrixLightHttpClient], httpx_mock: HTTPXMock +): + httpx_mock.add_response( + method="POST", + url=f"{BASE_URL}erase", + ) + + async with awtrix_http_client as client: + assert await client.erase() == None + + +async def test_reset_settings( + awtrix_http_client: AsyncIterator[AwtrixLightHttpClient], httpx_mock: HTTPXMock +): + httpx_mock.add_response( + method="POST", + url=f"{BASE_URL}resetSettings", + ) + + async with awtrix_http_client as client: + assert await client.reset_settings() == None diff --git a/tests/test_http_settings.py b/tests/test_http_settings.py new file mode 100644 index 0000000..32874a4 --- /dev/null +++ b/tests/test_http_settings.py @@ -0,0 +1,31 @@ +from pydantic import ValidationError +import pytest + +from awtrix_light_client.http_settings import AwtrixLightHttpClientSettings + + +async def test_no_settings(): + with pytest.raises(ValidationError): + AwtrixLightHttpClientSettings() + + +async def test_wrong_url(monkeypatch): + monkeypatch.setenv("AWTRIX_HTTP_CLIENT_AWTRIX", '{"base_url": "test"}') + + with pytest.raises(ValidationError): + AwtrixLightHttpClientSettings() + + +async def test_works_no_auth(monkeypatch): + monkeypatch.setenv("AWTRIX_HTTP_CLIENT_AWTRIX", '{"base_url": "http://test.fr"}') + + AwtrixLightHttpClientSettings() + + +async def test_works_auth(monkeypatch): + monkeypatch.setenv( + "AWTRIX_HTTP_CLIENT_AWTRIX", + '{"base_url": "http://test.fr", "auth": "username", "password": "password"}', + ) + + AwtrixLightHttpClientSettings()