├── .github └── workflows │ ├── develop.yaml │ ├── main.yaml │ ├── release.yaml │ └── test.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── docs └── index.md ├── examples ├── fastapi │ ├── README.md │ ├── app.py │ ├── poetry.lock │ └── pyproject.toml └── flask │ ├── README.md │ ├── app.py │ ├── poetry.lock │ └── pyproject.toml ├── mkdocs.yml ├── poetry.lock ├── pyproject.toml ├── solana_actions ├── __init__.py ├── action_identity.py ├── constants.py ├── create_post_response.py ├── encode_url.py ├── fetch_transaction.py ├── find_reference.py ├── parse_url.py └── types.py └── tests ├── conftest.py ├── test_create_post_response.py ├── test_encode_url.py ├── test_fetch_transaction.py ├── test_find_transaction_signature.py └── test_parse_url.py /.github/workflows/develop.yaml: -------------------------------------------------------------------------------- 1 | name: Test, build and deploy for Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - develop 7 | 8 | jobs: 9 | test: 10 | uses: ./.github/workflows/test.yaml 11 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: Test, release and publish 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | test: 10 | uses: ./.github/workflows/test.yaml 11 | release: 12 | uses: ./.github/workflows/release.yaml 13 | needs: test 14 | secrets: inherit 15 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Semantic Release 2 | 3 | on: 4 | workflow_call: 5 | 6 | jobs: 7 | release: 8 | runs-on: ubuntu-latest 9 | concurrency: release 10 | permissions: 11 | id-token: write 12 | contents: write 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Release 20 | uses: python-semantic-release/python-semantic-release@master 21 | with: 22 | github_token: ${{ secrets.GITHUB_TOKEN }} 23 | 24 | - name: Build and publish to pypi 25 | uses: JRubics/poetry-publish@v2.0 26 | with: 27 | pypi_token: ${{ secrets.PYPI_TOKEN }} 28 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Test 3 | 4 | on: 5 | workflow_call: 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | 13 | # If you wanted to use multiple Python versions, you'd have specify a matrix in the job and 14 | # reference the matrixe python version here. 15 | - uses: actions/setup-python@v5 16 | with: 17 | python-version: 3.11 18 | 19 | # Cache the installation of Poetry itself, e.g. the next step. This prevents the workflow 20 | # from installing Poetry every time, which can be slow. Note the use of the Poetry version 21 | # number in the cache key, and the "-0" suffix: this allows you to invalidate the cache 22 | # manually if/when you want to upgrade Poetry, or if something goes wrong. This could be 23 | # mildly cleaner by using an environment variable, but I don't really care. 24 | - name: cache poetry install 25 | uses: actions/cache@v4 26 | with: 27 | path: ~/.local 28 | key: poetry-1.8.2 29 | 30 | # Install Poetry. You could do this manually, or there are several actions that do this. 31 | # `snok/install-poetry` seems to be minimal yet complete, and really just calls out to 32 | # Poetry's default install script, which feels correct. I pin the Poetry version here 33 | # because Poetry does occasionally change APIs between versions and I don't want my 34 | # actions to break if it does. 35 | # 36 | # The key configuration value here is `virtualenvs-in-project: true`: this creates the 37 | # venv as a `.venv` in your testing directory, which allows the next step to easily 38 | # cache it. 39 | - uses: snok/install-poetry@v1 40 | with: 41 | version: 1.8.2 42 | virtualenvs-create: true 43 | virtualenvs-in-project: true 44 | 45 | # Cache your dependencies (i.e. all the stuff in your `pyproject.toml`). Note the cache 46 | # key: if you're using multiple Python versions, or multiple OSes, you'd need to include 47 | # them in the cache key. I'm not, so it can be simple and just depend on the poetry.lock. 48 | - name: cache deps 49 | id: cache-deps 50 | uses: actions/cache@v4 51 | with: 52 | path: .venv 53 | key: pydeps-${{ hashFiles('**/poetry.lock') }} 54 | 55 | # Install dependencies. `--no-root` means "install all dependencies but not the project 56 | # itself", which is what you want to avoid caching _your_ code. The `if` statement 57 | # ensures this only runs on a cache miss. 58 | - run: poetry install --no-interaction --no-root 59 | if: steps.cache-deps.outputs.cache-hit != 'true' 60 | 61 | # Now install _your_ project. This isn't necessary for many types of projects -- particularly 62 | # things like Django apps don't need this. But it's a good idea since it fully-exercises the 63 | # pyproject.toml and makes that if you add things like console-scripts at some point that 64 | # they'll be installed and working. 65 | - run: poetry install --no-interaction 66 | 67 | # And finally run tests. I'm using pytest and all my pytest config is in my `pyproject.toml` 68 | # so this line is super-simple. But it could be as complex as you need. 69 | - run: poetry run pytest 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 110 | .pdm.toml 111 | .pdm-python 112 | .pdm-build/ 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | #.idea/ 163 | 164 | 165 | requirements.txt 166 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | repos: 3 | 4 | - repo: https://github.com/python-poetry/poetry 5 | rev: '1.8.3' 6 | hooks: 7 | - id: poetry-check 8 | - id: poetry-lock 9 | - id: poetry-export 10 | - id: poetry-install 11 | 12 | - repo: https://github.com/commitizen-tools/commitizen 13 | rev: v2.24.0 14 | hooks: 15 | - id: commitizen 16 | stages: [commit-msg] 17 | 18 | - repo: https://github.com/pre-commit/pre-commit-hooks 19 | rev: v4.2.0 20 | hooks: 21 | - id: trailing-whitespace 22 | exclude_types: 23 | - javascript 24 | - id: check-docstring-first 25 | - id: check-executables-have-shebangs 26 | - id: check-json 27 | - id: check-yaml 28 | - id: end-of-file-fixer 29 | - id: fix-encoding-pragma 30 | 31 | - repo: https://github.com/PyCQA/isort 32 | rev: 5.12.0 33 | hooks: 34 | - id: isort 35 | name: isort (python) 36 | # https://github.com/PyCQA/isort/issues/1518 37 | args: ["--profile", "black", --line-length=72] 38 | 39 | - repo: https://github.com/ambv/black 40 | rev: 22.3.0 41 | hooks: 42 | - id: black 43 | language_version: python3 44 | 45 | - repo: https://github.com/PyCQA/autoflake 46 | rev: v2.1.1 47 | hooks: 48 | - id: autoflake 49 | args: 50 | - "--in-place" 51 | - "--expand-star-imports" 52 | - "--remove-duplicate-keys" 53 | - "--remove-unused-variables" 54 | - "--remove-all-unused-imports" 55 | # Autogenerated code, ets not autoflake this 56 | - "--exclude=gordon/mash/*" 57 | 58 | - repo: https://github.com/pre-commit/mirrors-mypy 59 | rev: v0.902 60 | hooks: 61 | - id: mypy 62 | files: ^gateway/ 63 | exclude: tests 64 | additional_dependencies: [types-python-dateutil, types-requests, types-pyyaml, types-click] 65 | args: [--ignore-missing-imports] # TODO add --strict! 66 | 67 | - repo: https://github.com/PyCQA/bandit 68 | rev: '1.7.8' 69 | hooks: 70 | - id: bandit 71 | args: ["-c", "pyproject.toml"] 72 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the OS, Python version and other tools you might need 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: "3.12" 13 | # You can also specify other tool versions: 14 | # nodejs: "19" 15 | # rust: "1.64" 16 | # golang: "1.19" 17 | jobs: 18 | post_create_environment: 19 | # Install poetry 20 | # https://python-poetry.org/docs/#installing-manually 21 | - pip install poetry 22 | post_install: 23 | # Install dependencies with 'docs' dependency group 24 | # https://python-poetry.org/docs/managing-dependencies/#dependency-groups 25 | # VIRTUAL_ENV needs to be set manually for now. 26 | # See https://github.com/readthedocs/readthedocs.org/pull/11152/ 27 | - VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH poetry install --with docs 28 | 29 | # Build documentation with mkdocs 30 | mkdocs: 31 | configuration: mkdocs.yml 32 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## v0.2.1 (2024-07-29) 4 | 5 | ### Chore 6 | 7 | * chore: full links to examples on github ([`a7b473a`](https://github.com/xeroc/python-solana-actions/commit/a7b473acb7626eb2fc75c4a3053732bc2108a387)) 8 | 9 | ### Fix 10 | 11 | * fix: bump dependencies in examples #4 ([`3fc1b26`](https://github.com/xeroc/python-solana-actions/commit/3fc1b2622d04bf2ef41c3d42705ca65ce7d93a2e)) 12 | 13 | ### Unknown 14 | 15 | * Merge pull request #5 from xeroc/release/202407291307 16 | 17 | Release/202407291307 ([`af873af`](https://github.com/xeroc/python-solana-actions/commit/af873afbb9e9962d22956b00f60a0ef4aab27763)) 18 | 19 | ## v0.2.0 (2024-07-09) 20 | 21 | ### Feature 22 | 23 | * feat: major release 24 | 25 | BREAKING CHANGE ([`a3988cc`](https://github.com/xeroc/python-solana-actions/commit/a3988cc041f3b3ce69e8ce55a56b97ef3a258a2f)) 26 | 27 | ### Unknown 28 | 29 | * Merge branch 'develop' ([`9a43f97`](https://github.com/xeroc/python-solana-actions/commit/9a43f97bfe957e81de74d6300ad0961b652cbd1c)) 30 | 31 | ## v0.1.0 (2024-07-09) 32 | 33 | ### Build 34 | 35 | * build: run coverage ([`271eb0c`](https://github.com/xeroc/python-solana-actions/commit/271eb0c253bd5854628b6dcbc06944004c47ce15)) 36 | 37 | ### Chore 38 | 39 | * chore: finalize functionality ([`e838dbf`](https://github.com/xeroc/python-solana-actions/commit/e838dbf28f648b717f77c5cb71dc911343d7c420)) 40 | 41 | ### Feature 42 | 43 | * feat: implement example in fastapi ([`c49a913`](https://github.com/xeroc/python-solana-actions/commit/c49a9139303584bf40c8777c6670e53884a6751c)) 44 | 45 | * feat: initial work on the flask app ([`476d88e`](https://github.com/xeroc/python-solana-actions/commit/476d88e5d7e034bd3d83528cd58098d70a9d6e29)) 46 | 47 | ### Fix 48 | 49 | * fix(readme): proper links to gh actions ([`2868469`](https://github.com/xeroc/python-solana-actions/commit/28684696af7fb56e6a2c9239fa13a43b8bc8a15a)) 50 | 51 | * fix(ci): seems pipeline does not work with updated tags ([`bba8656`](https://github.com/xeroc/python-solana-actions/commit/bba865637098f07849e31c082be12b9b4dfae127)) 52 | 53 | ### Unknown 54 | 55 | * Merge pull request #2 from xeroc/release/202407091607 56 | 57 | Release/202407091607 ([`9261f89`](https://github.com/xeroc/python-solana-actions/commit/9261f8928156e472f6703a5328e3282b5f42ea37)) 58 | 59 | ## v0.0.3 (2024-07-01) 60 | 61 | ### Fix 62 | 63 | * fix(ci): seems pipeline does not work with updated tags ([`4c0931c`](https://github.com/xeroc/python-solana-actions/commit/4c0931c23ba7ec8f9c9816566d051f06076164bc)) 64 | 65 | ## v0.0.2 (2024-07-01) 66 | 67 | ### Fix 68 | 69 | * fix: properly update pypoetry on release ([`b86cacb`](https://github.com/xeroc/python-solana-actions/commit/b86cacbf0ebd7a335f21abe49db9e00d1dbe3939)) 70 | 71 | ### Unknown 72 | 73 | * Merge branch 'develop' ([`c9dbba5`](https://github.com/xeroc/python-solana-actions/commit/c9dbba53bd9525eae6e5c97d13ef0973c3b1b234)) 74 | 75 | ## v0.0.1 (2024-07-01) 76 | 77 | ### Chore 78 | 79 | * chore: linting ([`e4909b9`](https://github.com/xeroc/python-solana-actions/commit/e4909b9585246c3de3b36116f2e477ac70d88556)) 80 | 81 | * chore: deploy read-the-docs ([`490ea6d`](https://github.com/xeroc/python-solana-actions/commit/490ea6d3e9433fc7581a92411a54e00a54797f2c)) 82 | 83 | * chore: unittests working ([`976d199`](https://github.com/xeroc/python-solana-actions/commit/976d19907135a3bf2b9b2750ee24ba0e5db6c27b)) 84 | 85 | * chore: initial commit ([`d6a2202`](https://github.com/xeroc/python-solana-actions/commit/d6a22020fcd1941dc0778e5da42b4d6e147f9bb8)) 86 | 87 | ### Ci 88 | 89 | * ci: allow upload of new commits from release ([`81810e5`](https://github.com/xeroc/python-solana-actions/commit/81810e52503e25b4a5f671707b84244a95d64a21)) 90 | 91 | * ci: no extra perms required ([`6442014`](https://github.com/xeroc/python-solana-actions/commit/6442014ee58bdf2dc3bf14dd2e9f6ff61e8c7ea0)) 92 | 93 | * ci(fix): we use workflows here ([`81775f7`](https://github.com/xeroc/python-solana-actions/commit/81775f74c491e78453a54e306b636888258a6686)) 94 | 95 | * ci(fix): we use workflows here ([`bea8486`](https://github.com/xeroc/python-solana-actions/commit/bea8486df618172045bcd1a4e66339c5c9632cb5)) 96 | 97 | * ci: activate release and publish ([`f3a13a1`](https://github.com/xeroc/python-solana-actions/commit/f3a13a18746b20a749b0413e4950f2335a4b8063)) 98 | 99 | * ci: release cycle ([`a9ceb64`](https://github.com/xeroc/python-solana-actions/commit/a9ceb645e2b74b845ffce4aef8de752519bdf749)) 100 | 101 | * ci: also run pipeline in develop ([`e87fffb`](https://github.com/xeroc/python-solana-actions/commit/e87fffbc79640eebc1589c9fc92ac13867768e4a)) 102 | 103 | ### Fix 104 | 105 | * fix: release cycle broke because file does not exist ([`1342f84`](https://github.com/xeroc/python-solana-actions/commit/1342f84e9d36651a21ec7fbc85532f2260465197)) 106 | 107 | ### Unknown 108 | 109 | * Merge branch 'develop' ([`7801cc2`](https://github.com/xeroc/python-solana-actions/commit/7801cc227fde3349cb8f546fc8df7e95e230c7c1)) 110 | 111 | * Merge branch 'develop' ([`7780577`](https://github.com/xeroc/python-solana-actions/commit/77805775c1fc2ed0a9c2a23274189ab97d6774d2)) 112 | 113 | * Merge pull request #1 from xeroc/release/202407011407 114 | 115 | Release/202407011407 ([`e3ce4a9`](https://github.com/xeroc/python-solana-actions/commit/e3ce4a938f45cd4d981c3e4da53121e649552612)) 116 | 117 | * ignore requirements.txt ([`7fbc6e2`](https://github.com/xeroc/python-solana-actions/commit/7fbc6e2d70613eb18fae70664a0095bf6a5c7a04)) 118 | 119 | * Initial commit ([`558fadb`](https://github.com/xeroc/python-solana-actions/commit/558fadbfbac264fc650597ef561deba6b786a77b)) 120 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Fabian Schuh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ROOT_DIR := $(shell pwd) 2 | PROJECT_NAME := $(notdir $(ROOT_DIR)) 3 | RELEASE_VERSION ?= $(shell git describe --always) 4 | RELEASE_DATE := $(shell date -u +'%Y-%m-%dT%H:%M:%SZ') 5 | 6 | .PHONY: clean 7 | clean: clean-build clean-pyc clean-pycache 8 | 9 | .PHONY: clean-build 10 | clean-build: 11 | rm -rf build/ 12 | rm -rf dist/ 13 | rm -rf *.egg-info 14 | rm -rf __pycache__/ .eggs/ .cache/ 15 | rm -rf .tox/ .pytest_cache/ .benchmarks/ .mypy_cache htmlcov 16 | 17 | .PHONY: clean-pycache 18 | clean-pycache: 19 | find . -name __pycache__ -exec rm -rf {} + 20 | 21 | .PHONY: clean-pyc 22 | clean-pyc: 23 | find . -name '*.pyc' -exec rm -f {} + 24 | find . -name '*.pyo' -exec rm -f {} + 25 | find . -name '*~' -exec rm -f {} + 26 | 27 | .PHONY: lint 28 | lint: 29 | flake8 $(PACKAGE_DIR) 30 | 31 | .PHONY: test 32 | test: 33 | python3 setup.py test 34 | 35 | .PHONY: tox 36 | tox: 37 | tox 38 | 39 | .PHONY: build 40 | build: 41 | python3 setup.py build 42 | 43 | .PHONY: install 44 | install: build 45 | python3 setup.py install 46 | 47 | .PHONY: install-user 48 | install-user: build 49 | python3 setup.py install --user 50 | 51 | .PHONY: git 52 | git: 53 | git push --all 54 | git push --tags 55 | 56 | .PHONY: check 57 | check: 58 | python3 setup.py check 59 | 60 | .PHONY: docs 61 | docs: 62 | poetry run mkdocs serve 63 | 64 | .PHONY: release 65 | release: 66 | git diff-index --quiet HEAD || { echo "untracked files! Aborting"; exit 1; } 67 | git checkout develop 68 | git checkout -b release/$(shell date +'%Y%m%d%H%m') 69 | git push origin release/$(shell date +'%Y%m%d%H%m') 70 | git checkout develop 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![.github/workflows/main.yaml](https://github.com/xeroc/python-solana-actions/actions/workflows/main.yaml/badge.svg?branch=main)](https://github.com/xeroc/python-solana-actions/actions/workflows/main.yaml) 2 | ![pypi](https://img.shields.io/pypi/v/solana-actions.svg) 3 | ![versions](https://img.shields.io/pypi/pyversions/solana-actions.svg) 4 | [![documentation](https://readthedocs.org/projects/python-solana-actions/badge/?version=latest)](https://python-solana-actions.readthedocs.org) 5 | [![Pre-Commit Enabled](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](https://github.com/pre-commit/pre-commit) 6 | 7 | # python-solana-actions 8 | 9 | Solana Actions library in python. 10 | 11 | ## Documentation 12 | 13 | Full Documentation is available on [python-solana-actions.rtfd.io](https://python-solana-actions.rtfd.io). 14 | 15 | ## Installation 16 | 17 | pip3 install solana-actions 18 | 19 | ## Resources 20 | 21 | - [How to Build Solana Actions](https://youtu.be/kCht01Ycif0) 22 | - [more resources for Solana Actions and blinks](https://solana.com/solutions/actions) 23 | 24 | ## Examples 25 | 26 | Please find implementation examples in this repo: 27 | 28 | - [Using Flask](https://github.com/xeroc/python-solana-actions/tree/main/examples/flask) 29 | - [Using FastAPI](https://github.com/xeroc/python-solana-actions/tree/main/examples/fastapi) 30 | 31 | ## Grant 32 | 33 | Many thanks to [Superteam](https://de.superteam.fun/) for funding the development of this library through an [instagrant](https://earn.superteam.fun/grants/). 34 | 35 | ## What are Solana Actions? 36 | 37 | [Solana Actions](https://solana.com/docs/advanced/actions#actions) are 38 | specification-compliant APIs that return transactions on the Solana blockchain 39 | to be previewed, signed, and sent across a number of various contexts, including 40 | QR codes, buttons + widgets, and websites across the internet. Actions make it 41 | simple for developers to integrate the things you can do throughout the Solana 42 | ecosystem right into your environment, allowing you to perform blockchain 43 | transactions without needing to navigate away to a different app or webpage. 44 | 45 | ## What are blockchain links (blinks)? 46 | 47 | [Blockchain links](https://solana.com/docs/advanced/actions#blinks) – or blinks 48 | – turn any Solana Action into a shareable, metadata-rich link. Blinks allow 49 | Action-aware clients (browser extension wallets, bots) to display additional 50 | capabilities for the user. On a website, a blink might immediately trigger a 51 | transaction preview in a wallet without going to a decentralized app; in 52 | Discord, a bot might expand the blink into an interactive set of buttons. This 53 | pushes the ability to interact on-chain to any web surface capable of displaying 54 | a URL. 55 | 56 | ## License 57 | 58 | [![Licence](https://img.shields.io/github/license/Ileriayo/markdown-badges?style=for-the-badge)](./LICENSE) 59 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Welcome to MkDocs 2 | 3 | For full documentation visit [mkdocs.org](https://www.mkdocs.org). 4 | 5 | ## Commands 6 | 7 | * `mkdocs new [dir-name]` - Create a new project. 8 | * `mkdocs serve` - Start the live-reloading docs server. 9 | * `mkdocs build` - Build the documentation site. 10 | * `mkdocs -h` - Print help message and exit. 11 | 12 | ## Project layout 13 | 14 | mkdocs.yml # The configuration file. 15 | docs/ 16 | index.md # The documentation homepage. 17 | ... # Other markdown pages, images and other files. 18 | -------------------------------------------------------------------------------- /examples/fastapi/README.md: -------------------------------------------------------------------------------- 1 | # Example implementation using Flask 2 | 3 | ## Installation 4 | 5 | ```bash 6 | poetry install 7 | ``` 8 | 9 | ## Running 10 | 11 | ```bash 12 | poetry run python app.py 13 | ``` 14 | -------------------------------------------------------------------------------- /examples/fastapi/app.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import uvicorn 4 | from fastapi import FastAPI, Request 5 | from fastapi.middleware.cors import CORSMiddleware 6 | from solana.rpc.api import Client 7 | from solana.transaction import Instruction, Transaction 8 | from solders.compute_budget import set_compute_unit_price 9 | from solders.instruction import AccountMeta 10 | from solders.pubkey import Pubkey 11 | 12 | from solana_actions.constants import MEMO_PROGRAM_ID 13 | from solana_actions.create_post_response import ( 14 | CreateActionPostResponseArgs, 15 | create_post_response, 16 | ) 17 | from solana_actions.types import ActionPostResponse 18 | 19 | SOLANA_API = "https://api.devnet.solana.com" 20 | 21 | 22 | app = FastAPI() 23 | 24 | origins = ["*"] 25 | app.add_middleware( 26 | CORSMiddleware, 27 | allow_origins=origins, 28 | allow_credentials=True, 29 | allow_methods=["*"], 30 | allow_headers=["*"], 31 | ) 32 | 33 | 34 | @app.middleware("http") 35 | async def ensure_cors_headers(request: Request, call_next): 36 | response = await call_next(request) 37 | # response.headers.update(ACTIONS_CORS_HEADERS) 38 | # response.content_type = "application/json" 39 | return response 40 | 41 | 42 | @app.get("/actions.json") 43 | def actions_json(): 44 | return { 45 | "rules": [ 46 | # map all root level routes to an action 47 | { 48 | "pathPattern": "/*", 49 | "apiPath": "/api/actions/*", 50 | }, 51 | # idempotent rule as the fallback 52 | { 53 | "pathPattern": "/api/actions/**", 54 | "apiPath": "/api/actions/**", 55 | }, 56 | ], 57 | } 58 | 59 | 60 | @app.get("/api/actions/memo") 61 | def get_actions_memo(): 62 | return { 63 | "title": "Actions Example - Simple On-chain Memo", 64 | "icon": "/solana_devs.jpg", 65 | "description": "Send a message on-chain using a Memo", 66 | "label": "Send Memo", 67 | } 68 | 69 | 70 | @app.post("/api/actions/memo") 71 | async def post_actions_memo(request: Request): 72 | body = await request.json() 73 | # NOTE: you may want to do validation here 74 | 75 | account = Pubkey.from_string(body["account"]) 76 | # NOTE: do some basic validation and exception handling 77 | 78 | connection = Client(SOLANA_API) 79 | latest_block_hash = connection.get_latest_blockhash() 80 | transaction = Transaction( 81 | recent_blockhash=latest_block_hash.value.blockhash, fee_payer=account 82 | ).add( 83 | set_compute_unit_price( 84 | micro_lamports=1000, 85 | ), 86 | Instruction( 87 | program_id=Pubkey.from_string(MEMO_PROGRAM_ID), 88 | data=b"this is a simple memo message2", 89 | accounts=[AccountMeta(account, is_signer=True, is_writable=True)], 90 | ), 91 | ) 92 | 93 | actions_args: ActionPostResponse = ActionPostResponse( 94 | transaction=transaction, 95 | message="Post this memo on-chain", 96 | ) 97 | payload: ActionPostResponse = create_post_response( 98 | args=CreateActionPostResponseArgs( 99 | fields=actions_args, 100 | # no additional signers are required for this transaction 101 | signers=None, 102 | ) 103 | ) 104 | return payload.model_dump() 105 | 106 | 107 | if __name__ == "__main__": 108 | uvicorn.run(app, port=5000) 109 | -------------------------------------------------------------------------------- /examples/fastapi/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "solana-actions-fastapi-example" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Fabian Schuh "] 6 | readme = "README.md" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.10" 10 | fastapi = "^0.111.0" 11 | pydantic = "^2.8.2" 12 | solana-actions = "^0.2.0" 13 | solana = "^0.34.2" 14 | solders = "^0.21.0" 15 | requests = "^2.32.3" 16 | PyNaCl = "^1.5.0" 17 | base58 = "^2.1.1" 18 | uvicorn = "^0.30.1" 19 | 20 | 21 | [build-system] 22 | requires = ["poetry-core"] 23 | build-backend = "poetry.core.masonry.api" 24 | -------------------------------------------------------------------------------- /examples/flask/README.md: -------------------------------------------------------------------------------- 1 | # Example implementation using Flask 2 | 3 | ## Installation 4 | 5 | ```bash 6 | poetry install 7 | ``` 8 | 9 | ## Running 10 | 11 | ```bash 12 | poetry run python app.py 13 | ``` 14 | -------------------------------------------------------------------------------- /examples/flask/app.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import functools 3 | 4 | from flask import Flask, jsonify, make_response, request 5 | from flask_cors import CORS 6 | from solana.rpc.api import Client 7 | from solana.transaction import Instruction, Transaction 8 | from solders.compute_budget import set_compute_unit_price 9 | from solders.instruction import AccountMeta 10 | from solders.pubkey import Pubkey 11 | 12 | from solana_actions.constants import ( 13 | ACTIONS_CORS_HEADERS, 14 | MEMO_PROGRAM_ID, 15 | ) 16 | from solana_actions.create_post_response import ( 17 | CreateActionPostResponseArgs, 18 | create_post_response, 19 | ) 20 | from solana_actions.types import ActionPostResponse 21 | 22 | SOLANA_API = "https://api.devnet.solana.com" 23 | 24 | 25 | app = Flask(__name__) 26 | CORS(app, resources={r"/api/*": {"origins": "*"}}) 27 | 28 | 29 | def return_json(f): 30 | @functools.wraps(f) 31 | def inner(**kwargs): 32 | ret = f(**kwargs) 33 | response = make_response(jsonify(ret)) 34 | response.headers.update(ACTIONS_CORS_HEADERS) 35 | response.content_type = "application/json" 36 | response.status_code = 200 37 | return response 38 | 39 | return inner 40 | 41 | 42 | @app.route("/actions.json", methods=["GET", "OPTIONS"]) 43 | @return_json 44 | def actions_json(): 45 | return { 46 | "rules": [ 47 | # map all root level routes to an action 48 | { 49 | "pathPattern": "/*", 50 | "apiPath": "/api/actions/*", 51 | }, 52 | # idempotent rule as the fallback 53 | { 54 | "pathPattern": "/api/actions/**", 55 | "apiPath": "/api/actions/**", 56 | }, 57 | ], 58 | } 59 | 60 | 61 | @app.route("/api/actions/memo", methods=["GET", "OPTIONS"]) 62 | @return_json 63 | def get_actions_memo(): 64 | return { 65 | "title": "Actions Example - Simple On-chain Memo", 66 | "icon": "/solana_devs.jpg", 67 | "description": "Send a message on-chain using a Memo", 68 | "label": "Send Memo", 69 | } 70 | 71 | 72 | @app.route("/api/actions/memo", methods=["POST"]) 73 | @return_json 74 | def post_actions_memo(): 75 | body = request.get_json(force=True) 76 | # NOTE: you may want to do validation here 77 | 78 | account = Pubkey.from_string(body["account"]) 79 | # NOTE: do some basic validation and exception handling 80 | 81 | connection = Client(SOLANA_API) 82 | latest_block_hash = connection.get_latest_blockhash() 83 | transaction = Transaction( 84 | recent_blockhash=latest_block_hash.value.blockhash, fee_payer=account 85 | ).add( 86 | set_compute_unit_price( 87 | micro_lamports=1000, 88 | ), 89 | Instruction( 90 | program_id=Pubkey.from_string(MEMO_PROGRAM_ID), 91 | data=b"this is a simple memo message2", 92 | accounts=[AccountMeta(account, is_signer=True, is_writable=True)], 93 | ), 94 | ) 95 | 96 | actions_args: ActionPostResponse = ActionPostResponse( 97 | transaction=transaction, 98 | message="Post this memo on-chain", 99 | ) 100 | payload: ActionPostResponse = create_post_response( 101 | args=CreateActionPostResponseArgs( 102 | fields=actions_args, 103 | # no additional signers are required for this transaction 104 | signers=None, 105 | ) 106 | ) 107 | return payload.model_dump() 108 | 109 | 110 | if __name__ == "__main__": 111 | app.run(port=5000) 112 | -------------------------------------------------------------------------------- /examples/flask/poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "annotated-types" 5 | version = "0.7.0" 6 | description = "Reusable constraint types to use with typing.Annotated" 7 | optional = false 8 | python-versions = ">=3.8" 9 | files = [ 10 | {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, 11 | {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, 12 | ] 13 | 14 | [[package]] 15 | name = "anyio" 16 | version = "4.4.0" 17 | description = "High level compatibility layer for multiple asynchronous event loop implementations" 18 | optional = false 19 | python-versions = ">=3.8" 20 | files = [ 21 | {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"}, 22 | {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"}, 23 | ] 24 | 25 | [package.dependencies] 26 | exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} 27 | idna = ">=2.8" 28 | sniffio = ">=1.1" 29 | typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} 30 | 31 | [package.extras] 32 | doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] 33 | test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] 34 | trio = ["trio (>=0.23)"] 35 | 36 | [[package]] 37 | name = "base58" 38 | version = "2.1.1" 39 | description = "Base58 and Base58Check implementation." 40 | optional = false 41 | python-versions = ">=3.5" 42 | files = [ 43 | {file = "base58-2.1.1-py3-none-any.whl", hash = "sha256:11a36f4d3ce51dfc1043f3218591ac4eb1ceb172919cebe05b52a5bcc8d245c2"}, 44 | {file = "base58-2.1.1.tar.gz", hash = "sha256:c5d0cb3f5b6e81e8e35da5754388ddcc6d0d14b6c6a132cb93d69ed580a7278c"}, 45 | ] 46 | 47 | [package.extras] 48 | tests = ["PyHamcrest (>=2.0.2)", "mypy", "pytest (>=4.6)", "pytest-benchmark", "pytest-cov", "pytest-flake8"] 49 | 50 | [[package]] 51 | name = "blinker" 52 | version = "1.8.2" 53 | description = "Fast, simple object-to-object and broadcast signaling" 54 | optional = false 55 | python-versions = ">=3.8" 56 | files = [ 57 | {file = "blinker-1.8.2-py3-none-any.whl", hash = "sha256:1779309f71bf239144b9399d06ae925637cf6634cf6bd131104184531bf67c01"}, 58 | {file = "blinker-1.8.2.tar.gz", hash = "sha256:8f77b09d3bf7c795e969e9486f39c2c5e9c39d4ee07424be2bc594ece9642d83"}, 59 | ] 60 | 61 | [[package]] 62 | name = "certifi" 63 | version = "2024.7.4" 64 | description = "Python package for providing Mozilla's CA Bundle." 65 | optional = false 66 | python-versions = ">=3.6" 67 | files = [ 68 | {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, 69 | {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, 70 | ] 71 | 72 | [[package]] 73 | name = "cffi" 74 | version = "1.16.0" 75 | description = "Foreign Function Interface for Python calling C code." 76 | optional = false 77 | python-versions = ">=3.8" 78 | files = [ 79 | {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, 80 | {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, 81 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, 82 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, 83 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, 84 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, 85 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, 86 | {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, 87 | {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, 88 | {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, 89 | {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, 90 | {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, 91 | {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, 92 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, 93 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, 94 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, 95 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, 96 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, 97 | {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, 98 | {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, 99 | {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, 100 | {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, 101 | {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, 102 | {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, 103 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, 104 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, 105 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, 106 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, 107 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, 108 | {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, 109 | {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, 110 | {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, 111 | {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, 112 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, 113 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, 114 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, 115 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, 116 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, 117 | {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, 118 | {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, 119 | {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, 120 | {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, 121 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, 122 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, 123 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, 124 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, 125 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, 126 | {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, 127 | {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, 128 | {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, 129 | {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, 130 | {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, 131 | ] 132 | 133 | [package.dependencies] 134 | pycparser = "*" 135 | 136 | [[package]] 137 | name = "charset-normalizer" 138 | version = "3.3.2" 139 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 140 | optional = false 141 | python-versions = ">=3.7.0" 142 | files = [ 143 | {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, 144 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, 145 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, 146 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, 147 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, 148 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, 149 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, 150 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, 151 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, 152 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, 153 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, 154 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, 155 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, 156 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, 157 | {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, 158 | {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, 159 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, 160 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, 161 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, 162 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, 163 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, 164 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, 165 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, 166 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, 167 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, 168 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, 169 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, 170 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, 171 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, 172 | {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, 173 | {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, 174 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, 175 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, 176 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, 177 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, 178 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, 179 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, 180 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, 181 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, 182 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, 183 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, 184 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, 185 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, 186 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, 187 | {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, 188 | {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, 189 | {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, 190 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, 191 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, 192 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, 193 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, 194 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, 195 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, 196 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, 197 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, 198 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, 199 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, 200 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, 201 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, 202 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, 203 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, 204 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, 205 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, 206 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, 207 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, 208 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, 209 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, 210 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, 211 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, 212 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, 213 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, 214 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, 215 | {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, 216 | {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, 217 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, 218 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, 219 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, 220 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, 221 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, 222 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, 223 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, 224 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, 225 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, 226 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, 227 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, 228 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, 229 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, 230 | {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, 231 | {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, 232 | {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, 233 | ] 234 | 235 | [[package]] 236 | name = "click" 237 | version = "8.1.7" 238 | description = "Composable command line interface toolkit" 239 | optional = false 240 | python-versions = ">=3.7" 241 | files = [ 242 | {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, 243 | {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, 244 | ] 245 | 246 | [package.dependencies] 247 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 248 | 249 | [[package]] 250 | name = "colorama" 251 | version = "0.4.6" 252 | description = "Cross-platform colored terminal text." 253 | optional = false 254 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 255 | files = [ 256 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 257 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 258 | ] 259 | 260 | [[package]] 261 | name = "construct" 262 | version = "2.10.68" 263 | description = "A powerful declarative symmetric parser/builder for binary data" 264 | optional = false 265 | python-versions = ">=3.6" 266 | files = [ 267 | {file = "construct-2.10.68.tar.gz", hash = "sha256:7b2a3fd8e5f597a5aa1d614c3bd516fa065db01704c72a1efaaeec6ef23d8b45"}, 268 | ] 269 | 270 | [package.extras] 271 | extras = ["arrow", "cloudpickle", "enum34", "lz4", "numpy", "ruamel.yaml"] 272 | 273 | [[package]] 274 | name = "construct-typing" 275 | version = "0.5.6" 276 | description = "Extension for the python package 'construct' that adds typing features" 277 | optional = false 278 | python-versions = ">=3.7" 279 | files = [ 280 | {file = "construct-typing-0.5.6.tar.gz", hash = "sha256:0dc501351cd6b308f15ec54e5fe7c0fbc07cc1530a1b77b4303062a0a93c1297"}, 281 | {file = "construct_typing-0.5.6-py3-none-any.whl", hash = "sha256:39c948329e880564e33521cba497b21b07967c465b9c9037d6334e2cffa1ced9"}, 282 | ] 283 | 284 | [package.dependencies] 285 | construct = "2.10.68" 286 | 287 | [[package]] 288 | name = "exceptiongroup" 289 | version = "1.2.2" 290 | description = "Backport of PEP 654 (exception groups)" 291 | optional = false 292 | python-versions = ">=3.7" 293 | files = [ 294 | {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, 295 | {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, 296 | ] 297 | 298 | [package.extras] 299 | test = ["pytest (>=6)"] 300 | 301 | [[package]] 302 | name = "flask" 303 | version = "3.0.3" 304 | description = "A simple framework for building complex web applications." 305 | optional = false 306 | python-versions = ">=3.8" 307 | files = [ 308 | {file = "flask-3.0.3-py3-none-any.whl", hash = "sha256:34e815dfaa43340d1d15a5c3a02b8476004037eb4840b34910c6e21679d288f3"}, 309 | {file = "flask-3.0.3.tar.gz", hash = "sha256:ceb27b0af3823ea2737928a4d99d125a06175b8512c445cbd9a9ce200ef76842"}, 310 | ] 311 | 312 | [package.dependencies] 313 | blinker = ">=1.6.2" 314 | click = ">=8.1.3" 315 | itsdangerous = ">=2.1.2" 316 | Jinja2 = ">=3.1.2" 317 | Werkzeug = ">=3.0.0" 318 | 319 | [package.extras] 320 | async = ["asgiref (>=3.2)"] 321 | dotenv = ["python-dotenv"] 322 | 323 | [[package]] 324 | name = "flask-cors" 325 | version = "4.0.1" 326 | description = "A Flask extension adding a decorator for CORS support" 327 | optional = false 328 | python-versions = "*" 329 | files = [ 330 | {file = "Flask_Cors-4.0.1-py2.py3-none-any.whl", hash = "sha256:f2a704e4458665580c074b714c4627dd5a306b333deb9074d0b1794dfa2fb677"}, 331 | {file = "flask_cors-4.0.1.tar.gz", hash = "sha256:eeb69b342142fdbf4766ad99357a7f3876a2ceb77689dc10ff912aac06c389e4"}, 332 | ] 333 | 334 | [package.dependencies] 335 | Flask = ">=0.9" 336 | 337 | [[package]] 338 | name = "h11" 339 | version = "0.14.0" 340 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 341 | optional = false 342 | python-versions = ">=3.7" 343 | files = [ 344 | {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, 345 | {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, 346 | ] 347 | 348 | [[package]] 349 | name = "httpcore" 350 | version = "1.0.5" 351 | description = "A minimal low-level HTTP client." 352 | optional = false 353 | python-versions = ">=3.8" 354 | files = [ 355 | {file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"}, 356 | {file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"}, 357 | ] 358 | 359 | [package.dependencies] 360 | certifi = "*" 361 | h11 = ">=0.13,<0.15" 362 | 363 | [package.extras] 364 | asyncio = ["anyio (>=4.0,<5.0)"] 365 | http2 = ["h2 (>=3,<5)"] 366 | socks = ["socksio (==1.*)"] 367 | trio = ["trio (>=0.22.0,<0.26.0)"] 368 | 369 | [[package]] 370 | name = "httpx" 371 | version = "0.27.0" 372 | description = "The next generation HTTP client." 373 | optional = false 374 | python-versions = ">=3.8" 375 | files = [ 376 | {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, 377 | {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, 378 | ] 379 | 380 | [package.dependencies] 381 | anyio = "*" 382 | certifi = "*" 383 | httpcore = "==1.*" 384 | idna = "*" 385 | sniffio = "*" 386 | 387 | [package.extras] 388 | brotli = ["brotli", "brotlicffi"] 389 | cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] 390 | http2 = ["h2 (>=3,<5)"] 391 | socks = ["socksio (==1.*)"] 392 | 393 | [[package]] 394 | name = "idna" 395 | version = "3.7" 396 | description = "Internationalized Domain Names in Applications (IDNA)" 397 | optional = false 398 | python-versions = ">=3.5" 399 | files = [ 400 | {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, 401 | {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, 402 | ] 403 | 404 | [[package]] 405 | name = "itsdangerous" 406 | version = "2.2.0" 407 | description = "Safely pass data to untrusted environments and back." 408 | optional = false 409 | python-versions = ">=3.8" 410 | files = [ 411 | {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, 412 | {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, 413 | ] 414 | 415 | [[package]] 416 | name = "jinja2" 417 | version = "3.1.4" 418 | description = "A very fast and expressive template engine." 419 | optional = false 420 | python-versions = ">=3.7" 421 | files = [ 422 | {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, 423 | {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, 424 | ] 425 | 426 | [package.dependencies] 427 | MarkupSafe = ">=2.0" 428 | 429 | [package.extras] 430 | i18n = ["Babel (>=2.7)"] 431 | 432 | [[package]] 433 | name = "jsonalias" 434 | version = "0.1.1" 435 | description = "A microlibrary that defines a Json type alias for Python." 436 | optional = false 437 | python-versions = ">=3.7,<4.0" 438 | files = [ 439 | {file = "jsonalias-0.1.1-py3-none-any.whl", hash = "sha256:a56d2888e6397812c606156504e861e8ec00e188005af149f003c787db3d3f18"}, 440 | {file = "jsonalias-0.1.1.tar.gz", hash = "sha256:64f04d935397d579fc94509e1fcb6212f2d081235d9d6395bd10baedf760a769"}, 441 | ] 442 | 443 | [[package]] 444 | name = "markupsafe" 445 | version = "2.1.5" 446 | description = "Safely add untrusted strings to HTML/XML markup." 447 | optional = false 448 | python-versions = ">=3.7" 449 | files = [ 450 | {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, 451 | {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, 452 | {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, 453 | {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, 454 | {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, 455 | {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, 456 | {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, 457 | {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, 458 | {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, 459 | {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, 460 | {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, 461 | {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, 462 | {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, 463 | {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, 464 | {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, 465 | {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, 466 | {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, 467 | {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, 468 | {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, 469 | {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, 470 | {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, 471 | {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, 472 | {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, 473 | {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, 474 | {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, 475 | {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, 476 | {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, 477 | {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, 478 | {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, 479 | {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, 480 | {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, 481 | {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, 482 | {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, 483 | {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, 484 | {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, 485 | {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, 486 | {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, 487 | {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, 488 | {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, 489 | {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, 490 | {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, 491 | {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, 492 | {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, 493 | {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, 494 | {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, 495 | {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, 496 | {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, 497 | {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, 498 | {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, 499 | {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, 500 | {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, 501 | {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, 502 | {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, 503 | {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, 504 | {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, 505 | {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, 506 | {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, 507 | {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, 508 | {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, 509 | {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, 510 | ] 511 | 512 | [[package]] 513 | name = "pycparser" 514 | version = "2.22" 515 | description = "C parser in Python" 516 | optional = false 517 | python-versions = ">=3.8" 518 | files = [ 519 | {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, 520 | {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, 521 | ] 522 | 523 | [[package]] 524 | name = "pydantic" 525 | version = "2.8.2" 526 | description = "Data validation using Python type hints" 527 | optional = false 528 | python-versions = ">=3.8" 529 | files = [ 530 | {file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"}, 531 | {file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"}, 532 | ] 533 | 534 | [package.dependencies] 535 | annotated-types = ">=0.4.0" 536 | pydantic-core = "2.20.1" 537 | typing-extensions = [ 538 | {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, 539 | {version = ">=4.6.1", markers = "python_version < \"3.13\""}, 540 | ] 541 | 542 | [package.extras] 543 | email = ["email-validator (>=2.0.0)"] 544 | 545 | [[package]] 546 | name = "pydantic-core" 547 | version = "2.20.1" 548 | description = "Core functionality for Pydantic validation and serialization" 549 | optional = false 550 | python-versions = ">=3.8" 551 | files = [ 552 | {file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"}, 553 | {file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"}, 554 | {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f239eb799a2081495ea659d8d4a43a8f42cd1fe9ff2e7e436295c38a10c286a"}, 555 | {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53e431da3fc53360db73eedf6f7124d1076e1b4ee4276b36fb25514544ceb4a3"}, 556 | {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1f62b2413c3a0e846c3b838b2ecd6c7a19ec6793b2a522745b0869e37ab5bc1"}, 557 | {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d41e6daee2813ecceea8eda38062d69e280b39df793f5a942fa515b8ed67953"}, 558 | {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d482efec8b7dc6bfaedc0f166b2ce349df0011f5d2f1f25537ced4cfc34fd98"}, 559 | {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e93e1a4b4b33daed65d781a57a522ff153dcf748dee70b40c7258c5861e1768a"}, 560 | {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7c4ea22b6739b162c9ecaaa41d718dfad48a244909fe7ef4b54c0b530effc5a"}, 561 | {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4f2790949cf385d985a31984907fecb3896999329103df4e4983a4a41e13e840"}, 562 | {file = "pydantic_core-2.20.1-cp310-none-win32.whl", hash = "sha256:5e999ba8dd90e93d57410c5e67ebb67ffcaadcea0ad973240fdfd3a135506250"}, 563 | {file = "pydantic_core-2.20.1-cp310-none-win_amd64.whl", hash = "sha256:512ecfbefef6dac7bc5eaaf46177b2de58cdf7acac8793fe033b24ece0b9566c"}, 564 | {file = "pydantic_core-2.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312"}, 565 | {file = "pydantic_core-2.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88"}, 566 | {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc"}, 567 | {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43"}, 568 | {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6"}, 569 | {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121"}, 570 | {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1"}, 571 | {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b"}, 572 | {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27"}, 573 | {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b"}, 574 | {file = "pydantic_core-2.20.1-cp311-none-win32.whl", hash = "sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a"}, 575 | {file = "pydantic_core-2.20.1-cp311-none-win_amd64.whl", hash = "sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2"}, 576 | {file = "pydantic_core-2.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231"}, 577 | {file = "pydantic_core-2.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9"}, 578 | {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f"}, 579 | {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52"}, 580 | {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237"}, 581 | {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe"}, 582 | {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e"}, 583 | {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24"}, 584 | {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1"}, 585 | {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd"}, 586 | {file = "pydantic_core-2.20.1-cp312-none-win32.whl", hash = "sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688"}, 587 | {file = "pydantic_core-2.20.1-cp312-none-win_amd64.whl", hash = "sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d"}, 588 | {file = "pydantic_core-2.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686"}, 589 | {file = "pydantic_core-2.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a"}, 590 | {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b"}, 591 | {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19"}, 592 | {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac"}, 593 | {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703"}, 594 | {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c"}, 595 | {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83"}, 596 | {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203"}, 597 | {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0"}, 598 | {file = "pydantic_core-2.20.1-cp313-none-win32.whl", hash = "sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e"}, 599 | {file = "pydantic_core-2.20.1-cp313-none-win_amd64.whl", hash = "sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20"}, 600 | {file = "pydantic_core-2.20.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:4745f4ac52cc6686390c40eaa01d48b18997cb130833154801a442323cc78f91"}, 601 | {file = "pydantic_core-2.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a8ad4c766d3f33ba8fd692f9aa297c9058970530a32c728a2c4bfd2616d3358b"}, 602 | {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41e81317dd6a0127cabce83c0c9c3fbecceae981c8391e6f1dec88a77c8a569a"}, 603 | {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04024d270cf63f586ad41fff13fde4311c4fc13ea74676962c876d9577bcc78f"}, 604 | {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eaad4ff2de1c3823fddf82f41121bdf453d922e9a238642b1dedb33c4e4f98ad"}, 605 | {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26ab812fa0c845df815e506be30337e2df27e88399b985d0bb4e3ecfe72df31c"}, 606 | {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c5ebac750d9d5f2706654c638c041635c385596caf68f81342011ddfa1e5598"}, 607 | {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2aafc5a503855ea5885559eae883978c9b6d8c8993d67766ee73d82e841300dd"}, 608 | {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4868f6bd7c9d98904b748a2653031fc9c2f85b6237009d475b1008bfaeb0a5aa"}, 609 | {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa2f457b4af386254372dfa78a2eda2563680d982422641a85f271c859df1987"}, 610 | {file = "pydantic_core-2.20.1-cp38-none-win32.whl", hash = "sha256:225b67a1f6d602de0ce7f6c1c3ae89a4aa25d3de9be857999e9124f15dab486a"}, 611 | {file = "pydantic_core-2.20.1-cp38-none-win_amd64.whl", hash = "sha256:6b507132dcfc0dea440cce23ee2182c0ce7aba7054576efc65634f080dbe9434"}, 612 | {file = "pydantic_core-2.20.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b03f7941783b4c4a26051846dea594628b38f6940a2fdc0df00b221aed39314c"}, 613 | {file = "pydantic_core-2.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1eedfeb6089ed3fad42e81a67755846ad4dcc14d73698c120a82e4ccf0f1f9f6"}, 614 | {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:635fee4e041ab9c479e31edda27fcf966ea9614fff1317e280d99eb3e5ab6fe2"}, 615 | {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:77bf3ac639c1ff567ae3b47f8d4cc3dc20f9966a2a6dd2311dcc055d3d04fb8a"}, 616 | {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ed1b0132f24beeec5a78b67d9388656d03e6a7c837394f99257e2d55b461611"}, 617 | {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6514f963b023aeee506678a1cf821fe31159b925c4b76fe2afa94cc70b3222b"}, 618 | {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10d4204d8ca33146e761c79f83cc861df20e7ae9f6487ca290a97702daf56006"}, 619 | {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d036c7187b9422ae5b262badb87a20a49eb6c5238b2004e96d4da1231badef1"}, 620 | {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ebfef07dbe1d93efb94b4700f2d278494e9162565a54f124c404a5656d7ff09"}, 621 | {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6b9d9bb600328a1ce523ab4f454859e9d439150abb0906c5a1983c146580ebab"}, 622 | {file = "pydantic_core-2.20.1-cp39-none-win32.whl", hash = "sha256:784c1214cb6dd1e3b15dd8b91b9a53852aed16671cc3fbe4786f4f1db07089e2"}, 623 | {file = "pydantic_core-2.20.1-cp39-none-win_amd64.whl", hash = "sha256:d2fe69c5434391727efa54b47a1e7986bb0186e72a41b203df8f5b0a19a4f669"}, 624 | {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906"}, 625 | {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94"}, 626 | {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f"}, 627 | {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482"}, 628 | {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6"}, 629 | {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc"}, 630 | {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99"}, 631 | {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6"}, 632 | {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331"}, 633 | {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad"}, 634 | {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1"}, 635 | {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86"}, 636 | {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e"}, 637 | {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0"}, 638 | {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a"}, 639 | {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7"}, 640 | {file = "pydantic_core-2.20.1.tar.gz", hash = "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4"}, 641 | ] 642 | 643 | [package.dependencies] 644 | typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" 645 | 646 | [[package]] 647 | name = "pynacl" 648 | version = "1.5.0" 649 | description = "Python binding to the Networking and Cryptography (NaCl) library" 650 | optional = false 651 | python-versions = ">=3.6" 652 | files = [ 653 | {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"}, 654 | {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"}, 655 | {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394"}, 656 | {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d"}, 657 | {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858"}, 658 | {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b"}, 659 | {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff"}, 660 | {file = "PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543"}, 661 | {file = "PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93"}, 662 | {file = "PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba"}, 663 | ] 664 | 665 | [package.dependencies] 666 | cffi = ">=1.4.1" 667 | 668 | [package.extras] 669 | docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] 670 | tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] 671 | 672 | [[package]] 673 | name = "requests" 674 | version = "2.32.3" 675 | description = "Python HTTP for Humans." 676 | optional = false 677 | python-versions = ">=3.8" 678 | files = [ 679 | {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, 680 | {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, 681 | ] 682 | 683 | [package.dependencies] 684 | certifi = ">=2017.4.17" 685 | charset-normalizer = ">=2,<4" 686 | idna = ">=2.5,<4" 687 | urllib3 = ">=1.21.1,<3" 688 | 689 | [package.extras] 690 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 691 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 692 | 693 | [[package]] 694 | name = "sniffio" 695 | version = "1.3.1" 696 | description = "Sniff out which async library your code is running under" 697 | optional = false 698 | python-versions = ">=3.7" 699 | files = [ 700 | {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, 701 | {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, 702 | ] 703 | 704 | [[package]] 705 | name = "solana" 706 | version = "0.34.2" 707 | description = "Solana Python API" 708 | optional = false 709 | python-versions = "<4.0,>=3.8" 710 | files = [ 711 | {file = "solana-0.34.2-py3-none-any.whl", hash = "sha256:89722dd7296f72872ff9d2745b90d58b7e6af7ed12fae86896a1f43a815f15b2"}, 712 | {file = "solana-0.34.2.tar.gz", hash = "sha256:b2c58c323f99a555795b797d214e784dfccae6cf08166be2411bba2a4bb18d44"}, 713 | ] 714 | 715 | [package.dependencies] 716 | construct-typing = ">=0.5.2,<0.6.0" 717 | httpx = ">=0.23.0" 718 | solders = ">=0.21.0,<0.22.0" 719 | typing-extensions = ">=4.2.0" 720 | websockets = ">=9.0,<12.0" 721 | 722 | [[package]] 723 | name = "solana-actions" 724 | version = "0.2.0" 725 | description = "Solana Actions library" 726 | optional = false 727 | python-versions = "<4.0,>=3.10" 728 | files = [ 729 | {file = "solana_actions-0.2.0-py3-none-any.whl", hash = "sha256:634c5dba5384fcfa6af726f81033917a8aead6b3c92d8f5718a15b22e573aa26"}, 730 | {file = "solana_actions-0.2.0.tar.gz", hash = "sha256:5a4d69a7a5b62d72cc643c5d9c8deeb93f7256ca2150482eacb8acf0ba7320c2"}, 731 | ] 732 | 733 | [package.dependencies] 734 | base58 = ">=2.1.1,<3.0.0" 735 | pydantic = ">=2.7.4,<3.0.0" 736 | pynacl = ">=1.5.0,<2.0.0" 737 | requests = ">=2.32.3,<3.0.0" 738 | solana = ">=0.34.2,<0.35.0" 739 | solders = ">=0.21.0,<0.22.0" 740 | 741 | [[package]] 742 | name = "solders" 743 | version = "0.21.0" 744 | description = "Python bindings for Solana Rust tools" 745 | optional = false 746 | python-versions = ">=3.7" 747 | files = [ 748 | {file = "solders-0.21.0-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7df88e59aea016644c0b2eac84f2f931d5aa570c654132770263b26f2928fdb7"}, 749 | {file = "solders-0.21.0-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a11dfc5933707c466880ef2116f1bffc74659bf677b79479f4280247d60543c9"}, 750 | {file = "solders-0.21.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33a28fedff80defd01455844700e3b9924c06a87d7ca93aff0a9298a9eb902ac"}, 751 | {file = "solders-0.21.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3ac70badd0da7e0d87db1c9c2edac63e48470903fd5f28e2fd6b22c7624ef52f"}, 752 | {file = "solders-0.21.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ac79feca36470945ac026433828d4105a4b3bada5422ea77b1083c0e8fe93872"}, 753 | {file = "solders-0.21.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:6993e2e1709aa04b94267597dc31e29ae5625cde3d65fdf452c6366c6c7f41cd"}, 754 | {file = "solders-0.21.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9521974ffa8b0fc8a4aa3b65f9057392c214a814c10af4f8cd2ad1d3f943ae61"}, 755 | {file = "solders-0.21.0-cp37-abi3-win_amd64.whl", hash = "sha256:7258b0faa97ab3dc2e1951082af63f2971f178519540f7abac43ec2385d84b7f"}, 756 | {file = "solders-0.21.0.tar.gz", hash = "sha256:a228c09b690f215acb01c55e17246efdfdb7c013f7332b057ecd0499363868ad"}, 757 | ] 758 | 759 | [package.dependencies] 760 | jsonalias = "0.1.1" 761 | typing-extensions = ">=4.2.0" 762 | 763 | [[package]] 764 | name = "typing-extensions" 765 | version = "4.12.2" 766 | description = "Backported and Experimental Type Hints for Python 3.8+" 767 | optional = false 768 | python-versions = ">=3.8" 769 | files = [ 770 | {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, 771 | {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, 772 | ] 773 | 774 | [[package]] 775 | name = "urllib3" 776 | version = "2.2.2" 777 | description = "HTTP library with thread-safe connection pooling, file post, and more." 778 | optional = false 779 | python-versions = ">=3.8" 780 | files = [ 781 | {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, 782 | {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, 783 | ] 784 | 785 | [package.extras] 786 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] 787 | h2 = ["h2 (>=4,<5)"] 788 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 789 | zstd = ["zstandard (>=0.18.0)"] 790 | 791 | [[package]] 792 | name = "websockets" 793 | version = "11.0.3" 794 | description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" 795 | optional = false 796 | python-versions = ">=3.7" 797 | files = [ 798 | {file = "websockets-11.0.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3ccc8a0c387629aec40f2fc9fdcb4b9d5431954f934da3eaf16cdc94f67dbfac"}, 799 | {file = "websockets-11.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d67ac60a307f760c6e65dad586f556dde58e683fab03323221a4e530ead6f74d"}, 800 | {file = "websockets-11.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84d27a4832cc1a0ee07cdcf2b0629a8a72db73f4cf6de6f0904f6661227f256f"}, 801 | {file = "websockets-11.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffd7dcaf744f25f82190856bc26ed81721508fc5cbf2a330751e135ff1283564"}, 802 | {file = "websockets-11.0.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7622a89d696fc87af8e8d280d9b421db5133ef5b29d3f7a1ce9f1a7bf7fcfa11"}, 803 | {file = "websockets-11.0.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bceab846bac555aff6427d060f2fcfff71042dba6f5fca7dc4f75cac815e57ca"}, 804 | {file = "websockets-11.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:54c6e5b3d3a8936a4ab6870d46bdd6ec500ad62bde9e44462c32d18f1e9a8e54"}, 805 | {file = "websockets-11.0.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:41f696ba95cd92dc047e46b41b26dd24518384749ed0d99bea0a941ca87404c4"}, 806 | {file = "websockets-11.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:86d2a77fd490ae3ff6fae1c6ceaecad063d3cc2320b44377efdde79880e11526"}, 807 | {file = "websockets-11.0.3-cp310-cp310-win32.whl", hash = "sha256:2d903ad4419f5b472de90cd2d40384573b25da71e33519a67797de17ef849b69"}, 808 | {file = "websockets-11.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:1d2256283fa4b7f4c7d7d3e84dc2ece74d341bce57d5b9bf385df109c2a1a82f"}, 809 | {file = "websockets-11.0.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e848f46a58b9fcf3d06061d17be388caf70ea5b8cc3466251963c8345e13f7eb"}, 810 | {file = "websockets-11.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa5003845cdd21ac0dc6c9bf661c5beddd01116f6eb9eb3c8e272353d45b3288"}, 811 | {file = "websockets-11.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b58cbf0697721120866820b89f93659abc31c1e876bf20d0b3d03cef14faf84d"}, 812 | {file = "websockets-11.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:660e2d9068d2bedc0912af508f30bbeb505bbbf9774d98def45f68278cea20d3"}, 813 | {file = "websockets-11.0.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c1f0524f203e3bd35149f12157438f406eff2e4fb30f71221c8a5eceb3617b6b"}, 814 | {file = "websockets-11.0.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:def07915168ac8f7853812cc593c71185a16216e9e4fa886358a17ed0fd9fcf6"}, 815 | {file = "websockets-11.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b30c6590146e53149f04e85a6e4fcae068df4289e31e4aee1fdf56a0dead8f97"}, 816 | {file = "websockets-11.0.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:619d9f06372b3a42bc29d0cd0354c9bb9fb39c2cbc1a9c5025b4538738dbffaf"}, 817 | {file = "websockets-11.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:01f5567d9cf6f502d655151645d4e8b72b453413d3819d2b6f1185abc23e82dd"}, 818 | {file = "websockets-11.0.3-cp311-cp311-win32.whl", hash = "sha256:e1459677e5d12be8bbc7584c35b992eea142911a6236a3278b9b5ce3326f282c"}, 819 | {file = "websockets-11.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:e7837cb169eca3b3ae94cc5787c4fed99eef74c0ab9506756eea335e0d6f3ed8"}, 820 | {file = "websockets-11.0.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9f59a3c656fef341a99e3d63189852be7084c0e54b75734cde571182c087b152"}, 821 | {file = "websockets-11.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2529338a6ff0eb0b50c7be33dc3d0e456381157a31eefc561771ee431134a97f"}, 822 | {file = "websockets-11.0.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34fd59a4ac42dff6d4681d8843217137f6bc85ed29722f2f7222bd619d15e95b"}, 823 | {file = "websockets-11.0.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:332d126167ddddec94597c2365537baf9ff62dfcc9db4266f263d455f2f031cb"}, 824 | {file = "websockets-11.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6505c1b31274723ccaf5f515c1824a4ad2f0d191cec942666b3d0f3aa4cb4007"}, 825 | {file = "websockets-11.0.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f467ba0050b7de85016b43f5a22b46383ef004c4f672148a8abf32bc999a87f0"}, 826 | {file = "websockets-11.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:9d9acd80072abcc98bd2c86c3c9cd4ac2347b5a5a0cae7ed5c0ee5675f86d9af"}, 827 | {file = "websockets-11.0.3-cp37-cp37m-win32.whl", hash = "sha256:e590228200fcfc7e9109509e4d9125eace2042fd52b595dd22bbc34bb282307f"}, 828 | {file = "websockets-11.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:b16fff62b45eccb9c7abb18e60e7e446998093cdcb50fed33134b9b6878836de"}, 829 | {file = "websockets-11.0.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fb06eea71a00a7af0ae6aefbb932fb8a7df3cb390cc217d51a9ad7343de1b8d0"}, 830 | {file = "websockets-11.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8a34e13a62a59c871064dfd8ffb150867e54291e46d4a7cf11d02c94a5275bae"}, 831 | {file = "websockets-11.0.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4841ed00f1026dfbced6fca7d963c4e7043aa832648671b5138008dc5a8f6d99"}, 832 | {file = "websockets-11.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a073fc9ab1c8aff37c99f11f1641e16da517770e31a37265d2755282a5d28aa"}, 833 | {file = "websockets-11.0.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68b977f21ce443d6d378dbd5ca38621755f2063d6fdb3335bda981d552cfff86"}, 834 | {file = "websockets-11.0.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1a99a7a71631f0efe727c10edfba09ea6bee4166a6f9c19aafb6c0b5917d09c"}, 835 | {file = "websockets-11.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bee9fcb41db2a23bed96c6b6ead6489702c12334ea20a297aa095ce6d31370d0"}, 836 | {file = "websockets-11.0.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4b253869ea05a5a073ebfdcb5cb3b0266a57c3764cf6fe114e4cd90f4bfa5f5e"}, 837 | {file = "websockets-11.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:1553cb82942b2a74dd9b15a018dce645d4e68674de2ca31ff13ebc2d9f283788"}, 838 | {file = "websockets-11.0.3-cp38-cp38-win32.whl", hash = "sha256:f61bdb1df43dc9c131791fbc2355535f9024b9a04398d3bd0684fc16ab07df74"}, 839 | {file = "websockets-11.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:03aae4edc0b1c68498f41a6772d80ac7c1e33c06c6ffa2ac1c27a07653e79d6f"}, 840 | {file = "websockets-11.0.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:777354ee16f02f643a4c7f2b3eff8027a33c9861edc691a2003531f5da4f6bc8"}, 841 | {file = "websockets-11.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8c82f11964f010053e13daafdc7154ce7385ecc538989a354ccc7067fd7028fd"}, 842 | {file = "websockets-11.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3580dd9c1ad0701169e4d6fc41e878ffe05e6bdcaf3c412f9d559389d0c9e016"}, 843 | {file = "websockets-11.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f1a3f10f836fab6ca6efa97bb952300b20ae56b409414ca85bff2ad241d2a61"}, 844 | {file = "websockets-11.0.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df41b9bc27c2c25b486bae7cf42fccdc52ff181c8c387bfd026624a491c2671b"}, 845 | {file = "websockets-11.0.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:279e5de4671e79a9ac877427f4ac4ce93751b8823f276b681d04b2156713b9dd"}, 846 | {file = "websockets-11.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1fdf26fa8a6a592f8f9235285b8affa72748dc12e964a5518c6c5e8f916716f7"}, 847 | {file = "websockets-11.0.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:69269f3a0b472e91125b503d3c0b3566bda26da0a3261c49f0027eb6075086d1"}, 848 | {file = "websockets-11.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:97b52894d948d2f6ea480171a27122d77af14ced35f62e5c892ca2fae9344311"}, 849 | {file = "websockets-11.0.3-cp39-cp39-win32.whl", hash = "sha256:c7f3cb904cce8e1be667c7e6fef4516b98d1a6a0635a58a57528d577ac18a128"}, 850 | {file = "websockets-11.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c792ea4eabc0159535608fc5658a74d1a81020eb35195dd63214dcf07556f67e"}, 851 | {file = "websockets-11.0.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f2e58f2c36cc52d41f2659e4c0cbf7353e28c8c9e63e30d8c6d3494dc9fdedcf"}, 852 | {file = "websockets-11.0.3-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de36fe9c02995c7e6ae6efe2e205816f5f00c22fd1fbf343d4d18c3d5ceac2f5"}, 853 | {file = "websockets-11.0.3-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0ac56b661e60edd453585f4bd68eb6a29ae25b5184fd5ba51e97652580458998"}, 854 | {file = "websockets-11.0.3-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e052b8467dd07d4943936009f46ae5ce7b908ddcac3fda581656b1b19c083d9b"}, 855 | {file = "websockets-11.0.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:42cc5452a54a8e46a032521d7365da775823e21bfba2895fb7b77633cce031bb"}, 856 | {file = "websockets-11.0.3-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e6316827e3e79b7b8e7d8e3b08f4e331af91a48e794d5d8b099928b6f0b85f20"}, 857 | {file = "websockets-11.0.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8531fdcad636d82c517b26a448dcfe62f720e1922b33c81ce695d0edb91eb931"}, 858 | {file = "websockets-11.0.3-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c114e8da9b475739dde229fd3bc6b05a6537a88a578358bc8eb29b4030fac9c9"}, 859 | {file = "websockets-11.0.3-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e063b1865974611313a3849d43f2c3f5368093691349cf3c7c8f8f75ad7cb280"}, 860 | {file = "websockets-11.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:92b2065d642bf8c0a82d59e59053dd2fdde64d4ed44efe4870fa816c1232647b"}, 861 | {file = "websockets-11.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0ee68fe502f9031f19d495dae2c268830df2760c0524cbac5d759921ba8c8e82"}, 862 | {file = "websockets-11.0.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcacf2c7a6c3a84e720d1bb2b543c675bf6c40e460300b628bab1b1efc7c034c"}, 863 | {file = "websockets-11.0.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b67c6f5e5a401fc56394f191f00f9b3811fe843ee93f4a70df3c389d1adf857d"}, 864 | {file = "websockets-11.0.3-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d5023a4b6a5b183dc838808087033ec5df77580485fc533e7dab2567851b0a4"}, 865 | {file = "websockets-11.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ed058398f55163a79bb9f06a90ef9ccc063b204bb346c4de78efc5d15abfe602"}, 866 | {file = "websockets-11.0.3-py3-none-any.whl", hash = "sha256:6681ba9e7f8f3b19440921e99efbb40fc89f26cd71bf539e45d8c8a25c976dc6"}, 867 | {file = "websockets-11.0.3.tar.gz", hash = "sha256:88fc51d9a26b10fc331be344f1781224a375b78488fc343620184e95a4b27016"}, 868 | ] 869 | 870 | [[package]] 871 | name = "werkzeug" 872 | version = "3.0.3" 873 | description = "The comprehensive WSGI web application library." 874 | optional = false 875 | python-versions = ">=3.8" 876 | files = [ 877 | {file = "werkzeug-3.0.3-py3-none-any.whl", hash = "sha256:fc9645dc43e03e4d630d23143a04a7f947a9a3b5727cd535fdfe155a17cc48c8"}, 878 | {file = "werkzeug-3.0.3.tar.gz", hash = "sha256:097e5bfda9f0aba8da6b8545146def481d06aa7d3266e7448e2cccf67dd8bd18"}, 879 | ] 880 | 881 | [package.dependencies] 882 | MarkupSafe = ">=2.1.1" 883 | 884 | [package.extras] 885 | watchdog = ["watchdog (>=2.3)"] 886 | 887 | [metadata] 888 | lock-version = "2.0" 889 | python-versions = "^3.10" 890 | content-hash = "edb08518e774382c63f5c52682c2a96bea52500902bdd4d296a5d5d4c6b4446f" 891 | -------------------------------------------------------------------------------- /examples/flask/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "solana-actions-flask-example" 3 | version = "0.1.0" 4 | description = "An example about how to use solana-actions in python" 5 | authors = ["Fabian Schuh "] 6 | license = "MIT" 7 | readme = "README.md" 8 | package-mode = false 9 | 10 | [tool.poetry.dependencies] 11 | python = "^3.10" 12 | Flask = "^3.0.3" 13 | Flask-Cors = "^4.0.1" 14 | solana-actions = "^0.2.0" 15 | solana = "^0.34.2" 16 | solders = "^0.21.0" 17 | requests = "^2.32.3" 18 | pynacl = "^1.5.0" 19 | base58 = "^2.1.1" 20 | 21 | 22 | [build-system] 23 | requires = ["poetry-core"] 24 | build-backend = "poetry.core.masonry.api" 25 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Solana Actions in Python 2 | site_url: https://solana-actions.readthedocs.io 3 | theme: 4 | name: material 5 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "solana-actions" 3 | version = "0.2.1" 4 | description = "Solana Actions library" 5 | authors = ["Fabian Schuh "] 6 | license = "MIT" 7 | readme = "README.md" 8 | 9 | [tool.poetry.dependencies] 10 | python = "^3.10" 11 | solana = "^0.34.2" 12 | pydantic = "^2.7.4" 13 | solders = "^0.21.0" 14 | base58 = "^2.1.1" 15 | pynacl = "^1.5.0" 16 | requests = "^2.32.3" 17 | 18 | [tool.poetry.group.dev.dependencies] 19 | pytest = "^8.2.2" 20 | coverage = "^7.5.4" 21 | black = "^24.4.2" 22 | mypy = "^1.10.1" 23 | autoflake = "^2.3.1" 24 | pytest-asyncio = "^0.23.7" 25 | 26 | [tool.poetry.group.docs] 27 | optional = true 28 | 29 | [tool.poetry.group.docs.dependencies] 30 | mkdocs-material = "^9.5.27" 31 | 32 | [build-system] 33 | requires = ["poetry-core"] 34 | build-backend = "poetry.core.masonry.api" 35 | 36 | [tool.flake8] 37 | max-line-length = 120 38 | 39 | [tool.semantic_release] 40 | assets = [] 41 | build_command_env = [] 42 | commit_message = "{version}\n\nAutomated Release Commit" 43 | commit_parser = "angular" 44 | logging_use_named_masks = false 45 | major_on_zero = true 46 | allow_zero_version = true 47 | tag_format = "v{version}" 48 | version_variables = ["solana_actions/__init__.py:__version__"] 49 | version_toml = ["pyproject.toml:tool.poetry.version"] 50 | 51 | [tool.semantic_release.branches.main] 52 | match = "(main|master)" 53 | prerelease_token = "rc" 54 | prerelease = false 55 | 56 | [tool.semantic_release.changelog] 57 | template_dir = "templates" 58 | changelog_file = "CHANGELOG.md" 59 | exclude_commit_patterns = [] 60 | 61 | [tool.semantic_release.changelog.environment] 62 | block_start_string = "{%" 63 | block_end_string = "%}" 64 | variable_start_string = "{{" 65 | variable_end_string = "}}" 66 | comment_start_string = "{#" 67 | comment_end_string = "#}" 68 | trim_blocks = false 69 | lstrip_blocks = false 70 | newline_sequence = "\n" 71 | keep_trailing_newline = false 72 | extensions = [] 73 | autoescape = true 74 | 75 | [tool.semantic_release.commit_author] 76 | env = "GIT_COMMIT_AUTHOR" 77 | default = "semantic-release " 78 | 79 | [tool.semantic_release.commit_parser_options] 80 | allowed_tags = ["build", "chore", "ci", "docs", "feat", "fix", "perf", "style", "refactor", "test"] 81 | minor_tags = ["feat"] 82 | patch_tags = ["fix", "perf"] 83 | default_bump_level = 0 84 | 85 | [tool.semantic_release.remote] 86 | name = "origin" 87 | type = "github" 88 | ignore_token_for_push = false 89 | insecure = false 90 | 91 | [tool.semantic_release.publish] 92 | dist_glob_patterns = ["dist/*"] 93 | upload_to_vcs_release = true 94 | 95 | [tool.pytest.ini_options] 96 | minversion = "6.0" 97 | testpaths = [ 98 | "tests", 99 | ] 100 | 101 | [tool.bandit] 102 | exclude_dirs = [".env", "tests"] 103 | skips = ["B101"] 104 | 105 | [tool.coverage.run] 106 | branch = true 107 | omit = ["tests/*", "examples/"] 108 | 109 | [tool.coverage.report] 110 | show_missing = true 111 | # Regexes for lines to exclude from consideration 112 | exclude_also = [ 113 | # Don't complain about missing debug-only code: 114 | "def __repr__", 115 | "if self\\.debug", 116 | 117 | # Don't complain if tests don't hit defensive assertion code: 118 | "raise AssertionError", 119 | "raise NotImplementedError", 120 | 121 | # Don't complain if non-runnable code isn't run: 122 | "if 0:", 123 | "if __name__ == .__main__.:", 124 | 125 | # Don't complain about abstract methods, they aren't run: 126 | "@(abc\\.)?abstractmethod", 127 | ] 128 | 129 | ignore_errors = true 130 | 131 | [tool.coverage.html] 132 | directory = "coverage_html_report" 133 | 134 | [tool.mypy] 135 | ignore_missing_imports = true 136 | -------------------------------------------------------------------------------- /solana_actions/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __version__ = "0.2.1" 3 | -------------------------------------------------------------------------------- /solana_actions/action_identity.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from typing import List, Optional, Union 3 | 4 | import base58 5 | import nacl.signing 6 | from pydantic import BaseModel 7 | from solana.rpc.api import Client 8 | from solana.transaction import Instruction 9 | from solders.keypair import Keypair 10 | from solders.pubkey import Pubkey 11 | 12 | from .constants import MEMO_PROGRAM_ID, SOLANA_ACTIONS_PROTOCOL 13 | from .find_reference import find_reference 14 | from .types import Reference 15 | 16 | 17 | class ActionsIdentitySchema(BaseModel): 18 | separator: str = ":" 19 | protocol: str = SOLANA_ACTIONS_PROTOCOL.replace(":", "") 20 | scheme: dict = { 21 | "protocol": 0, 22 | "identity": 1, 23 | "reference": 2, 24 | "signature": 3, 25 | } 26 | 27 | 28 | ACTIONS_IDENTITY_SCHEMA = ActionsIdentitySchema() 29 | 30 | 31 | class ActionIdentifierError(Exception): 32 | pass 33 | 34 | 35 | def create_action_identifier_memo(identity: Keypair, reference: Reference) -> str: 36 | signature = nacl.signing.SigningKey(identity.secret_key()).sign( 37 | reference.to_bytes() 38 | )[:64] 39 | 40 | identifier = [""] * len(ACTIONS_IDENTITY_SCHEMA.scheme) 41 | identifier[ 42 | ACTIONS_IDENTITY_SCHEMA.scheme["protocol"] 43 | ] = ACTIONS_IDENTITY_SCHEMA.protocol 44 | identifier[ACTIONS_IDENTITY_SCHEMA.scheme["identity"]] = str(identity.public_key) 45 | identifier[ACTIONS_IDENTITY_SCHEMA.scheme["reference"]] = str(reference) 46 | identifier[ACTIONS_IDENTITY_SCHEMA.scheme["signature"]] = base58.b58encode( 47 | signature 48 | ).decode() 49 | 50 | return ACTIONS_IDENTITY_SCHEMA.separator.join(identifier) 51 | 52 | 53 | def validate_action_identifier_memo( 54 | identity: Pubkey, memos: Union[str, List[str], None] 55 | ) -> Union[bool, dict]: 56 | if not memos: 57 | return False 58 | 59 | if isinstance(memos, str): 60 | memos = memos.split(";") 61 | 62 | for memo in memos: 63 | try: 64 | memo = memo.strip() 65 | if memo.startswith("[") and "] " in memo: 66 | memo = memo.split("] ", 1)[1].strip() 67 | 68 | if not memo.count(":") >= 2: 69 | raise ActionIdentifierError("invalid memo formatting") 70 | 71 | identifier = memo.split(ACTIONS_IDENTITY_SCHEMA.separator) 72 | if len(identifier) != len(ACTIONS_IDENTITY_SCHEMA.scheme): 73 | raise ActionIdentifierError("invalid memo length") 74 | 75 | try: 76 | memo_identity = Pubkey.from_string( 77 | identifier[ACTIONS_IDENTITY_SCHEMA.scheme["identity"]] 78 | ) 79 | except ValueError: 80 | raise ActionIdentifierError("malformed memo identity") 81 | 82 | if not memo_identity: 83 | raise ActionIdentifierError("invalid memo identity") 84 | if str(memo_identity) != str(identity): 85 | raise ActionIdentifierError("identity mismatch") 86 | 87 | verified = nacl.signing.VerifyKey(bytes(identity)).verify( 88 | base58.b58decode( 89 | identifier[ACTIONS_IDENTITY_SCHEMA.scheme["reference"]] 90 | ), 91 | base58.b58decode( 92 | identifier[ACTIONS_IDENTITY_SCHEMA.scheme["signature"]] 93 | ), 94 | ) 95 | 96 | if verified: 97 | return { 98 | "verified": True, 99 | "reference": identifier[ 100 | ACTIONS_IDENTITY_SCHEMA.scheme["reference"] 101 | ], 102 | } 103 | except Exception: 104 | return False 105 | return False 106 | 107 | 108 | def verify_signature_info_for_identity( 109 | connection: Client, identity: Keypair, sig_info: dict 110 | ) -> bool: 111 | try: 112 | validated = validate_action_identifier_memo( 113 | identity.public_key, sig_info.get("memo") 114 | ) 115 | if not validated: 116 | return False 117 | 118 | confirmed_sig_info = find_reference( 119 | connection, Pubkey.from_string(validated["reference"]) 120 | ) 121 | 122 | if confirmed_sig_info["signature"] == sig_info["signature"]: 123 | return True 124 | except Exception: 125 | return False 126 | return False 127 | 128 | 129 | def create_action_identifier_instruction( 130 | identity: Keypair, reference: Optional[Pubkey] = None 131 | ) -> dict: 132 | if reference is None: 133 | reference = Keypair().public_key 134 | 135 | memo = create_action_identifier_memo(identity, reference) 136 | 137 | return { 138 | "memo": memo, 139 | "reference": reference, 140 | "instruction": Instruction( 141 | program_id=Pubkey.from_string(MEMO_PROGRAM_ID), 142 | data=memo.encode("utf-8"), 143 | keys=[], 144 | ), 145 | } 146 | 147 | 148 | def get_action_identity_from_env(env_key: str = "ACTION_IDENTITY_SECRET") -> Keypair: 149 | import json 150 | import os 151 | 152 | try: 153 | if env_key not in os.environ: 154 | raise ValueError("missing env key") 155 | return Keypair.from_secret_key(bytes(json.loads(os.environ[env_key]))) 156 | except Exception: 157 | raise ValueError(f"invalid identity in env variable: '{env_key}'") 158 | -------------------------------------------------------------------------------- /solana_actions/constants.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from typing import Dict 3 | 4 | SOLANA_PAY_PROTOCOL = "solana:" 5 | SOLANA_ACTIONS_PROTOCOL = "solana-action:" 6 | SOLANA_ACTIONS_PROTOCOL_PLURAL = "solana-actions:" 7 | HTTPS_PROTOCOL = "https:" 8 | MEMO_PROGRAM_ID = "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr" 9 | BLINKS_QUERY_PARAM = "action" 10 | 11 | ACTIONS_CORS_HEADERS: Dict[str, str] = { 12 | "Access-Control-Allow-Origin": "*", 13 | "Access-Control-Allow-Methods": "GET,POST,PUT,OPTIONS", 14 | "Access-Control-Allow-Headers": "Content-Type, Authorization, Content-Encoding, Accept-Encoding", 15 | "Content-Type": "application/json", 16 | } 17 | -------------------------------------------------------------------------------- /solana_actions/create_post_response.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import base64 3 | from typing import List, Optional 4 | 5 | from pydantic import BaseModel, ConfigDict 6 | from solders.keypair import Keypair 7 | from solders.pubkey import Pubkey 8 | 9 | from .action_identity import ( 10 | create_action_identifier_instruction, 11 | get_action_identity_from_env, 12 | ) 13 | from .constants import MEMO_PROGRAM_ID 14 | from .types import ActionPostResponse, Reference 15 | 16 | 17 | class CreatePostResponseError(Exception): 18 | pass 19 | 20 | 21 | class CreateActionPostResponseArgs(BaseModel): 22 | model_config = ConfigDict(arbitrary_types_allowed=True) 23 | fields: ActionPostResponse 24 | signers: Optional[List[Keypair]] = None 25 | action_identity: Optional[Keypair] = None 26 | reference: Optional[Reference] = None 27 | options: Optional[dict] = None 28 | 29 | 30 | def create_post_response( 31 | args: CreateActionPostResponseArgs, 32 | ) -> ActionPostResponse: 33 | transaction = args.fields.transaction 34 | 35 | if not transaction.recent_blockhash: 36 | transaction.recent_blockhash = "11111111111111111111111111111111" 37 | 38 | if not args.action_identity: 39 | try: 40 | args.action_identity = get_action_identity_from_env() 41 | except Exception: 42 | args.action_identity = None 43 | 44 | if len(transaction.instructions) <= 0: 45 | raise CreatePostResponseError("at least 1 instruction is required") 46 | 47 | if args.action_identity: 48 | instruction_data = create_action_identifier_instruction( 49 | args.action_identity, args.reference 50 | ) 51 | transaction.add(instruction_data["instruction"]) 52 | 53 | memo_id = Pubkey.from_string(MEMO_PROGRAM_ID) 54 | non_memo_index = next( 55 | ( 56 | i 57 | for i, ix in enumerate(transaction.instructions) 58 | if ix.program_id != memo_id 59 | ), 60 | -1, 61 | ) 62 | if non_memo_index == -1: 63 | raise CreatePostResponseError( 64 | "transaction requires at least 1 non-memo instruction" 65 | ) 66 | 67 | transaction.instructions[non_memo_index].keys.extend( 68 | [ 69 | { 70 | "pubkey": args.action_identity.public_key, 71 | "is_signer": False, 72 | "is_writable": False, 73 | }, 74 | { 75 | "pubkey": instruction_data["reference"], 76 | "is_signer": False, 77 | "is_writable": False, 78 | }, 79 | ] 80 | ) 81 | 82 | if args.signers: 83 | for signer in args.signers: 84 | transaction.sign(signer) 85 | 86 | args.fields.transaction = base64.b64encode(bytes(transaction._solders)).decode( 87 | "ascii" 88 | ) 89 | return ActionPostResponse(**args.fields.dict()) 90 | -------------------------------------------------------------------------------- /solana_actions/encode_url.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from typing import Union 3 | from urllib.parse import urlencode 4 | 5 | from .constants import BLINKS_QUERY_PARAM, SOLANA_ACTIONS_PROTOCOL 6 | from .types import ( 7 | ActionRequestURLFields, 8 | BlinkURLFields, 9 | SupportedProtocols, 10 | ) 11 | 12 | 13 | class EncodeURLError(Exception): 14 | pass 15 | 16 | 17 | def encode_url( 18 | fields: Union[ActionRequestURLFields, BlinkURLFields], 19 | protocol: SupportedProtocols = SOLANA_ACTIONS_PROTOCOL, 20 | ) -> str: 21 | if isinstance(fields, BlinkURLFields): 22 | return encode_blink_url(fields, protocol) 23 | return encode_action_request_url(fields, protocol) 24 | 25 | 26 | def encode_action_request_url( 27 | fields: ActionRequestURLFields, 28 | protocol: SupportedProtocols = SOLANA_ACTIONS_PROTOCOL, 29 | ) -> str: 30 | 31 | # TODO: do we maybe need a urllib.parse.quote around this? 32 | pathname = str(fields.link).rstrip("/") 33 | print(protocol) 34 | url = f"{protocol}{pathname}" 35 | 36 | params = {} 37 | if fields.label: 38 | params["label"] = fields.label 39 | if fields.message: 40 | params["message"] = fields.message 41 | 42 | if params: 43 | url += "?" + urlencode(params) 44 | 45 | return url 46 | 47 | 48 | def encode_blink_url( 49 | fields: BlinkURLFields, protocol: SupportedProtocols = SOLANA_ACTIONS_PROTOCOL 50 | ) -> str: 51 | blink_url = str(fields.blink) 52 | action_url = encode_action_request_url(fields.action, protocol) 53 | 54 | separator = "&" if "?" in blink_url else "?" 55 | # TODO: do we maybe need a urllib.parse.quote around action_url? 56 | return f"{blink_url}{separator}{BLINKS_QUERY_PARAM}={(action_url)}" 57 | -------------------------------------------------------------------------------- /solana_actions/fetch_transaction.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import base64 3 | from typing import Any, Dict, Optional, Union 4 | 5 | import nacl.signing 6 | import requests 7 | from pydantic import BaseModel, ConfigDict 8 | from solana.rpc.api import Client 9 | from solana.transaction import Transaction 10 | from solders.pubkey import Pubkey 11 | 12 | 13 | class FetchActionError(Exception): 14 | pass 15 | 16 | 17 | class SerializeTransactionError(Exception): 18 | pass 19 | 20 | 21 | class ActionPostRequest: 22 | account: str 23 | 24 | 25 | class ActionPostResponse(BaseModel): 26 | transaction: str 27 | message: Optional[str] = None 28 | 29 | 30 | class ActionPostResponseWithSerializedTransaction(ActionPostResponse): 31 | model_config = ConfigDict(arbitrary_types_allowed=True) 32 | transaction: Transaction 33 | 34 | 35 | def fetch_transaction( 36 | connection: Client, 37 | link: str, 38 | fields: ActionPostRequest, 39 | options: Dict[str, Any] = {}, 40 | ) -> ActionPostResponseWithSerializedTransaction: 41 | response = requests.post( 42 | str(link), 43 | json=fields.dict(), 44 | headers={ 45 | "Accept": "application/json", 46 | "Content-Type": "application/json", 47 | }, 48 | timeout=5, 49 | ) 50 | json_data = response.json() 51 | 52 | if not json_data.get("transaction"): 53 | raise FetchActionError("missing transaction") 54 | if not isinstance(json_data["transaction"], str): 55 | raise FetchActionError("invalid transaction") 56 | 57 | transaction = serialize_transaction( 58 | connection, fields.account, json_data["transaction"], options 59 | ) 60 | 61 | return ActionPostResponseWithSerializedTransaction( 62 | **json_data, transaction=transaction 63 | ) 64 | 65 | 66 | def serialize_transaction( 67 | connection: Client, 68 | account: Union[str, Pubkey], 69 | base64_transaction: str, 70 | options: Dict[str, Any] = {}, 71 | ) -> Transaction: 72 | if isinstance(account, str): 73 | account = Pubkey.from_string(account) 74 | 75 | transaction = Transaction.deserialize(base64.b64decode(base64_transaction)) 76 | signatures = transaction.signatures 77 | fee_payer = transaction.fee_payer() 78 | recent_blockhash = transaction.recent_blockhash 79 | 80 | if signatures: 81 | if not fee_payer: 82 | raise SerializeTransactionError("missing fee payer") 83 | if fee_payer != signatures[0].pubkey: 84 | raise SerializeTransactionError("invalid fee payer") 85 | if not recent_blockhash: 86 | raise SerializeTransactionError("missing recent blockhash") 87 | 88 | message = transaction.serialize_message() 89 | for sig in signatures: 90 | if sig.signature: 91 | if not nacl.signing.VerifyKey(bytes(sig.pubkey)).verify( 92 | message, sig.signature 93 | ): 94 | raise SerializeTransactionError("invalid signature") 95 | elif sig.pubkey == account: 96 | if len(signatures) == 1: 97 | transaction.recent_blockhash = ( 98 | connection.get_recent_blockhash( 99 | commitment=options.get("commitment") 100 | ) 101 | ).value.blockhash 102 | else: 103 | raise SerializeTransactionError("missing signature") 104 | else: 105 | transaction.fee_payer = account 106 | transaction.recent_blockhash = ( 107 | connection.get_recent_blockhash(commitment=options.get("commitment")) 108 | ).value.blockhash 109 | 110 | return transaction 111 | -------------------------------------------------------------------------------- /solana_actions/find_reference.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from typing import Any, Dict 3 | 4 | from solana.rpc.async_api import AsyncClient 5 | from solana.rpc.commitment import Confirmed 6 | from solders.pubkey import Pubkey 7 | 8 | 9 | class FindReferenceError(Exception): 10 | pass 11 | 12 | 13 | async def find_reference( 14 | connection: AsyncClient, reference: Pubkey, options: Dict[str, Any] = {} 15 | ): 16 | finality = options.pop("finality", Confirmed) 17 | 18 | signatures = await connection.get_signatures_for_address( 19 | reference, 20 | limit=options.get("limit", 1000), 21 | before=options.get("before"), 22 | until=options.get("until"), 23 | commitment=finality, 24 | ) 25 | 26 | if not signatures: 27 | raise FindReferenceError("not found") 28 | 29 | oldest = signatures[-1] 30 | 31 | if len(signatures) < options.get("limit", 1000): 32 | return oldest 33 | 34 | try: 35 | return await find_reference( 36 | connection, 37 | reference, 38 | { 39 | "finality": finality, 40 | **options, 41 | "before": oldest.signature, 42 | }, 43 | ) 44 | except FindReferenceError: 45 | return oldest 46 | -------------------------------------------------------------------------------- /solana_actions/parse_url.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from typing import Union 3 | from urllib.parse import parse_qs, unquote, urlparse 4 | 5 | from pydantic import BaseModel 6 | 7 | from .constants import ( 8 | BLINKS_QUERY_PARAM, 9 | HTTPS_PROTOCOL, 10 | SOLANA_ACTIONS_PROTOCOL, 11 | SOLANA_ACTIONS_PROTOCOL_PLURAL, 12 | SOLANA_PAY_PROTOCOL, 13 | ) 14 | 15 | 16 | class ParseURLError(Exception): 17 | pass 18 | 19 | 20 | class ActionRequestURLFields(BaseModel): 21 | link: str 22 | label: str | None = None 23 | message: str | None = None 24 | 25 | 26 | class BlinkURLFields(BaseModel): 27 | blink: str 28 | action: ActionRequestURLFields 29 | 30 | 31 | def parse_url(url: str) -> Union[ActionRequestURLFields, BlinkURLFields]: 32 | if len(url) > 2048: 33 | raise ParseURLError("length invalid") 34 | 35 | parsed_url = urlparse(url) 36 | 37 | if parsed_url.scheme in ("http", "https"): 38 | return parse_blink_url(parsed_url) 39 | 40 | if parsed_url.scheme not in ( 41 | SOLANA_PAY_PROTOCOL[:-1], 42 | SOLANA_ACTIONS_PROTOCOL[:-1], 43 | SOLANA_ACTIONS_PROTOCOL_PLURAL[:-1], 44 | ): 45 | raise ParseURLError("protocol invalid") 46 | 47 | if not parsed_url.path: 48 | raise ParseURLError("pathname missing") 49 | 50 | if not any(char in parsed_url.path for char in [":", "%"]): 51 | raise ParseURLError("pathname invalid") 52 | 53 | return parse_action_request_url(parsed_url) 54 | 55 | 56 | def parse_action_request_url(parsed_url) -> ActionRequestURLFields: 57 | path = unquote(parsed_url.path) 58 | link = urlparse(path) 59 | if link.scheme != HTTPS_PROTOCOL[:-1]: 60 | raise ParseURLError("link invalid") 61 | 62 | query_params = parse_qs(parsed_url.query) 63 | 64 | return ActionRequestURLFields( 65 | link=path, 66 | label=query_params.get("label", [None])[0], 67 | message=query_params.get("message", [None])[0], 68 | ) 69 | 70 | 71 | def parse_blink_url(parsed_url) -> BlinkURLFields: 72 | query_params = parse_qs(parsed_url.query) 73 | link = query_params.get(BLINKS_QUERY_PARAM, [None])[0] 74 | 75 | if not link: 76 | raise ParseURLError("invalid blink url") 77 | 78 | return BlinkURLFields(blink=(parsed_url.geturl()), action=parse_url(link)) 79 | -------------------------------------------------------------------------------- /solana_actions/types.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from typing import List, Literal, Optional 3 | 4 | from pydantic import BaseModel, ConfigDict 5 | from solana.transaction import Transaction 6 | from solders.pubkey import Pubkey 7 | 8 | 9 | # `reference` in the [Solana Actions spec](https://github.com/solana-labs/solana-pay/blob/master/SPEC.md#reference). 10 | class Reference(Pubkey): 11 | ... 12 | 13 | 14 | # `memo` in the [Solana Actions spec](https://github.com/solana-labs/solana-pay/blob/master/SPEC.md#memo 15 | class Memo(str): 16 | ... 17 | 18 | 19 | class ActionsJson(BaseModel): 20 | rules: List["ActionRuleObject"] 21 | 22 | 23 | class ActionRuleObject(BaseModel): 24 | pathPattern: str 25 | apiPath: str 26 | 27 | 28 | class ActionRequestURLFields(BaseModel): 29 | link: str 30 | label: Optional[str] = None 31 | message: Optional[str] = None 32 | 33 | 34 | class BlinkURLFields(BaseModel): 35 | blink: str 36 | action: ActionRequestURLFields 37 | 38 | 39 | class ActionParameter(BaseModel): 40 | name: str 41 | label: Optional[str] = None 42 | required: Optional[bool] = False 43 | 44 | 45 | class LinkedAction(BaseModel): 46 | href: str 47 | label: str 48 | parameters: Optional[List[ActionParameter]] = None 49 | 50 | 51 | class ActionError(BaseModel): 52 | message: str 53 | 54 | 55 | class ActionGetResponse(BaseModel): 56 | icon: str 57 | title: str 58 | description: str 59 | label: str 60 | disabled: Optional[bool] = None 61 | links: Optional[dict] = None 62 | error: Optional[ActionError] = None 63 | 64 | 65 | class ActionPostRequest(BaseModel): 66 | account: str 67 | 68 | 69 | class ActionPostResponse(BaseModel): 70 | model_config = ConfigDict(arbitrary_types_allowed=True) 71 | transaction: Transaction | str 72 | message: Optional[str] = None 73 | 74 | 75 | # Pydantic cannot use constants loaded from .constants here, unfortunately 76 | SupportedProtocols: Optional[ 77 | Literal["solana-actions://", "solana-action://", "solana://"] 78 | ] = None 79 | ActionsJson.update_forward_refs() 80 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/conka8/Python-solana-actions/44544cd44519efd1fc7301ebc3e6f92d97980f77/tests/conftest.py -------------------------------------------------------------------------------- /tests/test_create_post_response.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from solana_actions import create_post_response 3 | 4 | 5 | def test_create_post_response_import(): 6 | assert create_post_response 7 | -------------------------------------------------------------------------------- /tests/test_encode_url.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from urllib.parse import parse_qs, urlparse 3 | 4 | from solana_actions.encode_url import ( 5 | ActionRequestURLFields, 6 | BlinkURLFields, 7 | encode_url, 8 | ) 9 | 10 | 11 | def test_action_request_url_without_params(): 12 | link = "https://example.com/api/action" 13 | url = encode_url(ActionRequestURLFields(link=link)) 14 | assert str(url) == f"solana-action:{link}" 15 | 16 | 17 | def test_action_request_url_with_params(): 18 | link = "https://example.com/api/action" 19 | label = "label" 20 | message = "message" 21 | url = encode_url(ActionRequestURLFields(link=link, label=label, message=message)) 22 | assert str(url) == f"solana-action:{link}?label={label}&message={message}" 23 | 24 | 25 | def test_action_request_url_with_query_params(): 26 | link = "https://example.com/api/action?query=param" 27 | url = encode_url(ActionRequestURLFields(link=link)) 28 | assert str(url) == f"solana-action:{(link)}" 29 | 30 | 31 | def test_action_request_url_with_query_and_action_params(): 32 | link = "https://example.com/api/action?query=param&amount=1337" 33 | label = "label" 34 | message = "message" 35 | url = encode_url(ActionRequestURLFields(link=link, label=label, message=message)) 36 | assert str(url) == f"solana-action:{(link)}?label={label}&message={message}" 37 | 38 | 39 | def test_blink_url_without_action_params(): 40 | blink = "https://blink.com/" 41 | link = "https://action.com/api/action" 42 | url = encode_url( 43 | BlinkURLFields(blink=blink, action=ActionRequestURLFields(link=link)) 44 | ) 45 | parsed_url = urlparse(url) 46 | query_params = parse_qs(parsed_url.query) 47 | assert query_params["action"][0] == (f"solana-action:{link}") 48 | 49 | 50 | def test_blink_url_with_action_params(): 51 | blink = "https://blink.com/" 52 | link = "https://action.com/api/action?query=param" 53 | url = encode_url( 54 | BlinkURLFields(blink=blink, action=ActionRequestURLFields(link=link)) 55 | ) 56 | parsed_url = urlparse(url) 57 | query_params = parse_qs(parsed_url.query) 58 | assert query_params["action"][0] == (f"solana-action:{(link)}") 59 | 60 | 61 | def test_blink_url_with_query_params_without_action_params(): 62 | blink = "https://blink.com/?other=one" 63 | link = "https://action.com/api/action" 64 | url = encode_url( 65 | BlinkURLFields(blink=blink, action=ActionRequestURLFields(link=link)) 66 | ) 67 | parsed_url = urlparse(url) 68 | query_params = parse_qs(parsed_url.query) 69 | assert query_params["action"][0] == (f"solana-action:{link}") 70 | 71 | 72 | def test_blink_url_with_query_params_and_action_params(): 73 | blink = "https://blink.com/?other=one" 74 | link = "https://action.com/api/action?query=param" 75 | url = encode_url( 76 | BlinkURLFields(blink=blink, action=ActionRequestURLFields(link=link)) 77 | ) 78 | parsed_url = urlparse(url) 79 | query_params = parse_qs(parsed_url.query) 80 | assert query_params["action"][0] == (f"solana-action:{(link)}") 81 | -------------------------------------------------------------------------------- /tests/test_fetch_transaction.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from solana_actions import fetch_transaction 3 | 4 | 5 | def test_fetch_transaction_import(): 6 | assert fetch_transaction 7 | -------------------------------------------------------------------------------- /tests/test_find_transaction_signature.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pytest 3 | from solana.rpc.async_api import AsyncClient 4 | from solders.keypair import Keypair 5 | from solders.pubkey import Pubkey 6 | 7 | from solana_actions.find_reference import ( 8 | FindReferenceError, 9 | find_reference, 10 | ) 11 | 12 | 13 | @pytest.fixture 14 | def mock_connection(): 15 | reference = Keypair().pubkey 16 | signatures_for_address = {str(reference): [{"signature": "signature"}]} 17 | 18 | class MockConnection(AsyncClient): 19 | async def get_signatures_for_address(self, reference: Pubkey, **kwargs): 20 | return signatures_for_address.get(str(reference), []) 21 | 22 | return MockConnection("http://localhost:8899"), reference 23 | 24 | 25 | @pytest.mark.asyncio 26 | async def test_find_reference_returns_last_signature(mock_connection): 27 | connection, reference = mock_connection 28 | found = await find_reference(connection, reference) 29 | assert found == {"signature": "signature"} 30 | 31 | 32 | @pytest.mark.asyncio 33 | async def test_find_reference_throws_error_on_signature_not_found(mock_connection): 34 | connection, _ = mock_connection 35 | reference = Keypair().pubkey 36 | 37 | with pytest.raises(FindReferenceError, match="not found"): 38 | await find_reference(connection, reference) 39 | -------------------------------------------------------------------------------- /tests/test_parse_url.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from urllib.parse import parse_qs, quote, urlparse 3 | 4 | import pytest 5 | 6 | from solana_actions.parse_url import ( 7 | ActionRequestURLFields, 8 | BlinkURLFields, 9 | ParseURLError, 10 | parse_url, 11 | ) 12 | 13 | 14 | def test_allow_solana_pay_protocol(): 15 | url = "solana:https://example.com/api/action" 16 | result = parse_url(url) 17 | assert isinstance(result, ActionRequestURLFields) 18 | assert str(result.link) == "https://example.com/api/action" 19 | 20 | 21 | def test_allow_solana_action_protocol(): 22 | url = "solana-action:https://example.com/api/action" 23 | result = parse_url(url) 24 | assert isinstance(result, ActionRequestURLFields) 25 | assert str(result.link) == "https://example.com/api/action" 26 | 27 | 28 | def test_parse_simple_action_url(): 29 | url = "solana-action:https://example.com/api/action" 30 | result = parse_url(url) 31 | assert isinstance(result, ActionRequestURLFields) 32 | assert str(result.link) == "https://example.com/api/action" 33 | 34 | 35 | def test_parse_action_url_with_action_params(): 36 | url = "solana-action:https%3A%2F%2Fexample.com%2Fapi%2Faction%3Famount%3D1337%26another%3Dyes" 37 | result = parse_url(url) 38 | assert isinstance(result, ActionRequestURLFields) 39 | assert str(result.link) == "https://example.com/api/action?amount=1337&another=yes" 40 | 41 | 42 | def test_parse_action_url_with_extra_action_params(): 43 | url = "solana-action:https://example.com/api/action?label=Michael&message=Thanks%20for%20all%20the%20fish" 44 | result = parse_url(url) 45 | assert isinstance(result, ActionRequestURLFields) 46 | assert str(result.link) == "https://example.com/api/action" 47 | assert result.label == "Michael" 48 | assert result.message == "Thanks for all the fish" 49 | 50 | 51 | def test_parse_action_url_with_query_params_and_action_params(): 52 | url = "solana-action:https%3A%2F%2Fexample.com%2Fapi%2Faction%3Famount%3D1337%26another%3Dyes?label=Michael&message=Thanks%20for%20all%20the%20fish" 53 | result = parse_url(url) 54 | assert isinstance(result, ActionRequestURLFields) 55 | assert str(result.link) == "https://example.com/api/action?amount=1337&another=yes" 56 | assert result.label == "Michael" 57 | assert result.message == "Thanks for all the fish" 58 | 59 | 60 | def test_parse_blinks_without_action_query_params(): 61 | action_link = "https://action.com/api/action" 62 | action_url = f"solana-action:{quote(action_link)}" 63 | url = f"https://blink.com/?other=one&action={quote(action_url)}" 64 | 65 | result = parse_url(url) 66 | assert isinstance(result, BlinkURLFields) 67 | assert str(result.blink) == url 68 | assert parse_qs(urlparse(result.blink).query)["action"][0] == action_url 69 | assert str(result.action.link) == action_link 70 | 71 | 72 | def test_parse_blinks_with_action_query_params(): 73 | action_link = "https://action.com/api/action?query=param" 74 | action_url = f"solana-action:{quote(action_link)}?label=Michael&message=Thanks%20for%20all%20the%20fish" 75 | url = f"https://blink.com/?other=one&action={quote(action_url)}" 76 | 77 | result = parse_url(url) 78 | assert isinstance(result, BlinkURLFields) 79 | assert str(result.blink) == url 80 | assert parse_qs(urlparse(result.blink).query)["action"][0] == action_url 81 | assert str(result.action.link) == action_link 82 | assert result.action.label == "Michael" 83 | assert result.action.message == "Thanks for all the fish" 84 | 85 | 86 | def test_error_on_invalid_length(): 87 | url = "X" * 2049 88 | with pytest.raises(ParseURLError, match="length invalid"): 89 | parse_url(url) 90 | 91 | 92 | def test_error_on_invalid_protocol(): 93 | url = "eth:0xffff" 94 | with pytest.raises(ParseURLError, match="protocol invalid"): 95 | parse_url(url) 96 | 97 | 98 | def test_error_on_missing_pathname(): 99 | url = "solana-action:" 100 | with pytest.raises(ParseURLError, match="pathname missing"): 101 | parse_url(url) 102 | 103 | 104 | def test_error_on_invalid_pathname(): 105 | url = "solana-action:0xffff" 106 | with pytest.raises(ParseURLError, match="pathname invalid"): 107 | parse_url(url) 108 | 109 | 110 | def test_error_on_invalid_blink_urls(): 111 | url = "https://blink.com/?other=one&action=" 112 | with pytest.raises(ParseURLError, match="invalid blink url"): 113 | parse_url(url) 114 | 115 | 116 | def test_error_on_invalid_protocol_in_blink_action_param(): 117 | url = "https://blink.com/?other=one&action=unknown-protocol%3Ahttps%253A%252F%252Faction.com%252Fapi%252Faction%253Fquery%253Dparam" 118 | with pytest.raises(ParseURLError, match="protocol invalid"): 119 | parse_url(url) 120 | 121 | 122 | def test_error_on_invalid_link_in_blink_action_param(): 123 | url = "https://blink.com/?other=one&action=solana-action%3Aftp%253A%252F%252Faction.com%252Fapi%252Faction%253Fquery%253Dparam" 124 | with pytest.raises(ParseURLError, match="link invalid"): 125 | parse_url(url) 126 | --------------------------------------------------------------------------------