1 Commits

Author SHA1 Message Date
Lars Immisch d3aa602a04 Setup PCM device in constructor. 2020-04-02 23:24:48 +02:00
34 changed files with 3362 additions and 5586 deletions
-40
View File
@@ -1,40 +0,0 @@
on:
workflow_dispatch:
push:
branches:
- mttbernardini/chore/modern-packaging
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Install uv
uses: astral-sh/setup-uv@v6
- name: Install ALSA headers
run: sudo apt-get install libasound2-dev
- name: Build HTML documentation
run: make -C doc html
- name: Upload artifacts
uses: actions/upload-pages-artifact@v4
with:
path: doc/html
deploy:
runs-on: ubuntu-latest
needs: build
permissions:
contents: read
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
-59
View File
@@ -1,59 +0,0 @@
# Adapted from: https://github.com/pypa/cibuildwheel/blob/main/examples/github-deploy.yml
on:
workflow_dispatch:
push:
tags: "*"
jobs:
build_wheels:
name: Build wheels on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
os:
- ubuntu-24.04
- ubuntu-24.04-arm
steps:
- uses: actions/checkout@v5
- name: Install uv
uses: astral-sh/setup-uv@v6
- name: Build wheels
run: make build_wheels
- uses: actions/upload-artifact@v4
with:
name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }}
path: ./wheelhouse/*.whl
build_sdist:
name: Build source distribution
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Install uv
uses: astral-sh/setup-uv@v6
- name: Build sdist
run: make build_sdist
- uses: actions/upload-artifact@v4
with:
name: cibw-sdist
path: dist/*.tar.gz
upload_pypi:
needs: [build_wheels, build_sdist]
runs-on: ubuntu-latest
# TODO: set environment back to pypi after testing
environment: testpypi
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v5
with:
# unpacks all CIBW artifacts into dist/
pattern: cibw-*
path: dist
merge-multiple: true
- uses: pypa/gh-action-pypi-publish@release/v1
# TODO: delete below once workflow is proved functional
with:
repository-url: https://test.pypi.org/legacy/
-6
View File
@@ -4,12 +4,6 @@ MANIFEST
doc/gh-pages/ doc/gh-pages/
doc/html/ doc/html/
doc/doctrees/ doc/doctrees/
doc/_build/
gh-pages/ gh-pages/
build/ build/
dist/ dist/
wheelhouse/
.vscode/
/__pycache__/
*.egg-info/
*.raw
+99
View File
@@ -0,0 +1,99 @@
Version 0.8.6:
- Added four methods to the 'PCM' class to allow users to get detailed information about the device:
- 'getformats()' returns a dictionary of name / value pairs, one for each of the card's
supported formats - e.g. '{"U8": 1, "S16_LE": 2}',
- 'getchannels()' returns a list of the supported channel numbers, e.g. '[1, 2]',
- 'getrates()' returns supported sample rates for the device, e.g. '[48000]',
- 'getratebounds()' returns the device's official minimum and maximum supported
sample rates as a tuple, e.g. '(4000, 48000)'.
(#82 contributed by @jdstmporter)
- Prevent hang on close after capturing audio (#80 contributed by @daym)
Version 0.8.5:
- Return an empty string/bytestring when 'read()' detects an
overrun. Previously the returned data was undefined (contributed by @jcea)
- Unlimited setperiod buffer size when reading frames (contributed by @jcea)
Version 0.8.4:
- Fix Python3 API usage broken in 0.8.3
Version 0.8.3:
- Add DSD sample formats (contributed by @lintweaker)
- Add Mixer.handleevents() to acknowledge events identified by select.poll (contributed by @PaulSD)
- Add functions for listing cards and their names (contributed by @chrisdiamand)
- Add a method for setting enums (contributed by @chrisdiamand)
Version 0.8.2:
- fix #3 (we cannot get the revision from git for pip installs)
Version 0.8.1:
- document changes (this file)
Version 0.8:
- 'PCM()' has new 'device' and 'cardindex' keyword arguments.
The keyword 'device' allows to select virtual devices, 'cardindex' can be
used to select hardware cards by index (as with 'mixers()' and 'Mixer()').
The 'card' keyword argument is still supported, but deprecated.
The reason for this change is that the 'card' keyword argument guessed
a device name from the card name, but this only works sometimes, and breaks
opening virtual devices.
- new function 'pcms()' to list available PCM devices.
- mixers() and Mixer() take an additional 'device' keyword argument.
This allows to list or open virtual devices.
- The default behaviour of Mixer() without any arguments has changed.
Now Mixer() will try to open the 'default' Mixer instead of the Mixer
that is associated with card 0.
- fix a memory leak under Python 3.x
- some more memory leaks in error conditions fixed.
Version 0.7:
- fixed several memory leaks (patch 3372909), contributed by Erik Kulyk)
Version 0.6:
- mostly reverted patch 2594366: alsapcm_setup did not do complete error
checking for good reasons; some ALSA functions in alsapcm_setup may fail without
rendering the device unusable
Version 0.5:
- applied patch 2777035: Fixed setrec method in alsaaudio.c
This included a mixertest with more features
- fixed/applied patch 2594366: alsapcm_setup does not do any error checking
Version 0.4:
- API changes: mixers() and Mixer() now take a card index instead of a
card name as optional parameter.
- Support for Python 3.0
- Documentation converted to reStructuredText; use Sphinx instead of LaTeX.
- added cards()
- added PCM.close()
- added Mixer.close()
- added mixer.getenum()
Version 0.3:
- wrapped blocking calls with Py_BEGIN_ALLOW_THREADS/Py_END_ALLOW_THREADS
- added pause
Version 0.2:
- Many bugfixes related to playback in particular
- Module documentation in the doc subdirectory
Version 0.1:
- Initial version
-127
View File
@@ -1,127 +0,0 @@
# Version 0.11.0
- Fixed `Mixer.getvolume()` returning outdated value (#126)
- Fixed PCM crashing with some sample formats due to buffer size
miscalculation
- Fixed `PCM.read()` ignoring overruns (regression in 0.10.0)
- Reverted to `PCM.write()` not throwing an exception on playback buffer
underrun; instead, return -EPIPE like `PCM.read()` does on overrun (#130)
- Added `PCM.avail()` and `PCM.polldescriptors_revents()` functions
- Added `nominal_bits` and `physical_bits` entries to `PCM.info()`'s
return value
- Added Python type hint file, and adjusted documentation accordingly (#58)
- Improvements to the examples, in particular isine.py (#42)
Contributions by @ossilator and @viinikv.
# Version 0.10.0
- assorted improvements (#123 from @ossilator)
- support for `periods` in the `PCM` constructor.
- new functions `PCM.state()`, `PCM.drop()` and `PCM.drain()`
- improved underrun/overrun handling
- documentation improvements/consolidation (docstrings were removed in favour of online documentation)
- more sampling rates
- bug fixes
# Version 0.9.2
- Fix alsamixer_getvolume (#112 from @stephensp)
# Version 0.9.1:
- Support decibel, percentage, and raw volumes in getvolume, setvolume, and getrange (#109 from @chrisdiamand)
# Version 0.9.0:
- Added keyword arguments for channels, format, rate and periodsize
- Deprecated `setchannel`, `setformat`, `setrate` and `setperiodsize`
# Version 0.8.6:
- Added four methods to the `PCM` class to allow users to get detailed information about the device:
- `getformats()` returns a dictionary of name / value pairs, one for each of the card's
supported formats - e.g. `{"U8": 1, "S16_LE": 2}`,
- `getchannels()` returns a list of the supported channel numbers, e.g. `[1, 2]`,
- `getrates()` returns supported sample rates for the device, e.g. `[48000]`,
- `getratebounds()` returns the device's official minimum and maximum supported
sample rates as a tuple, e.g. `(4000, 48000)`.
(#82 contributed by @jdstmporter)
- Prevent hang on close after capturing audio (#80 contributed by @daym)
# Version 0.8.5:
- Return an empty string/bytestring when `read()` detects an
overrun. Previously the returned data was undefined (contributed by @jcea)
- Unlimited setperiod buffer size when reading frames (contributed by @jcea)
# Version 0.8.4:
- Fix Python3 API usage broken in 0.8.3
# Version 0.8.3:
- Add DSD sample formats (contributed by @lintweaker)
- Add Mixer.handleevents() to acknowledge events identified by select.poll (contributed by @PaulSD)
- Add functions for listing cards and their names (contributed by @chrisdiamand)
- Add a method for setting enums (contributed by @chrisdiamand)
# Version 0.8.2:
- fix #3 (we cannot get the revision from git for pip installs)
# Version 0.8.1:
- document changes (this file)
# Version 0.8:
- `PCM()` has new `device` and `cardindex` keyword arguments.
The keyword `device` allows to select virtual devices, `cardindex` can be
used to select hardware cards by index (as with `mixers()` and `Mixer()`).
The `card` keyword argument is still supported, but deprecated.
The reason for this change is that the `card` keyword argument guessed
a device name from the card name, but this only works sometimes, and breaks
opening virtual devices.
- new function `pcms()` to list available PCM devices.
- `mixers()` and `Mixer()` take an additional `device` keyword argument.
This allows to list or open virtual devices.
- The default behaviour of `Mixer()` without any arguments has changed.
Now Mixer() will try to open the `default` Mixer instead of the Mixer
that is associated with card 0.
- fix a memory leak under Python 3.x
- some more memory leaks in error conditions fixed.
# Version 0.7:
- fixed several memory leaks (patch 3372909), contributed by Erik Kulyk)
# Version 0.6:
- mostly reverted patch 2594366: alsapcm_setup did not do complete error
checking for good reasons; some ALSA functions in alsapcm_setup may fail without
rendering the device unusable
# Version 0.5:
- applied patch 2777035: Fixed setrec method in alsaaudio.c
This included a mixertest with more features
- fixed/applied patch 2594366: alsapcm_setup does not do any error checking
# Version 0.4:
- API changes: mixers() and Mixer() now take a card index instead of a
card name as optional parameter.
- Support for Python 3.0
- Documentation converted to reStructuredText; use Sphinx instead of LaTeX.
- added `cards()`
- added `PCM.close()`
- added `Mixer.close()`
- added `mixer.getenum()`
# Version 0.3:
- wrapped blocking calls with `Py_BEGIN_ALLOW_THREADS`/`Py_END_ALLOW_THREADS`
- added pause
# Version 0.2:
- Many bugfixes related to playback in particular
- Module documentation in the doc subdirectory
# Version 0.1:
- Initial version
+5 -2
View File
@@ -1,2 +1,5 @@
graft doc/ include *.py
graft examples/ include CHANGES
include TODO
include LICENSE
recursive-include doc *.html *.gif *.png *.css *.py *.rst *.js *.json Makefile
-16
View File
@@ -1,16 +0,0 @@
.PHONY: lint test build_wheels build_sdist build
lint:
uv run --group lint pyright; \
uv run --group lint stubtest alsaaudio
test:
uv run tests/test.py
build_wheels:
uvx cibuildwheel==3.1.4
build_sdist:
uv build --sdist
build: build_sdist build_wheels
+11
View File
@@ -0,0 +1,11 @@
# Publishing the documentation
- Install Sphinx; `sudo pip install sphinx`
- Clone gh-pages branch: `cd doc; git clone -b gh-pages git@github.com:larsimmisch/pyalsaaudio.git gh-pages`
- `cd doc; make publish`
# Release procedure
- Update version number in setup.py
- Create tag and push it, i.e. `git tag x.y.z; git push origin x.y.z`
- `python setup.py sdist upload -r pypi`
+2626
View File
File diff suppressed because it is too large Load Diff
-22
View File
@@ -1,22 +0,0 @@
#!/bin/sh
# NOTE: ensure to use the correct package manager depending on the manylinux
# image used.
if which yum; then
yum install -y alsa-lib-devel
exit
fi
if which apk; then
apk add alsa-lib-dev
exit
fi
if which apt-get; then
apt-get install -y libasound2-dev
exit
fi
echo "Cannot install ALSA headers! No package manager detected"
exit 1
+7 -1
View File
@@ -3,7 +3,7 @@
# You can set these variables from the command line. # You can set these variables from the command line.
SPHINXOPTS = SPHINXOPTS =
SPHINXBUILD = uv run --group doc sphinx-build SPHINXBUILD = sphinx-build
PAPER = PAPER =
# Internal variables. # Internal variables.
@@ -70,3 +70,9 @@ linkcheck:
@echo @echo
@echo "Link check complete; look for any errors in the above output " \ @echo "Link check complete; look for any errors in the above output " \
"or in linkcheck/output.txt." "or in linkcheck/output.txt."
gh-pages: html
cd gh-pages; git pull --rebase; cd ..; cp -r ./html/* gh-pages
publish: gh-pages
cd gh-pages; git commit -a; git push; cd ..
-23
View File
@@ -1,26 +1,3 @@
# Make a new release
Update the version in setup.py
pyalsa_version = '0.9.0'
Commit and push the update.
Create and push a tag naming the version (i.e. 0.9.0):
git tag 0.9.0
git push origin 0.9.0
Create the package:
python3 setup.py sdist
Upload the package
twine upload dist/*
Don't forget to update the documentation.
# Publish the documentation # Publish the documentation
The documentation is published through the `gh-pages` branch. The documentation is published through the `gh-pages` branch.
+4 -3
View File
@@ -20,8 +20,9 @@
# import sys # import sys
# sys.path.insert(0, os.path.abspath('.')) # sys.path.insert(0, os.path.abspath('.'))
from importlib.metadata import version import sys
pyalsa_version = version("pyalsaaudio") sys.path.insert(0, '..')
from setup import pyalsa_version
# -- General configuration ------------------------------------------------ # -- General configuration ------------------------------------------------
@@ -66,7 +67,7 @@ release = version
# #
# This is also used if you do content translation via gettext catalogs. # This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases. # Usually you set "language" from the command line for these cases.
language = 'en' language = None
# List of patterns, relative to source directory, that match files and # List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files. # directories to ignore when looking for source files.
+9
View File
@@ -1,3 +1,8 @@
.. alsaaudio documentation documentation master file, created by
sphinx-quickstart on Thu Mar 30 23:52:21 2017.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
alsaaudio documentation alsaaudio documentation
=================================================== ===================================================
@@ -14,12 +19,14 @@ Download
* `Download from pypi <https://pypi.python.org/pypi/pyalsaaudio>`_ * `Download from pypi <https://pypi.python.org/pypi/pyalsaaudio>`_
Github Github
====== ======
* `Repository <https://github.com/larsimmisch/pyalsaaudio/>`_ * `Repository <https://github.com/larsimmisch/pyalsaaudio/>`_
* `Bug tracker <https://github.com/larsimmisch/pyalsaaudio/issues>`_ * `Bug tracker <https://github.com/larsimmisch/pyalsaaudio/issues>`_
Indices and tables Indices and tables
================== ==================
@@ -27,3 +34,5 @@ Indices and tables
* :ref:`modindex` * :ref:`modindex`
* :ref:`search` * :ref:`search`
+170 -390
View File
@@ -5,18 +5,41 @@
.. module:: alsaaudio .. module:: alsaaudio
:platform: Linux :platform: Linux
.. % \declaremodule{builtin}{alsaaudio} % standard library, in C
.. % not standard, in C
.. moduleauthor:: Casper Wilstrup <cwi@aves.dk> .. moduleauthor:: Casper Wilstrup <cwi@aves.dk>
.. moduleauthor:: Lars Immisch <lars@ibp.de> .. moduleauthor:: Lars Immisch <lars@ibp.de>
.. % Author of the module code;
The :mod:`alsaaudio` module defines functions and classes for using ALSA. The :mod:`alsaaudio` module defines functions and classes for using ALSA.
.. function:: pcms(pcmtype: int = PCM_PLAYBACK) ->list[str] .. % ---- 3.1. ----
.. % For each function, use a ``funcdesc'' block. This has exactly two
.. % parameters (each parameters is contained in a set of curly braces):
.. % the first parameter is the function name (this automatically
.. % generates an index entry); the second parameter is the function's
.. % argument list. If there are no arguments, use an empty pair of
.. % curly braces. If there is more than one argument, separate the
.. % arguments with backslash-comma. Optional parts of the parameter
.. % list are contained in \optional{...} (this generates a set of square
.. % brackets around its parameter). Arguments are automatically set in
.. % italics in the parameter list. Each argument should be mentioned at
.. % least once in the description; each usage (even inside \code{...})
.. % should be enclosed in \var{...}.
.. function:: pcms([type=PCM_PLAYBACK])
List available PCM devices by name. List available PCM devices by name.
Arguments are: Arguments are:
* *pcmtype* - can be either :const:`PCM_CAPTURE` or :const:`PCM_PLAYBACK` * *type* - can be either :const:`PCM_CAPTURE` or :const:`PCM_PLAYBACK`
(default). (default).
**Note:** **Note:**
@@ -34,19 +57,14 @@ The :mod:`alsaaudio` module defines functions and classes for using ALSA.
*New in 0.8* *New in 0.8*
.. function:: cards() -> list[str] .. function:: cards()
List the available ALSA cards by name. This function is only moderately List the available ALSA cards by name. This function is only moderately
useful. If you want to see a list of available PCM devices, use :func:`pcms` useful. If you want to see a list of available PCM devices, use :func:`pcms`
instead. instead.
..
Omitted by intention due to being superseded by cards():
.. function:: card_indexes() .. function:: mixers(cardindex=-1, device='default')
.. function:: card_name()
.. function:: mixers(cardindex: int = -1, device: str = 'default') -> list[str]
List the available mixers. The arguments are: List the available mixers. The arguments are:
@@ -55,8 +73,6 @@ The :mod:`alsaaudio` module defines functions and classes for using ALSA.
the `device` keyword argument is ignored. ``0`` is the first hardware sound the `device` keyword argument is ignored. ``0`` is the first hardware sound
card. card.
**Note:** This should not be used, as it bypasses most of ALSA's configuration.
* *device* - the name of the device on which the mixer resides. The default * *device* - the name of the device on which the mixer resides. The default
is ``'default'``. is ``'default'``.
@@ -82,10 +98,6 @@ The :mod:`alsaaudio` module defines functions and classes for using ALSA.
changed. Since 0.8, this functions returns the mixers for the default changed. Since 0.8, this functions returns the mixers for the default
device, not the mixers for the first card. device, not the mixers for the first card.
.. function:: asoundlib_version() -> str
Return a Python string containing the ALSA version found.
.. _pcm-objects: .. _pcm-objects:
@@ -96,27 +108,87 @@ PCM objects in :mod:`alsaaudio` can play or capture (record) PCM
sound through speakers or a microphone. The PCM constructor takes the sound through speakers or a microphone. The PCM constructor takes the
following arguments: following arguments:
.. class:: PCM(type: int = PCM_PLAYBACK, mode: int = PCM_NORMAL, rate: int = 44100, channels: int = 2, .. class:: PCM(type=PCM_PLAYBACK, mode=PCM_NORMAL, device='default', cardindex=-1)
format: int = PCM_FORMAT_S16_LE, periodsize: int = 32, periods: int = 4,
device: str = 'default', cardindex: int = -1) -> PCM
This class is used to represent a PCM device (either for playback or This class is used to represent a PCM device (either for playback and
recording). The constructor's arguments are: recording). The arguments are:
* *type* - can be either :const:`PCM_CAPTURE` or :const:`PCM_PLAYBACK` * *type* - can be either :const:`PCM_CAPTURE` or :const:`PCM_PLAYBACK`
(default). (default).
* *mode* - can be either :const:`PCM_NONBLOCK`, or :const:`PCM_NORMAL` * *mode* - can be either :const:`PCM_NONBLOCK`, or :const:`PCM_NORMAL`
(default). (default).
* *rate* - the sampling rate in Hz. Typical values are ``8000`` (mainly used for telephony), ``16000``, ``44100`` (default), ``48000`` and ``96000``. * *device* - the name of the PCM device that should be used (for example
* *channels* - the number of channels. The default value is 2 (stereo). a value from the output of :func:`pcms`). The default value is
* *format* - the data format. This controls how the PCM device interprets data for playback, and how data is encoded in captures. ``'default'``.
The default value is :const:`PCM_FORMAT_S16_LE`. * *cardindex* - the card index. If this argument is given, the device name
is constructed as 'hw:*cardindex*' and
the `device` keyword argument is ignored.
``0`` is the first hardware sound card.
This will construct a PCM object with these default settings:
* Sample format: :const:`PCM_FORMAT_S16_LE`
* Rate: 44100 Hz
* Channels: 2
* Period size: 32 frames
*Changed in 0.8:*
- The `card` keyword argument is still supported,
but deprecated. Please use `device` instead.
- The keyword argument `cardindex` was added.
The `card` keyword is deprecated because it guesses the real ALSA
name of the card. This was always fragile and broke some legitimate usecases.
PCM objects have the following methods:
.. method:: PCM.pcmtype()
Returns the type of PCM object. Either :const:`PCM_CAPTURE` or
:const:`PCM_PLAYBACK`.
.. method:: PCM.pcmmode()
Return the mode of the PCM object. One of :const:`PCM_NONBLOCK`,
:const:`PCM_ASYNC`, or :const:`PCM_NORMAL`
.. method:: PCM.cardname()
Return the name of the sound card used by this PCM object.
.. method:: PCM.setchannels(nchannels)
Used to set the number of capture or playback channels. Common
values are: ``1`` = mono, ``2`` = stereo, and ``6`` = full 6 channel audio.
Few sound cards support more than 2 channels
.. method:: PCM.setrate(rate)
Set the sample rate in Hz for the device. Typical values are ``8000``
(mainly used for telephony), ``16000``, ``44100`` (CD quality),
``48000`` and ``96000``.
.. method:: PCM.setformat(format)
The sound *format* of the device. Sound format controls how the PCM device
interpret data for playback, and how data is encoded in captures.
The following formats are provided by ALSA:
========================= =============== ========================= ===============
Format Description Format Description
========================= =============== ========================= ===============
``PCM_FORMAT_S8`` Signed 8 bit samples for each channel ``PCM_FORMAT_S8`` Signed 8 bit samples for each channel
``PCM_FORMAT_U8`` Unsigned 8 bit samples for each channel ``PCM_FORMAT_U8`` Signed 8 bit samples for each channel
``PCM_FORMAT_S16_LE`` Signed 16 bit samples for each channel Little Endian byte order) ``PCM_FORMAT_S16_LE`` Signed 16 bit samples for each channel Little Endian byte order)
``PCM_FORMAT_S16_BE`` Signed 16 bit samples for each channel (Big Endian byte order) ``PCM_FORMAT_S16_BE`` Signed 16 bit samples for each channel (Big Endian byte order)
``PCM_FORMAT_U16_LE`` Unsigned 16 bit samples for each channel (Little Endian byte order) ``PCM_FORMAT_U16_LE`` Unsigned 16 bit samples for each channel (Little Endian byte order)
@@ -144,211 +216,16 @@ following arguments:
``PCM_FORMAT_U24_3BE`` Unsigned 24 bit samples for each channel (Big Endian byte order in 3 bytes) ``PCM_FORMAT_U24_3BE`` Unsigned 24 bit samples for each channel (Big Endian byte order in 3 bytes)
========================= =============== ========================= ===============
* *periodsize* - the period size in frames.
Make sure you understand :ref:`the meaning of periods <term-period>`.
The default value is 32, which is below the actual minimum of most devices,
and will therefore likely be larger in practice.
* *periods* - the number of periods in the buffer. The default value is 4.
* *device* - the name of the PCM device that should be used (for example
a value from the output of :func:`pcms`). The default value is
``'default'``.
* *cardindex* - the card index. If this argument is given, the device name
is constructed as 'hw:*cardindex*' and
the `device` keyword argument is ignored.
``0`` is the first hardware sound card.
**Note:** This should not be used, as it bypasses most of ALSA's configuration. .. method:: PCM.setperiodsize(period)
The defaults mentioned above are values passed by :mod:alsaaudio Sets the actual period size in frames. Each write should consist of
to ALSA, not anything internal to ALSA. exactly this number of frames, and each read will return this
number of frames (unless the device is in :const:`PCM_NONBLOCK` mode, in
which case it may return nothing at all)
**Note:** For default and non-default values alike, there is no
guarantee that a PCM device supports the requested configuration,
and ALSA may pick realizable values which it believes to be closest
to the request. Therefore, after creating a PCM object, it is
necessary to verify whether its realized configuration is acceptable.
The :func:info method can be used to query it.
*Changed in 0.10:* .. method:: PCM.read()
- Added the optional named parameter `periods`.
*Changed in 0.9:*
- Added the optional named parameters `rate`, `channels`, `format` and `periodsize`.
*Changed in 0.8:*
- The `card` keyword argument is still supported,
but deprecated. Please use `device` instead.
- The keyword argument `cardindex` was added.
The `card` keyword is deprecated because it guesses the real ALSA
name of the card. This was always fragile and broke some legitimate usecases.
PCM objects have the following methods:
.. method:: PCM.info() -> dict
Returns a dictionary containing the configuration of a PCM device.
A small subset of properties reflects fixed parameters given by the
user, stored within alsaaudio. To distinguish them from properties
retrieved from ALSA when the call is made, they have their name
prefixed with **" (call value) "**.
Descriptions of properties which can be directly set during PCM object
instantiation carry the prefix "PCM():", followed by the respective
constructor parameter. Note that due to device limitations, the values
may deviate from those originally requested.
Yet another set of properties cannot be set, and derives directly from
the hardware, possibly depending on other properties. Those properties'
descriptions are prefixed with "hw:" below.
=========================== ==================================== ==================================================================
Key Description (Reference) Type
=========================== ==================================== ==================================================================
name PCM():device string
card_no *index of card* integer (negative indicates device not associable with a card)
device_no *index of PCM device* integer
subdevice_no *index of PCM subdevice* integer
state *name of PCM state* string
access_type *name of PCM access type* string
(call value) type PCM():type integer
(call value) type_name PCM():type string
(call value) mode PCM():mode integer
(call value) mode_name PCM():mode string
format PCM():format integer
format_name PCM():format string
format_description PCM():format string
subformat_name *name of PCM subformat* string
subformat_description *description of subformat* string
channels PCM():channels integer
rate PCM():rate integer (Hz)
period_time *period duration* integer (:math:`\mu s`)
period_size PCM():period_size integer (frames)
buffer_time *buffer time* integer (:math:`\mu s`) (negative indicates error)
buffer_size *buffer size* integer (frames) (negative indicates error)
get_periods *approx. periods in buffer* integer (negative indicates error)
rate_numden *numerator, denominator* tuple (integer (Hz), integer (Hz))
significant_bits *significant bits in sample* [#tss]_ integer (negative indicates error)
nominal_bits *nominal bits in sample* [#tss]_ integer (negative indicates error)
physical_bits *sample width in bits* [#tss]_ integer (negative indicates error)
is_batch *hw: double buffering* boolean (True: hardware supported)
is_block_transfer *hw: block transfer* boolean (True: hardware supported)
is_double *hw: double buffering* boolean (True: hardware supported)
is_half_duplex *hw: half-duplex* boolean (True: hardware supported)
is_joint_duplex *hw: joint-duplex* boolean (True: hardware supported)
can_overrange *hw: overrange detection* boolean (True: hardware supported)
can_mmap_sample_resolution *hw: sample-resol. mmap* boolean (True: hardware supported)
can_pause *hw: pause* boolean (True: hardware supported)
can_resume *hw: resume* boolean (True: hardware supported)
can_sync_start *hw: synchronized start* boolean (True: hardware supported)
=========================== ==================================== ==================================================================
.. [#tss] More information in the :ref:`terminology section for sample size <term-sample-size>`
..
The italicized descriptions give a summary of the "full" description
as can be found in the
`ALSA documentation <https://www.alsa-project.org/alsa-doc>`_.
*New in 0.9.1*
.. method:: PCM.dumpinfo()
Dumps the PCM object's configured parameters to stdout.
.. method:: PCM.pcmtype() -> int
Returns the type of PCM object. Either :const:`PCM_CAPTURE` or
:const:`PCM_PLAYBACK`.
.. method:: PCM.pcmmode() -> int
Return the mode of the PCM object. One of :const:`PCM_NONBLOCK`,
:const:`PCM_ASYNC`, or :const:`PCM_NORMAL`
.. method:: PCM.cardname() -> string
Return the name of the sound card used by this PCM object.
..
Omitted by intention due to not really fitting the c'tor-based setup concept:
.. method:: PCM.getchannels()
Returns list of the device's supported channel counts.
.. method:: PCM.getratebounds()
Returns the card's minimum and maximum supported sample rates as
a tuple of integers.
.. method:: PCM.getrates()
Returns the sample rates supported by the device.
The returned value can be of one of the following, depending on
the card's properties:
* Card supports only a single rate: returns the rate
* Card supports a continuous range of rates: returns a tuple of
the range's lower and upper bounds (inclusive)
* Card supports a collection of well-known rates: returns a list of
the supported rates
.. method:: PCM.getformats()
Returns a dictionary of supported format codes (integers) keyed by
their standard ALSA names (strings).
.. method:: PCM.setchannels(nchannels: int) -> int
.. deprecated:: 0.9 Use the `channels` named argument to :func:`PCM`.
.. method:: PCM.setrate(rate: int) -> int
.. deprecated:: 0.9 Use the `rate` named argument to :func:`PCM`.
.. method:: PCM.setformat(format: int) -> int
.. deprecated:: 0.9 Use the `format` named argument to :func:`PCM`.
.. method:: PCM.setperiodsize(period: int) -> int
.. deprecated:: 0.9 Use the `periodsize` named argument to :func:`PCM`.
.. method:: PCM.state() -> int
Returs the current state of the stream, which can be one of
:const:`PCM_STATE_OPEN` (this should not actually happen),
:const:`PCM_STATE_SETUP` (after :func:`drop` or :func:`drain`),
:const:`PCM_STATE_PREPARED` (after construction),
:const:`PCM_STATE_RUNNING`,
:const:`PCM_STATE_XRUN`,
:const:`PCM_STATE_DRAINING`,
:const:`PCM_STATE_PAUSED`,
:const:`PCM_STATE_SUSPENDED`, and
:const:`PCM_STATE_DISCONNECTED`.
*New in 0.10*
.. method:: PCM.avail() -> int
For :const:`PCM_PLAYBACK` PCM objects, returns the number of writable
(that is, free) frames in the buffer.
For :const:`PCM_CAPTURE` PCM objects, returns the number of readable
(that is, filled) frames in the buffer.
An attempt to read/write more frames than indicated will block (in
:const:`PCM_NORMAL` mode) or fail and return zero (in
:const:`PCM_NONBLOCK` mode).
*New in 0.11*
.. method:: PCM.read() -> tuple[int, bytes]
In :const:`PCM_NORMAL` mode, this function blocks until a full period is In :const:`PCM_NORMAL` mode, this function blocks until a full period is
available, and then returns a tuple (length,data) where *length* is available, and then returns a tuple (length,data) where *length* is
@@ -360,135 +237,42 @@ PCM objects have the following methods:
``(0,'')`` if no new period has become available since the last ``(0,'')`` if no new period has become available since the last
call to read. call to read.
In case of a buffer overrun, this function will return the negative In case of an overrun, this function will return a negative size: :const:`-EPIPE`.
size :const:`-EPIPE`, and no data is read. This indicates that data was lost, even if the operation itself succeeded.
This indicates that data was lost. To resume capturing, just call read Try using a larger periodsize.
again, but note that the stream was already corrupted.
To avoid the problem in the future, try using a larger period size
and/or more periods, at the cost of higher latency.
.. method:: PCM.write(data: bytes) -> int .. method:: PCM.write(data)
Writes (plays) the sound in data. The length of data *must* be a Writes (plays) the sound in data. The length of data *must* be a
multiple of the frame size, and *should* be exactly the size of a multiple of the frame size, and *should* be exactly the size of a
period. If less than 'period size' frames are provided, the actual period. If less than 'period size' frames are provided, the actual
playout will not happen until more data is written. playout will not happen until more data is written.
If the data was successfully written, the call returns the size of the If the device is not in :const:`PCM_NONBLOCK` mode, this call will block if
data *in frames*. the kernel buffer is full, and until enough sound has been played
to allow the sound data to be buffered. The call always returns the
If the device is not in :const:`PCM_NONBLOCK` mode, this call will block size of the data provided.
if the kernel buffer is full, and until enough sound has been played
to allow the sound data to be buffered.
In :const:`PCM_NONBLOCK` mode, the call will return immediately, with a In :const:`PCM_NONBLOCK` mode, the call will return immediately, with a
return value of zero, if the buffer is full. In this case, the data return value of zero, if the buffer is full. In this case, the data
should be written again at a later time. should be written at a later time.
In case of a buffer underrun, this function will return the negative
size :const:`-EPIPE`, and no data is written.
At this point, the playback was already corrupted. If you want to play
the data nonetheless, call write again with the same data.
To avoid the problem in the future, try using a larger period size
and/or more periods, at the cost of higher latency.
Note that this call completing means only that the samples were buffered .. method:: PCM.pause([enable=True])
in the kernel, and playout will continue afterwards. Make sure that the
stream is drained before discarding the PCM handle.
.. method:: PCM.pause([enable: int = True]) -> int
If *enable* is :const:`True`, playback or capture is paused. If *enable* is :const:`True`, playback or capture is paused.
Otherwise, playback/capture is resumed. Otherwise, playback/capture is resumed.
.. method:: PCM.drop() -> int
Stop the stream and drop residual buffered frames. .. method:: PCM.polldescriptors()
*New in 0.9* Returns a tuple of *(file descriptor, eventmask)* that can be used to
wait for changes on the mixer with *select.poll*.
.. method:: PCM.drain() -> int
For :const:`PCM_PLAYBACK` PCM objects, play residual buffered frames
and then stop the stream. In :const:`PCM_NORMAL` mode,
this function blocks until all pending playback is drained.
For :const:`PCM_CAPTURE` PCM objects, this function is not very useful.
*New in 0.10*
.. method:: PCM.close() -> None
Closes the PCM device.
For :const:`PCM_PLAYBACK` PCM objects in :const:`PCM_NORMAL` mode,
this function blocks until all pending playback is drained.
.. method:: PCM.polldescriptors() -> list[tuple[int, int]]
Returns a list of tuples of *(file descriptor, eventmask)* that can be
used to wait for changes on the PCM with *select.poll*.
The *eventmask* value is compatible with `poll.register`__ in the Python The *eventmask* value is compatible with `poll.register`__ in the Python
:const:`select` module. :const:`select` module.
.. method:: PCM.polldescriptors_revents(descriptors: list[tuple[int, int]]) -> int __ poll_objects_
Processes the descriptor list returned by :func:`polldescriptors` after
using it with *select.poll*, and returns a single *eventmask* value that
is meaningful for deciding whether :func:`read` or :func:`write` should
be called.
*New in 0.11*
.. method:: PCM.set_tstamp_mode([mode: int = PCM_TSTAMP_ENABLE])
Set the ALSA timestamp mode on the device. The mode argument can be set to
either :const:`PCM_TSTAMP_NONE` or :const:`PCM_TSTAMP_ENABLE`.
.. method:: PCM.get_tstamp_mode() -> int
Return the integer value corresponding to the ALSA timestamp mode. The
return value can be either :const:`PCM_TSTAMP_NONE` or :const:`PCM_TSTAMP_ENABLE`.
.. method:: PCM.set_tstamp_type([type: int = PCM_TSTAMP_TYPE_GETTIMEOFDAY]) -> None
Set the ALSA timestamp mode on the device. The type argument
can be set to either :const:`PCM_TSTAMP_TYPE_GETTIMEOFDAY`,
:const:`PCM_TSTAMP_TYPE_MONOTONIC` or :const:`PCM_TSTAMP_TYPE_MONOTONIC_RAW`.
.. method:: PCM.get_tstamp_type() -> int
Return the integer value corresponding to the ALSA timestamp type. The
return value can be either :const:`PCM_TSTAMP_TYPE_GETTIMEOFDAY`,
:const:`PCM_TSTAMP_TYPE_MONOTONIC` or :const:`PCM_TSTAMP_TYPE_MONOTONIC_RAW`.
.. method:: PCM.htimestamp() -> tuple[int, int, int]
Return a Python tuple *(seconds, nanoseconds, frames_available_in_buffer)*.
The type of output is controlled by the tstamp_type, as described in the table below.
================================= ===========================================
Timestamp Type Description
================================= ===========================================
``PCM_TSTAMP_TYPE_GETTIMEOFDAY`` System-wide realtime clock with seconds
since epoch.
``PCM_TSTAMP_TYPE_MONOTONIC`` Monotonic time from an unspecified starting
time. Progress is NTP synchronized.
``PCM_TSTAMP_TYPE_MONOTONIC_RAW`` Monotonic time from an unspecified starting
time using only the system clock.
================================= ===========================================
The timestamp mode is controlled by the tstamp_mode, as described in the table below.
================================= ===========================================
Timestamp Mode Description
================================= ===========================================
``PCM_TSTAMP_NONE`` No timestamp.
``PCM_TSTAMP_ENABLE`` Update timestamp at every hardware position
update.
================================= ===========================================
**A few hints on using PCM devices for playback** **A few hints on using PCM devices for playback**
@@ -496,7 +280,7 @@ The most common reason for problems with playback of PCM audio is that writes
to PCM devices must *exactly* match the data rate of the device. to PCM devices must *exactly* match the data rate of the device.
If too little data is written to the device, it will underrun, and If too little data is written to the device, it will underrun, and
ugly clicking sounds will occur. Conversely, if too much data is ugly clicking sounds will occur. Conversely, of too much data is
written to the device, the write function will either block written to the device, the write function will either block
(:const:`PCM_NORMAL` mode) or return zero (:const:`PCM_NONBLOCK` mode). (:const:`PCM_NORMAL` mode) or return zero (:const:`PCM_NONBLOCK` mode).
@@ -525,7 +309,8 @@ Mixer Objects
Mixer objects provides access to the ALSA mixer API. Mixer objects provides access to the ALSA mixer API.
.. class:: Mixer(control: str = 'Master', id: int = 0, cardindex: int = -1, device: str = 'default') -> Mixer
.. class:: Mixer(control='Master', id=0, cardindex=-1, device='default')
Arguments are: Arguments are:
@@ -552,20 +337,23 @@ Mixer objects provides access to the ALSA mixer API.
Mixer objects have the following methods: Mixer objects have the following methods:
.. method:: Mixer.cardname() -> str .. method:: Mixer.cardname()
Return the name of the sound card used by this Mixer object Return the name of the sound card used by this Mixer object
.. method:: Mixer.mixer() -> str
.. method:: Mixer.mixer()
Return the name of the specific mixer controlled by this object, For example Return the name of the specific mixer controlled by this object, For example
``'Master'`` or ``'PCM'`` ``'Master'`` or ``'PCM'``
.. method:: Mixer.mixerid() -> int
.. method:: Mixer.mixerid()
Return the ID of the ALSA mixer controlled by this object. Return the ID of the ALSA mixer controlled by this object.
.. method:: Mixer.switchcap() -> int
.. method:: Mixer.switchcap()
Returns a list of the switches which are defined by this specific mixer. Returns a list of the switches which are defined by this specific mixer.
Possible values in this list are: Possible values in this list are:
@@ -585,7 +373,8 @@ Mixer objects have the following methods:
To manipulate these switches use the :meth:`setrec` or To manipulate these switches use the :meth:`setrec` or
:meth:`setmute` methods :meth:`setmute` methods
.. method:: Mixer.volumecap() -> int
.. method:: Mixer.volumecap()
Returns a list of the volume control capabilities of this Returns a list of the volume control capabilities of this
mixer. Possible values in the list are: mixer. Possible values in the list are:
@@ -601,7 +390,7 @@ Mixer objects have the following methods:
'Joined Capture Volume' Manipulate sound capture volume for all channels at a time 'Joined Capture Volume' Manipulate sound capture volume for all channels at a time
======================== ================ ======================== ================
.. method:: Mixer.getenum() -> tuple[str, list[str]] .. method:: Mixer.getenum()
For enumerated controls, return the currently selected item and the list of For enumerated controls, return the currently selected item and the list of
items available. items available.
@@ -627,64 +416,59 @@ Mixer objects have the following methods:
This method will return an empty tuple if the mixer is not an enumerated This method will return an empty tuple if the mixer is not an enumerated
control. control.
.. method:: Mixer.setenum(index: int) -> None
For enumerated controls, sets the currently selected item. .. method:: Mixer.getmute()
*index* is an index into the list of available enumerated items returned
by :func:`getenum`.
.. method:: Mixer.getrange(pcmtype: int = PCM_PLAYBACK, units: int = VOLUME_UNITS_RAW) -> tuple[int, int] Return a list indicating the current mute setting for each
channel. 0 means not muted, 1 means muted.
This method will fail if the mixer has no playback switch capabilities.
.. method:: Mixer.getrange([direction])
Return the volume range of the ALSA mixer controlled by this object. Return the volume range of the ALSA mixer controlled by this object.
The value is a tuple of integers whose meaning is determined by the
*units* argument.
The optional *pcmtype* argument can be either :const:`PCM_PLAYBACK` or The optional *direction* argument can be either :const:`PCM_PLAYBACK` or
:const:`PCM_CAPTURE`, which is relevant if the mixer can control both :const:`PCM_CAPTURE`, which is relevant if the mixer can control both
playback and capture volume. The default value is :const:`PCM_PLAYBACK` playback and capture volume. The default value is :const:`PCM_PLAYBACK`
if the mixer has playback channels, otherwise it is :const:`PCM_CAPTURE`. if the mixer has playback channels, otherwise it is :const:`PCM_CAPTURE`.
The optional *units* argument can be one of :const:`VOLUME_UNITS_PERCENTAGE`,
:const:`VOLUME_UNITS_RAW`, or :const:`VOLUME_UNITS_DB`.
.. method:: Mixer.getvolume(pcmtype: int = PCM_PLAYBACK, units: int = VOLUME_UNITS_PERCENTAGE) -> int .. method:: Mixer.getrec()
Return a list indicating the current record mute setting for each channel. 0
means not recording, 1 means recording.
This method will fail if the mixer has no capture switch capabilities.
.. method:: Mixer.getvolume([direction])
Returns a list with the current volume settings for each channel. The list Returns a list with the current volume settings for each channel. The list
elements are integers whose meaning is determined by the *units* argument. elements are integer percentages.
The optional *pcmtype* argument can be either :const:`PCM_PLAYBACK` or The optional *direction* argument can be either :const:`PCM_PLAYBACK` or
:const:`PCM_CAPTURE`, which is relevant if the mixer can control both :const:`PCM_CAPTURE`, which is relevant if the mixer can control both
playback and capture volume. The default value is :const:`PCM_PLAYBACK` playback and capture volume. The default value is :const:`PCM_PLAYBACK`
if the mixer has playback channels, otherwise it is :const:`PCM_CAPTURE`. if the mixer has playback channels, otherwise it is :const:`PCM_CAPTURE`.
The optional *units* argument can be one of :const:`VOLUME_UNITS_PERCENTAGE`,
:const:`VOLUME_UNITS_RAW`, or :const:`VOLUME_UNITS_DB`.
.. method:: Mixer.setvolume(volume: int, pcmtype: int = PCM_PLAYBACK, units: int = VOLUME_UNITS_PERCENTAGE, channel: (int | None) = None) -> None .. method:: Mixer.setvolume(volume, [channel], [direction])
Change the current volume settings for this mixer. The *volume* argument Change the current volume settings for this mixer. The *volume* argument
is an integer whose meaning is determined by the *units* argument. controls the new volume setting as an integer percentage.
If the optional argument *channel* is present, the volume is set If the optional argument *channel* is present, the volume is set
only for this channel. This assumes that the mixer can control the only for this channel. This assumes that the mixer can control the
volume for the channels independently. volume for the channels independently.
The optional *pcmtype* argument can be either :const:`PCM_PLAYBACK` or The optional *direction* argument can be either :const:`PCM_PLAYBACK` or
:const:`PCM_CAPTURE`, which is relevant if the mixer can control both :const:`PCM_CAPTURE`, which is relevant if the mixer can control both
playback and capture volume. The default value is :const:`PCM_PLAYBACK` playback and capture volume. The default value is :const:`PCM_PLAYBACK`
if the mixer has playback channels, otherwise it is :const:`PCM_CAPTURE`. if the mixer has playback channels, otherwise it is :const:`PCM_CAPTURE`.
The optional *units* argument can be one of :const:`VOLUME_UNITS_PERCENTAGE`, .. method:: Mixer.setmute(mute, [channel])
:const:`VOLUME_UNITS_RAW`, or :const:`VOLUME_UNITS_DB`.
.. method:: Mixer.getmute() -> list[int]
Return a list indicating the current mute setting for each channel.
0 means not muted, 1 means muted.
This method will fail if the mixer has no playback switch capabilities.
.. method:: Mixer.setmute(mute: bool, channel: (int | None) = None) -> None
Sets the mute flag to a new value. The *mute* argument is either 0 for not Sets the mute flag to a new value. The *mute* argument is either 0 for not
muted, or 1 for muted. muted, or 1 for muted.
@@ -694,14 +478,8 @@ Mixer objects have the following methods:
This method will fail if the mixer has no playback mute capabilities This method will fail if the mixer has no playback mute capabilities
.. method:: Mixer.getrec() -> list[int]
Return a list indicating the current record mute setting for each channel. .. method:: Mixer.setrec(capture, [channel])
0 means not recording, 1 means recording.
This method will fail if the mixer has no capture switch capabilities.
.. method:: Mixer.setrec(capture: int, channel: (int | None) = None) -> None
Sets the capture mute flag to a new value. The *capture* argument Sets the capture mute flag to a new value. The *capture* argument
is either 0 for no capture, or 1 for capture. is either 0 for no capture, or 1 for capture.
@@ -711,24 +489,22 @@ Mixer objects have the following methods:
This method will fail if the mixer has no capture switch capabilities. This method will fail if the mixer has no capture switch capabilities.
.. method:: Mixer.polldescriptors() -> list[tuple[int, int]] .. method:: Mixer.polldescriptors()
Returns a list of tuples of *(file descriptor, eventmask)* that can be Returns a tuple of *(file descriptor, eventmask)* that can be used to
used to wait for changes on the mixer with *select.poll*. wait for changes on the mixer with *select.poll*.
The *eventmask* value is compatible with `poll.register`__ in the Python The *eventmask* value is compatible with `poll.register`__ in the Python
:const:`select` module. :const:`select` module.
.. method:: Mixer.handleevents() -> int __ poll_objects_
Acknowledge events on the :func:`polldescriptors` file descriptors .. method:: Mixer.handleevents()
Acknowledge events on the *polldescriptors* file descriptors
to prevent subsequent polls from returning the same events again. to prevent subsequent polls from returning the same events again.
Returns the number of events that were acknowledged. Returns the number of events that were acknowledged.
.. method:: Mixer.close() -> None
Closes the Mixer device.
**A rant on the ALSA Mixer API** **A rant on the ALSA Mixer API**
The ALSA mixer API is extremely complicated - and hardly documented at all. The ALSA mixer API is extremely complicated - and hardly documented at all.
@@ -751,6 +527,8 @@ Unfortunately, I'm not able to create such a HOWTO myself, since I only
understand half of the API, and that which I do understand has come from a understand half of the API, and that which I do understand has come from a
painful trial and error process. painful trial and error process.
.. % ==== 4. ====
.. _pcm-example: .. _pcm-example:
@@ -792,7 +570,6 @@ To test PCM playback (on your default soundcard), run::
recordtest.py and playbacktest.py recordtest.py and playbacktest.py
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**recordtest.py** and **playbacktest.py** will record and play a raw **recordtest.py** and **playbacktest.py** will record and play a raw
sound file in CD quality. sound file in CD quality.
@@ -807,9 +584,6 @@ Play back the recording with::
$ python playbacktest.py <filename> $ python playbacktest.py <filename>
playbacktest.py accepts the commandline option
*-d <device>*.
mixertest.py mixertest.py
~~~~~~~~~~~~ ~~~~~~~~~~~~
@@ -865,3 +639,9 @@ argument::
Capabilities: Playback Volume Playback Mute Capabilities: Playback Volume Playback Mute
Channel 0 volume: 61% Channel 0 volume: 61%
Channel 1 volume: 61% Channel 1 volume: 61%
.. rubric:: Footnotes
.. [#f1] ALSA also allows ``PCM_ASYNC``, but this is not supported yet.
.. _poll_objects: http://docs.python.org/library/select.html#poll-objects
+17 -4
View File
@@ -7,12 +7,26 @@ Introduction
.. |release| replace:: version .. |release| replace:: version
.. % At minimum, give your name and an email address. You can include a
.. % snail-mail address if you like.
.. % This makes the Abstract go on a separate page in the HTML version;
.. % if a copyright notice is used, it should go immediately after this.
.. %
.. _front: .. _front:
This software is licensed under the PSF license - the same one used by the This software is licensed under the PSF license - the same one used by the
majority of the python distribution. Basically you can use it for anything you majority of the python distribution. Basically you can use it for anything you
wish (even commercial purposes). There is no warranty whatsoever. wish (even commercial purposes). There is no warranty whatsoever.
.. % Copyright statement should go here, if needed.
.. % The abstract should be a paragraph or two long, and describe the
.. % scope of the document.
.. topic:: Abstract .. topic:: Abstract
This package contains wrappers for accessing the ALSA API from Python. It is This package contains wrappers for accessing the ALSA API from Python. It is
@@ -50,8 +64,8 @@ More information about ALSA may be found on the project homepage
ALSA and Python ALSA and Python
=============== ===============
The older Linux sound API (OSS) -- which is now deprecated -- is well supported The older Linux sound API (OSS) which is now deprecated is well supported from
by the standard Python library, through the ossaudiodev module. No native ALSA the standard Python library, through the ossaudiodev module. No native ALSA
support exists in the standard library. support exists in the standard library.
There are a few other "ALSA for Python" projects available, including at least There are a few other "ALSA for Python" projects available, including at least
@@ -77,7 +91,7 @@ Note: the wrappers link with the alsasound library (from the alsa-lib package)
and need the ALSA headers for compilation. Verify that you have and need the ALSA headers for compilation. Verify that you have
/usr/lib/libasound.so and /usr/include/alsa (or similar paths) before building. /usr/lib/libasound.so and /usr/include/alsa (or similar paths) before building.
*On Debian/Ubuntu, install libasound2-dev.* *On Debian (and probably Ubuntu), install libasound2-dev.*
Naturally you also need to use a kernel with proper ALSA support. This is the Naturally you also need to use a kernel with proper ALSA support. This is the
default in Linux kernel 2.6 and later. If you are using kernel version 2.4 you default in Linux kernel 2.6 and later. If you are using kernel version 2.4 you
@@ -92,7 +106,6 @@ And then as root: --- ::
# python setup.py install # python setup.py install
******* *******
Testing Testing
******* *******
+18 -49
View File
@@ -19,7 +19,7 @@ Sample
Musically, the sample size determines the dynamic range. The Musically, the sample size determines the dynamic range. The
dynamic range is the difference between the quietest and the dynamic range is the difference between the quietest and the
loudest signal that can be reproduced. loudest signal that can be resproduced.
Frame Frame
A frame consists of exactly one sample per channel. If there is only one A frame consists of exactly one sample per channel. If there is only one
@@ -29,8 +29,8 @@ Frame
Frame size Frame size
This is the size in bytes of each frame. This can vary a lot: if each sample This is the size in bytes of each frame. This can vary a lot: if each sample
is 8 bits, and we're handling mono sound, the frame size is one byte. is 8 bits, and we're handling mono sound, the frame size is one byte.
For six channel audio with 64 bit floating point samples, the frame size Similarly in 6 channel audio with 64 bit floating point samples, the frame
is 48 bytes. size is 48 bytes
Rate Rate
PCM sound consists of a flow of sound frames. The sound rate controls how PCM sound consists of a flow of sound frames. The sound rate controls how
@@ -38,7 +38,7 @@ Rate
means that a new frame is played or captured 8000 times per second. means that a new frame is played or captured 8000 times per second.
Data rate Data rate
This is the number of bytes which must be consumed or provided per This is the number of bytes, which must be recorded or provided per
second at a certain frame size and rate. second at a certain frame size and rate.
8000 Hz mono sound with 8 bit (1 byte) samples has a data rate of 8000 Hz mono sound with 8 bit (1 byte) samples has a data rate of
@@ -46,60 +46,29 @@ Data rate
At the other end of the scale, 96000 Hz, 6 channel sound with 64 At the other end of the scale, 96000 Hz, 6 channel sound with 64
bit (8 bytes) samples has a data rate of 96000 \* 6 \* 8 = 4608 bit (8 bytes) samples has a data rate of 96000 \* 6 \* 8 = 4608
kb/s (almost 5 MB sound data per second). kb/s (almost 5 MB sound data per second)
If the data rate requirement is not met, an overrun (on capture) or
underrun (on playback) occurs; the term "xrun" is used to refer to
either event.
.. _term-period:
Period Period
The CPU processes sample data in chunks of frames, so-called periods When the hardware processes data this is done in chunks of frames. The time
(also called fragments by some systems). The operating system kernel's interval between each processing (A/D or D/A conversion) is known
sample buffer must hold at least two periods (at any given time, one as the period.
is processed by the sound hardware, and one by the CPU). The size of the period has direct implication on the latency of the
sound input or output. For low-latency the period size should be
The completion of a *period* triggers a CPU interrupt, which causes very small, while low CPU resource usage would usually demand
processing and context switching overhead. Therefore, a smaller period larger period sizes. With ALSA, the CPU utilization is not impacted
size causes higher CPU resource usage at a given data rate. much by the period size, since the kernel layer buffers multiple
periods internally, so each period generates an interrupt and a
A bigger size of the *buffer* improves the system's resilience to xruns. memory copy, but userspace can be slower and read or write multiple
The buffer being split into a bigger number of smaller periods also does periods at the same time.
that, as it allows it to be drained / topped up sooner.
On the other hand, a bigger size of the *buffer* also increases the
playback latency, that is, the time it takes for a frame from being
sent out by the application to being actually audible.
Similarly, a bigger *period* size increases the capture latency.
The trade-off between latency, xrun resilience, and resource usage
must be made depending on the application.
Period size Period size
This is the size of each period in frames. *Not bytes, but frames!* This is the size of each period in Hz. *Not bytes, but Hz!.* In
In :mod:`alsaaudio` the period size is set directly, and it is :mod:`alsaaudio` the period size is set directly, and it is
therefore important to understand the significance of this therefore important to understand the significance of this
number. If the period size is configured to for example 32, number. If the period size is configured to for example 32,
each write should contain exactly 32 frames of sound data, and each each write should contain exactly 32 frames of sound data, and each
read will return either 32 frames of data or nothing at all. read will return either 32 frames of data or nothing at all.
.. _term-sample-size:
Sample size
Each sample takes *physical_bits* of space. *nominal_bits* tells
how many least significant bits are used. This is the bit depth
in the format name and sometimes called just *sample bits* or
*format width*. *significant_bits* tells how many most significant
bits of the *nominal_bits* are used by the sample. This can be thought
of as the *sample resolution*. This is visualized as follows::
MSB |00000000 XXXXXXXX XXXXXXXX 00000000| LSB
|--significant--|
|---------nominal---------|
|-------------physical--------------|
Once you understand these concepts, you will be ready to use the PCM API. Read Once you understand these concepts, you will be ready to use the PCM API. Read
on. on.
-403
View File
@@ -1,403 +0,0 @@
#!/usr/bin/env python3
# -*- mode: python; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*-
import sys
import select
import logging
import re
import struct
import subprocess
from datetime import datetime, timedelta
from alsaaudio import (PCM, pcms, PCM_PLAYBACK, PCM_CAPTURE, PCM_NONBLOCK, Mixer,
PCM_STATE_OPEN, PCM_STATE_SETUP, PCM_STATE_PREPARED, PCM_STATE_RUNNING, PCM_STATE_XRUN, PCM_STATE_DRAINING,
PCM_STATE_PAUSED, PCM_STATE_SUSPENDED, ALSAAudioError)
from argparse import ArgumentParser
poll_names = {
select.POLLIN: 'POLLIN',
select.POLLPRI: 'POLLPRI',
select.POLLOUT: 'POLLOUT',
select.POLLERR: 'POLLERR',
select.POLLHUP: 'POLLHUP',
select.POLLRDHUP: 'POLLRDHUP',
select.POLLNVAL: 'POLLNVAL'
}
state_names = {
PCM_STATE_OPEN: 'PCM_STATE_OPEN',
PCM_STATE_SETUP: 'PCM_STATE_SETUP',
PCM_STATE_PREPARED: 'PCM_STATE_PREPARED',
PCM_STATE_RUNNING: 'PCM_STATE_RUNNING',
PCM_STATE_XRUN: 'PCM_STATE_XRUN',
PCM_STATE_DRAINING: 'PCM_STATE_DRAINING',
PCM_STATE_PAUSED: 'PCM_STATE_PAUSED',
PCM_STATE_SUSPENDED: 'PCM_STATE_SUSPENDED'
}
def poll_desc(mask):
return '|'.join([poll_names[bit] for bit, name in poll_names.items() if mask & bit])
class PollDescriptor(object):
'''File Descriptor, event mask and a name for logging'''
def __init__(self, name, fd, mask):
self.name = name
self.fd = fd
self.mask = mask
def as_tuple(self):
return (self.fd, self.mask)
@classmethod
def from_alsa_object(cls, name, alsaobject, mask=None):
# TODO maybe refactor: we ignore objects that have more then one polldescriptor
fd, alsamask = alsaobject.polldescriptors()[0]
if mask is None:
mask = alsamask
return cls(name, fd, mask)
class Loopback(object):
'''Loopback state and event handling'''
def __init__(self, capture, playback_args, volume_handler, run_after_stop=None, run_before_start=None):
self.playback_args = playback_args
self.playback = None
self.volume_handler = volume_handler
self.capture_started = None
self.last_capture_event = None
self.capture = capture
self.capture_pd = PollDescriptor.from_alsa_object('capture', capture)
self.run_after_stop = None
if run_after_stop:
self.run_after_stop = run_after_stop.split(' ')
self.run_before_start = None
if run_before_start:
self.run_before_start = run_before_start.split(' ')
self.run_after_stop_did_run = False
self.waitBeforeOpen = False
self.queue = []
self.period_size = 0
self.silent_periods = 0
@staticmethod
def compute_energy(data):
values = struct.unpack(f'{len(data)//2}h', data)
e = 0
for v in values:
e = e + v * v
return e
@staticmethod
def run_command(cmd):
if cmd:
rc = subprocess.run(cmd)
if rc.returncode:
logging.warning(f'run {cmd}, return code {rc.returncode}')
else:
logging.info(f'run {cmd}, return code {rc.returncode}')
def register(self, reactor):
reactor.register_timeout_handler(self.timeout_handler)
reactor.register(self.capture_pd, self)
def start(self):
# start reading data
size, data = self.capture.read()
if size:
self.queue.append(data)
def timeout_handler(self):
if self.playback and self.capture_started:
if self.last_capture_event:
if datetime.now() - self.last_capture_event > timedelta(seconds=2):
logging.info('timeout - closing playback device')
self.playback.close()
self.playback = None
self.capture_started = None
if self.volume_handler:
self.volume_handler.stop()
self.run_command(self.run_after_stop)
return
self.waitBeforeOpen = False
if not self.run_after_stop_did_run and not self.playback:
if self.volume_handler:
self.volume_handler.stop()
self.run_command(self.run_after_stop)
self.run_after_stop_did_run = True
def pop(self):
if len(self.queue):
return self.queue.pop()
else:
return None
def handle_capture_event(self, eventmask, name):
'''called when data is available for reading'''
self.last_capture_event = datetime.now()
size, data = self.capture.read()
if not size:
logging.warning(f'capture event but no data')
return False
energy = self.compute_energy(data)
logging.debug(f'energy: {energy}')
# the usecase is a USB capture device where we get perfect silence when it's idle
if energy == 0:
self.silent_periods = self.silent_periods + 1
# turn off playback after two seconds of silence
# 2 channels * 2 seconds * 2 bytes per sample
fps = self.playback_args['rate'] * 8 // (self.playback_args['periodsize'] * self.playback_args['periods'])
logging.debug(f'{self.silent_periods} of {fps} silent periods: {self.playback}')
if self.silent_periods > fps and self.playback:
logging.info(f'closing playback due to silence')
self.playback.close()
self.playback = None
if self.volume_handler:
self.volume_handler.stop()
self.run_command(self.run_after_stop)
self.run_after_stop_did_run = True
if not self.playback:
return
else:
self.silent_periods = 0
if not self.playback:
if self.waitBeforeOpen:
return False
try:
if self.volume_handler:
self.volume_handler.start()
self.run_command(self.run_before_start)
self.playback = PCM(**self.playback_args)
self.period_size = self.playback.info()['period_size']
logging.info(f'opened playback device with period_size {self.period_size}')
except ALSAAudioError as e:
logging.info('opening PCM playback device failed: %s', e)
self.waitBeforeOpen = True
return False
self.capture_started = datetime.now()
logging.info(f'{self.playback} capture started: {self.capture_started}')
self.queue.append(data)
if len(self.queue) <= 2:
logging.info(f'buffering: {len(self.queue)}')
return False
try:
data = self.pop()
if data:
space = self.playback.avail()
written = self.playback.write(data)
logging.debug(f'wrote {written} bytes while space was {space}')
except ALSAAudioError:
logging.error('underrun', exc_info=1)
return True
def __call__(self, fd, eventmask, name):
if fd == self.capture_pd.fd:
real_mask = self.capture.polldescriptors_revents([self.capture_pd.as_tuple()])
if real_mask:
return self.handle_capture_event(real_mask, name)
else:
logging.debug('null capture event')
return False
else:
real_mask = self.playback.polldescriptors_revents([self.playback_pd.as_tuple()])
if real_mask:
return self.handle_playback_event(real_mask, name)
else:
logging.debug('null playback event')
return False
class VolumeForwarder(object):
'''Volume control event handling'''
def __init__(self, capture_control, playback_control):
self.playback_control = playback_control
self.capture_control = capture_control
self.active = True
self.volume = None
def start(self):
self.active = True
if self.volume:
self.volume = playback_control.setvolume(self.volume)
def stop(self):
self.active = False
self.volume = self.playback_control.getvolume(pcmtype=PCM_CAPTURE)[0]
def __call__(self, fd, eventmask, name):
if not self.active:
return
volume = self.capture_control.getvolume(pcmtype=PCM_CAPTURE)
# indicate that we've handled the event
self.capture_control.handleevents()
logging.info(f'{name} adjusting volume to {volume}')
if volume:
self.playback_control.setvolume(volume[0])
class Reactor(object):
'''A wrapper around select.poll'''
def __init__(self):
self.poll = select.poll()
self.descriptors = {}
self.timeout_handlers = set()
def register(self, polldescriptor, callable):
logging.debug(f'registered {polldescriptor.name}: {poll_desc(polldescriptor.mask)}')
self.descriptors[polldescriptor.fd] = (polldescriptor, callable)
self.poll.register(polldescriptor.fd, polldescriptor.mask)
def unregister(self, polldescriptor):
self.poll.unregister(polldescriptor.fd)
del self.descriptors[polldescriptor.fd]
def register_timeout_handler(self, callable):
self.timeout_handlers.add(callable)
def unregister_timeout_handler(self, callable):
self.timeout_handlers.remove(callable)
def run(self):
last_timeout_ev = datetime.now()
while True:
# poll for a bit, then send a timeout to registered handlers
events = self.poll.poll(0.25)
for fd, ev in events:
polldescriptor, handler = self.descriptors[fd]
# very chatty - log all events
# logging.debug(f'{polldescriptor.name}: {poll_desc(ev)} ({ev})')
handler(fd, ev, polldescriptor.name)
if datetime.now() - last_timeout_ev > timedelta(seconds=0.25):
for t in self.timeout_handlers:
t()
last_timeout_ev = datetime.now()
if __name__ == '__main__':
logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s', level=logging.INFO)
parser = ArgumentParser(description='ALSA loopback (with volume forwarding)')
playback_pcms = pcms(pcmtype=PCM_PLAYBACK)
capture_pcms = pcms(pcmtype=PCM_CAPTURE)
if not playback_pcms:
logging.error('no playback PCM found')
sys.exit(2)
if not capture_pcms:
logging.error('no capture PCM found')
sys.exit(2)
parser.add_argument('-d', '--debug', action='store_true')
parser.add_argument('-i', '--input', default=capture_pcms[0])
parser.add_argument('-o', '--output', default=playback_pcms[0])
parser.add_argument('-r', '--rate', type=int, default=44100)
parser.add_argument('-c', '--channels', type=int, default=2)
parser.add_argument('-p', '--periodsize', type=int, default=444) # must be divisible by 6 for 44k1
parser.add_argument('-P', '--periods', type=int, default=2)
parser.add_argument('-I', '--input-mixer', help='Control of the input mixer, can contain the card index, e.g. Digital:2')
parser.add_argument('-O', '--output-mixer', help='Control of the output mixer, can contain the card index, e.g. PCM:1')
parser.add_argument('-A', '--run-after-stop', help='command to run when the capture device is idle/silent')
parser.add_argument('-B', '--run-before-start', help='command to run when the capture device becomes active')
parser.add_argument('-V', '--volume', help='Initial volume (default is leave unchanged)')
args = parser.parse_args()
if args.debug:
logging.getLogger().setLevel(logging.DEBUG)
playback_args = {
'type': PCM_PLAYBACK,
'mode': PCM_NONBLOCK,
'device': args.output,
'rate': args.rate,
'channels': args.channels,
'periodsize': args.periodsize,
'periods': args.periods
}
reactor = Reactor()
# If args.input_mixer and args.output_mixer are set, forward the capture volume to the playback volume.
# The usecase is a capture device that is implemented using g_audio, i.e. the Linux USB gadget driver.
# When a USB device (eg. an iPad) is connected to this machine, its volume events will go to the volume control
# of the output device
capture = None
playback = None
volume_handler = None
if args.input_mixer and args.output_mixer:
re_mixer = re.compile(r'([a-zA-Z0-9]+):?([0-9+])?')
input_mixer_card = None
m = re_mixer.match(args.input_mixer)
if m:
input_mixer = m.group(1)
if m.group(2):
input_mixer_card = int(m.group(2))
else:
parser.print_usage()
sys.exit(1)
output_mixer_card = None
m = re_mixer.match(args.output_mixer)
if m:
output_mixer = m.group(1)
if m.group(2):
output_mixer_card = int(m.group(2))
else:
parser.print_usage()
sys.exit(1)
if input_mixer_card is None:
capture = PCM(type=PCM_CAPTURE, mode=PCM_NONBLOCK, device=args.input, rate=args.rate,
channels=args.channels, periodsize=args.periodsize, periods=args.periods)
input_mixer_card = capture.info()['card_no']
if output_mixer_card is None:
playback = PCM(**playback_args)
output_mixer_card = playback.info()['card_no']
playback.close()
playback_control = Mixer(control=output_mixer, cardindex=int(output_mixer_card))
capture_control = Mixer(control=input_mixer, cardindex=int(input_mixer_card))
volume_handler = VolumeForwarder(capture_control, playback_control)
reactor.register(PollDescriptor.from_alsa_object('capture_control', capture_control, select.POLLIN), volume_handler)
if args.volume and playback_control:
playback_control.setvolume(int(args.volume))
loopback = Loopback(capture, playback_args, volume_handler, args.run_after_stop, args.run_before_start)
loopback.register(reactor)
loopback.start()
reactor.run()
-64
View File
@@ -1,64 +0,0 @@
#!/usr/bin/env python3
# -*- mode: python; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*-
# Simple test script that plays (some) wav files
from __future__ import print_function
import sys
import wave
import getopt
import alsaaudio
def play(device, f):
format = None
# 8bit is unsigned in wav files
if f.getsampwidth() == 1:
format = alsaaudio.PCM_FORMAT_U8
# Otherwise we assume signed data, little endian
elif f.getsampwidth() == 2:
format = alsaaudio.PCM_FORMAT_S16_LE
elif f.getsampwidth() == 3:
format = alsaaudio.PCM_FORMAT_S24_3LE
elif f.getsampwidth() == 4:
format = alsaaudio.PCM_FORMAT_S32_LE
else:
raise ValueError('Unsupported format')
periodsize = f.getframerate() // 8
print('%d channels, %d sampling rate, format %d, periodsize %d\n' % (f.getnchannels(),
f.getframerate(),
format,
periodsize))
device = alsaaudio.PCM(channels=f.getnchannels(), rate=f.getframerate(), format=format, periodsize=periodsize, device=device)
data = f.readframes(periodsize)
while data:
# Read data from stdin
if device.write(data) < 0:
print("Playback buffer underrun! Continuing nonetheless ...")
data = f.readframes(periodsize)
def usage():
print('usage: playwav.py [-d <device>] <file>', file=sys.stderr)
sys.exit(2)
if __name__ == '__main__':
device = 'default'
opts, args = getopt.getopt(sys.argv[1:], 'd:')
for o, a in opts:
if o == '-d':
device = a
if not args:
usage()
with wave.open(args[0], 'rb') as f:
play(device, f)
-66
View File
@@ -1,66 +0,0 @@
#!/usr/bin/env python3
# -*- mode: python; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*-
## recordtest.py
##
## This is an example of a simple sound capture script.
##
## The script opens an ALSA pcm device for sound capture, sets
## various attributes of the capture, and reads in a loop,
## writing the data to standard out.
##
## To test it out do the following:
## python recordtest.py out.raw # talk to the microphone
## aplay -r 8000 -f S16_LE -c 1 out.raw
#!/usr/bin/env python
from __future__ import print_function
import sys
import time
import getopt
import alsaaudio
def usage():
print('usage: recordtest.py [-d <device>] <file>', file=sys.stderr)
sys.exit(2)
if __name__ == '__main__':
device = 'default'
opts, args = getopt.getopt(sys.argv[1:], 'd:')
for o, a in opts:
if o == '-d':
device = a
if not args:
usage()
f = open(args[0], 'wb')
# Open the device in nonblocking capture mode in mono, with a sampling rate of 44100 Hz
# and 16 bit little endian samples
# The period size controls the internal number of frames per period.
# The significance of this parameter is documented in the ALSA api.
# For our purposes, it is suficcient to know that reads from the device
# will return this many frames. Each frame being 2 bytes long.
# This means that the reads below will return either 320 bytes of data
# or 0 bytes of data. The latter is possible because we are in nonblocking
# mode.
inp = alsaaudio.PCM(alsaaudio.PCM_CAPTURE, alsaaudio.PCM_NONBLOCK,
channels=1, rate=44100, format=alsaaudio.PCM_FORMAT_S16_LE,
periodsize=160, device=device)
loops = 1000000
while loops > 0:
loops -= 1
# Read data from device
l, data = inp.read()
if l < 0:
print("Capture buffer overrun! Continuing nonetheless ...")
elif l:
f.write(data)
time.sleep(.001)
+20 -20
View File
@@ -7,7 +7,6 @@
from __future__ import print_function from __future__ import print_function
import sys import sys
import time
from threading import Thread from threading import Thread
from multiprocessing import Queue from multiprocessing import Queue
@@ -16,15 +15,14 @@ if sys.version_info[0] < 3:
else: else:
from queue import Empty from queue import Empty
from math import pi, sin, ceil from math import pi, sin
import struct import struct
import itertools
import alsaaudio import alsaaudio
sampling_rate = 48000 sampling_rate = 48000
format = alsaaudio.PCM_FORMAT_S16_LE format = alsaaudio.PCM_FORMAT_S16_LE
pack_format = 'h' # short int, matching S16 framesize = 2 # bytes per frame for the values above
channels = 2 channels = 2
def nearest_frequency(frequency): def nearest_frequency(frequency):
@@ -36,29 +34,32 @@ def generate(frequency, duration = 0.125):
# generate a buffer with a sine wave of `frequency` # generate a buffer with a sine wave of `frequency`
# that is approximately `duration` seconds long # that is approximately `duration` seconds long
# the buffersize we approximately want
target_size = int(sampling_rate * channels * duration)
# the length of a full sine wave at the frequency # the length of a full sine wave at the frequency
cycle_size = int(sampling_rate / frequency) cycle_size = int(sampling_rate / frequency)
# number of full cycles we can fit into the duration # number of full cycles we can fit into target_size
factor = int(ceil(duration * frequency)) factor = int(target_size / cycle_size)
# total number of frames size = max(int(cycle_size * factor), 1)
size = cycle_size * factor
sine = [ int(32767 * sin(2 * pi * frequency * i / sampling_rate)) \ sine = [ int(32767 * sin(2 * pi * frequency * i / sampling_rate)) \
for i in range(size)] for i in range(size)]
if channels > 1: return struct.pack('%dh' % size, *sine)
sine = list(itertools.chain.from_iterable(itertools.repeat(x, channels) for x in sine))
return struct.pack(str(size * channels) + pack_format, *sine)
class SinePlayer(Thread): class SinePlayer(Thread):
def __init__(self, frequency = 440.0): def __init__(self, frequency = 440.0):
Thread.__init__(self, daemon=True) Thread.__init__(self)
self.device = alsaaudio.PCM(channels=channels, format=format, rate=sampling_rate) self.setDaemon(True)
self.device = alsaaudio.PCM()
self.device.setchannels(channels)
self.device.setformat(format)
self.device.setrate(sampling_rate)
self.queue = Queue() self.queue = Queue()
self.change(frequency) self.change(frequency)
@@ -81,17 +82,16 @@ class SinePlayer(Thread):
while True: while True:
try: try:
buffer = self.queue.get(False) buffer = self.queue.get(False)
self.device.setperiodsize(int(len(buffer) / framesize))
self.device.write(buffer)
except Empty: except Empty:
pass
if buffer: if buffer:
if self.device.write(buffer) < 0: self.device.write(buffer)
print("Playback buffer underrun! Continuing nonetheless ...")
isine = SinePlayer() isine = SinePlayer()
isine.start() isine.start()
time.sleep(1) def change(f):
isine.change(1000) isine.change(f)
time.sleep(1)
+2 -32
View File
@@ -43,41 +43,11 @@ def show_mixer(name, kwargs):
sys.exit(1) sys.exit(1)
print("Mixer name: '%s'" % mixer.mixer()) print("Mixer name: '%s'" % mixer.mixer())
volcap = mixer.volumecap() print("Capabilities: %s %s" % (' '.join(mixer.volumecap()),
print("Capabilities: %s %s" % (' '.join(volcap),
' '.join(mixer.switchcap()))) ' '.join(mixer.switchcap())))
if "Volume" in volcap or "Joined Volume" in volcap or "Playback Volume" in volcap:
pmin, pmax = mixer.getrange(alsaaudio.PCM_PLAYBACK)
pmin_keyword, pmax_keyword = mixer.getrange(pcmtype=alsaaudio.PCM_PLAYBACK, units=alsaaudio.VOLUME_UNITS_RAW)
pmin_default, pmax_default = mixer.getrange()
assert pmin == pmin_keyword
assert pmax == pmax_keyword
assert pmin == pmin_default
assert pmax == pmax_default
print("Raw playback volume range {}-{}".format(pmin, pmax))
pmin_dB, pmax_dB = mixer.getrange(units=alsaaudio.VOLUME_UNITS_DB)
print("dB playback volume range {}-{}".format(pmin_dB / 100.0, pmax_dB / 100.0))
if "Capture Volume" in volcap or "Joined Capture Volume" in volcap:
# Check that `getrange` works with keyword and positional arguments
cmin, cmax = mixer.getrange(alsaaudio.PCM_CAPTURE)
cmin_keyword, cmax_keyword = mixer.getrange(pcmtype=alsaaudio.PCM_CAPTURE, units=alsaaudio.VOLUME_UNITS_RAW)
assert cmin == cmin_keyword
assert cmax == cmax_keyword
print("Raw capture volume range {}-{}".format(cmin, cmax))
cmin_dB, cmax_dB = mixer.getrange(pcmtype=alsaaudio.PCM_CAPTURE, units=alsaaudio.VOLUME_UNITS_DB)
print("dB capture volume range {}-{}".format(cmin_dB / 100.0, cmax_dB / 100.0))
volumes = mixer.getvolume() volumes = mixer.getvolume()
volumes_dB = mixer.getvolume(units=alsaaudio.VOLUME_UNITS_DB)
for i in range(len(volumes)): for i in range(len(volumes)):
print("Channel %i playback volume: %i%% (%.1f dB)" % (i, volumes[i], volumes_dB[i] / 100.0)) print("Channel %i volume: %i%%" % (i,volumes[i]))
volumes = mixer.getvolume(pcmtype=alsaaudio.PCM_CAPTURE)
volumes_dB = mixer.getvolume(pcmtype=alsaaudio.PCM_CAPTURE, units=alsaaudio.VOLUME_UNITS_DB)
for i in range(len(volumes)):
print("Channel %i capture volume: %i%% (%.1f dB)" % (i, volumes[i], volumes_dB[i] / 100.0))
try: try:
mutes = mixer.getmute() mutes = mixer.getmute()
+12 -7
View File
@@ -1,5 +1,4 @@
#!/usr/bin/env python3 #!/usr/bin/env python
# -*- mode: python; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*-
## playbacktest.py ## playbacktest.py
## ##
@@ -39,16 +38,22 @@ if __name__ == '__main__':
f = open(args[0], 'rb') f = open(args[0], 'rb')
# Open the device in playback mode in Mono, 44100 Hz, 16 bit little endian frames # Open the device in playback mode.
out = alsaaudio.PCM(alsaaudio.PCM_PLAYBACK, device=device)
# Set attributes: Mono, 44100 Hz, 16 bit little endian frames
out.setchannels(1)
out.setrate(44100)
out.setformat(alsaaudio.PCM_FORMAT_S16_LE)
# The period size controls the internal number of frames per period. # The period size controls the internal number of frames per period.
# The significance of this parameter is documented in the ALSA api. # The significance of this parameter is documented in the ALSA api.
out.setperiodsize(160)
out = alsaaudio.PCM(alsaaudio.PCM_PLAYBACK, channels=1, rate=44100, format=alsaaudio.PCM_FORMAT_S16_LE, periodsize=160, device=device)
# Read data from stdin # Read data from stdin
data = f.read(320) data = f.read(320)
while data: while data:
if out.write(data) < 0: out.write(data)
print("Playback buffer underrun! Continuing nonetheless ...")
data = f.read(320) data = f.read(320)
out.close()
Executable
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env python
# Simple test script that plays (some) wav files
from __future__ import print_function
import sys
import wave
import getopt
import alsaaudio
def play(device, f):
print('%d channels, %d sampling rate\n' % (f.getnchannels(),
f.getframerate()))
# Set attributes
device.setchannels(f.getnchannels())
device.setrate(f.getframerate())
# 8bit is unsigned in wav files
if f.getsampwidth() == 1:
device.setformat(alsaaudio.PCM_FORMAT_U8)
# Otherwise we assume signed data, little endian
elif f.getsampwidth() == 2:
device.setformat(alsaaudio.PCM_FORMAT_S16_LE)
elif f.getsampwidth() == 3:
device.setformat(alsaaudio.PCM_FORMAT_S24_3LE)
elif f.getsampwidth() == 4:
device.setformat(alsaaudio.PCM_FORMAT_S32_LE)
else:
raise ValueError('Unsupported format')
periodsize = f.getframerate() // 8
device.setperiodsize(periodsize)
data = f.readframes(periodsize)
while data:
# Read data from stdin
device.write(data)
data = f.readframes(periodsize)
def usage():
print('usage: playwav.py [-d <device>] <file>', file=sys.stderr)
sys.exit(2)
if __name__ == '__main__':
device = 'default'
opts, args = getopt.getopt(sys.argv[1:], 'd:')
for o, a in opts:
if o == '-d':
device = a
if not args:
usage()
f = wave.open(args[0], 'rb')
device = alsaaudio.PCM(device=device)
play(device, f)
f.close()
-70
View File
@@ -1,70 +0,0 @@
[project]
name = "pyalsaaudio"
version = "0.11.0"
description = "ALSA bindings"
readme.text = """
This package contains wrappers for accessing the ALSA API from Python.
It is fairly complete for PCM devices and Mixer access.
"""
readme.content-type = "text/plain"
license.file = "LICENSE"
# NOTE: license as a table has been deprecated by PEP-639, however support in
# setuptools has been introduced in v77.0 which is not installable on Python
# 3.9. Since this project doesn't constraint the minimum python version, we have
# to stick to the legacy `license` definition for now.
# license="PSF-2.0"
# license-files=["LICENSE"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Operating System :: POSIX :: Linux",
"Programming Language :: Python :: 3",
"Topic :: Multimedia :: Sound/Audio",
"Topic :: Multimedia :: Sound/Audio :: Mixers",
"Topic :: Multimedia :: Sound/Audio :: Players",
"Topic :: Multimedia :: Sound/Audio :: Capture/Recording",
]
[[project.authors]]
name = "Casper Wilstrup"
email = "cwi@aves.dk"
[[project.maintainers]]
name = "Lars Immisch"
email = "lars@ibp.de"
[project.urls]
homepage = "http://larsimmisch.github.io/pyalsaaudio/"
source = "https://github.com/larsimmisch/pyalsaaudio"
documentation = "https://larsimmisch.github.io/pyalsaaudio/"
issues = "https://github.com/larsimmisch/pyalsaaudio/issues"
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
[tool.setuptools]
ext-modules = [
{ name = "alsaaudio", sources = ["src/alsaaudio.c"], libraries = ["asound"] }
]
[tool.cibuildwheel]
build-frontend = "build[uv]"
before-all = "ci/setup_cibuildwheel.sh"
[dependency-groups]
lint = [
"mypy>=1.17.1",
"pyright>=1.1.403",
]
doc = [
"sphinx>=8.1.3",
]
Executable
+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env python
## recordtest.py
##
## This is an example of a simple sound capture script.
##
## The script opens an ALSA pcm device for sound capture, sets
## various attributes of the capture, and reads in a loop,
## writing the data to standard out.
##
## To test it out do the following:
## python recordtest.py out.raw # talk to the microphone
## aplay -r 8000 -f S16_LE -c 1 out.raw
#!/usr/bin/env python
from __future__ import print_function
import sys
import time
import getopt
import alsaaudio
def usage():
print('usage: recordtest.py [-d <device>] <file>', file=sys.stderr)
sys.exit(2)
if __name__ == '__main__':
device = 'default'
opts, args = getopt.getopt(sys.argv[1:], 'd:')
for o, a in opts:
if o == '-d':
device = a
if not args:
usage()
f = open(args[0], 'wb')
# Open the device in nonblocking capture mode. The last argument could
# just as well have been zero for blocking mode. Then we could have
# left out the sleep call in the bottom of the loop
inp = alsaaudio.PCM(alsaaudio.PCM_CAPTURE, alsaaudio.PCM_NONBLOCK, device=device)
# Set attributes: Mono, 44100 Hz, 16 bit little endian samples
inp.setchannels(1)
inp.setrate(44100)
inp.setformat(alsaaudio.PCM_FORMAT_S16_LE)
# The period size controls the internal number of frames per period.
# The significance of this parameter is documented in the ALSA api.
# For our purposes, it is suficcient to know that reads from the device
# will return this many frames. Each frame being 2 bytes long.
# This means that the reads below will return either 320 bytes of data
# or 0 bytes of data. The latter is possible because we are in nonblocking
# mode.
inp.setperiodsize(160)
loops = 1000000
while loops > 0:
loops -= 1
# Read data from device
l, data = inp.read()
if l:
f.write(data)
time.sleep(.001)
Executable
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env python
'''This package contains wrappers for accessing the ALSA API from Python.
It is fairly complete for PCM devices and Mixer access.
'''
from setuptools import setup
from setuptools.extension import Extension
from sys import version
pyalsa_version = '0.8.6'
if __name__ == '__main__':
setup(
name = 'pyalsaaudio',
version = pyalsa_version,
description = 'ALSA bindings',
long_description = __doc__,
author = 'Casper Wilstrup',
author_email='cwi@aves.dk',
maintainer = 'Lars Immisch',
maintainer_email = 'lars@ibp.de',
license='PSF',
platforms=['posix'],
url='http://larsimmisch.github.io/pyalsaaudio/',
classifiers = [
'Development Status :: 4 - Beta',
'Intended Audience :: Developers',
'License :: OSI Approved :: Python Software Foundation License',
'Operating System :: POSIX :: Linux',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 3',
'Topic :: Multimedia :: Sound/Audio',
'Topic :: Multimedia :: Sound/Audio :: Mixers',
'Topic :: Multimedia :: Sound/Audio :: Players',
'Topic :: Multimedia :: Sound/Audio :: Capture/Recording',
],
ext_modules=[Extension('alsaaudio',['alsaaudio.c'],
libraries=['asound'])]
)
-154
View File
@@ -1,154 +0,0 @@
from typing import Final, final
PCM_PLAYBACK: Final[int]
PCM_CAPTURE: Final[int]
PCM_NORMAL: Final[int]
PCM_NONBLOCK: Final[int]
PCM_ASYNC: Final[int]
PCM_FORMAT_S8: Final[int]
PCM_FORMAT_U8: Final[int]
PCM_FORMAT_S16_LE: Final[int]
PCM_FORMAT_S16_BE: Final[int]
PCM_FORMAT_U16_LE: Final[int]
PCM_FORMAT_U16_BE: Final[int]
PCM_FORMAT_S24_LE: Final[int]
PCM_FORMAT_S24_BE: Final[int]
PCM_FORMAT_U24_LE: Final[int]
PCM_FORMAT_U24_BE: Final[int]
PCM_FORMAT_S32_LE: Final[int]
PCM_FORMAT_S32_BE: Final[int]
PCM_FORMAT_U32_LE: Final[int]
PCM_FORMAT_U32_BE: Final[int]
PCM_FORMAT_FLOAT_LE: Final[int]
PCM_FORMAT_FLOAT_BE: Final[int]
PCM_FORMAT_FLOAT64_LE: Final[int]
PCM_FORMAT_FLOAT64_BE: Final[int]
PCM_FORMAT_MU_LAW: Final[int]
PCM_FORMAT_A_LAW: Final[int]
PCM_FORMAT_IMA_ADPCM: Final[int]
PCM_FORMAT_MPEG: Final[int]
PCM_FORMAT_GSM: Final[int]
PCM_FORMAT_S24_3LE: Final[int]
PCM_FORMAT_S24_3BE: Final[int]
PCM_FORMAT_U24_3LE: Final[int]
PCM_FORMAT_U24_3BE: Final[int]
PCM_TSTAMP_NONE: Final[int]
PCM_TSTAMP_ENABLE: Final[int]
PCM_TSTAMP_TYPE_GETTIMEOFDAY: Final[int]
PCM_TSTAMP_TYPE_MONOTONIC: Final[int]
PCM_TSTAMP_TYPE_MONOTONIC_RAW: Final[int]
PCM_FORMAT_DSD_U8: Final[int]
PCM_FORMAT_DSD_U16_LE: Final[int]
PCM_FORMAT_DSD_U32_LE: Final[int]
PCM_FORMAT_DSD_U32_BE: Final[int]
PCM_STATE_OPEN: Final[int]
PCM_STATE_SETUP: Final[int]
PCM_STATE_PREPARED: Final[int]
PCM_STATE_RUNNING: Final[int]
PCM_STATE_XRUN: Final[int]
PCM_STATE_DRAINING: Final[int]
PCM_STATE_PAUSED: Final[int]
PCM_STATE_SUSPENDED: Final[int]
PCM_STATE_DISCONNECTED: Final[int]
MIXER_CHANNEL_ALL: Final[int]
# NOTE: Omit for now - use case unknown (see alsaaudio.c)
#MIXER_SCHN_UNKNOWN: Final[int]
#MIXER_SCHN_FRONT_LEFT: Final[int]
#MIXER_SCHN_FRONT_RIGHT: Final[int]
#MIXER_SCHN_REAR_LEFT: Final[int]
#MIXER_SCHN_REAR_RIGHT: Final[int]
#MIXER_SCHN_FRONT_CENTER: Final[int]
#MIXER_SCHN_WOOFER: Final[int]
#MIXER_SCHN_SIDE_LEFT: Final[int]
#MIXER_SCHN_SIDE_RIGHT: Final[int]
#MIXER_SCHN_REAR_CENTER: Final[int]
#MIXER_SCHN_MONO: Final[int]
VOLUME_UNITS_PERCENTAGE: Final[int]
VOLUME_UNITS_RAW: Final[int]
VOLUME_UNITS_DB: Final[int]
def pcms(pcmtype: int) -> list[str]: ...
def cards() -> list[str]: ...
def mixers(cardindex: int = -1, device: str = 'default') -> list[str]: ...
def asoundlib_version() -> str: ...
def card_indexes() -> list[int]: ...
def card_name(index: int): ...
_DEPRECATED = ...
@final
class PCM:
def __init__(
self,
type: int = PCM_PLAYBACK,
mode: int = PCM_NORMAL,
device: str = "default",
cardindex: int = -1,
card: str = _DEPRECATED,
rate: int = 44100,
channels: int = 2,
format: int = PCM_FORMAT_S16_LE,
periodsize: int = 32,
periods: int = 4,
) -> None: ...
def close(self) -> None: ...
def dumpinfo(self) -> None: ...
def info(self) -> dict: ...
def state(self) -> int: ...
def htimestamp(self) -> tuple[int, int, int]: ...
def set_tstamp_mode(self, mode: int = PCM_TSTAMP_ENABLE) -> None: ...
def get_tstamp_mode(self) -> int: ...
def set_tstamp_type(self, type: int = PCM_TSTAMP_TYPE_GETTIMEOFDAY) -> None: ...
def get_tstamp_type(self) -> int: ...
def getformats(self) -> dict: ...
def getratebounds(self) -> tuple[int, int]: ...
def getrates(self) -> int | tuple[int, int] | list[int]: ...
def getchannels(self) -> list[int]: ...
def setchannels(self, nchannels: int) -> None: ...
def pcmtype(self) -> int: ...
def pcmmode(self) -> int: ...
def cardname(self) -> str: ...
def setrate(self, rate: int) -> None: ...
def setformat(self, format: int) -> int: ...
def setperiodsize(self, period: int) -> int: ...
def read(self) -> tuple[int, bytes]: ...
def write(self, data: bytes) -> int: ...
def avail(self) -> int: ...
def pause(self, enable: bool = True) -> int: ...
def drop(self) -> int: ...
def drain(self) -> int: ...
def polldescriptors(self) -> list[tuple[int, int]]: ...
def polldescriptors_revents(self, descriptors: list[tuple[int, int]]) -> int: ...
@final
class Mixer:
def __init__(self, control: str = 'Master', id: int = 0, cardindex: int = -1, device: str = 'default') -> None: ...
def cardname(self) -> str: ...
def close(self) -> None: ...
def mixer(self) -> str: ...
def mixerid(self) -> int: ...
def switchcap(self) -> int: ...
def volumecap(self) -> int: ...
def getvolume(self, pcmtype: int = PCM_PLAYBACK, units: int = VOLUME_UNITS_PERCENTAGE) -> int: ...
def getrange(self, pcmtype: int = PCM_PLAYBACK, units: int = VOLUME_UNITS_RAW) -> tuple[int, int]: ...
def getenum(self) -> tuple[str, list[str]]: ...
def getmute(self) -> list[int]: ...
def getrec(self) -> list[int]: ...
def setvolume(self, volume: int, pcmtype: int = PCM_PLAYBACK, units: int = VOLUME_UNITS_PERCENTAGE, channel: (int | None) = None) -> None: ...
def setenum(self, index: int) -> None: ...
def setmute(self, mute: bool, channel: (int | None) = None) -> None: ...
def setrec(self, capture: int, channel: (int | None) = None) -> None: ...
def polldescriptors(self) -> list[tuple[int, int]]: ...
def handleevents(self) -> int: ...
class ALSAAudioError(Exception): ...
-3252
View File
File diff suppressed because it is too large Load Diff
Executable
+136
View File
@@ -0,0 +1,136 @@
#!/usr/bin/env python
# These are internal tests. They shouldn't fail, but they don't cover all
# of the ALSA API. Most importantly PCM.read and PCM.write are missing.
# These need to be tested by playbacktest.py and recordtest.py
# In case of a problem, run these tests. If they fail, file a bug report on
# http://github.com/larsimmisch/pyalsaaudio/issues
import unittest
import alsaaudio
import warnings
# we can't test read and write well - these are tested otherwise
PCMMethods = [('pcmtype', None),
('pcmmode', None),
('cardname', None),
('setchannels', (2,)),
('setrate', (44100,)),
('setformat', (alsaaudio.PCM_FORMAT_S8,)),
('setperiodsize', (320,))]
# A clever test would look at the Mixer capabilities and selectively run the
# omitted tests, but I am too tired for that.
MixerMethods = [('cardname', None),
('mixer', None),
('mixerid', None),
('switchcap', None),
('volumecap', None),
('getvolume', None),
('getrange', None),
('getenum', None),
# ('getmute', None),
# ('getrec', None),
# ('setvolume', (60,)),
# ('setmute', (0,))
# ('setrec', (0')),
]
class MixerTest(unittest.TestCase):
"""Test Mixer objects"""
def testMixer(self):
"""Open the default Mixers and the Mixers on every card"""
for c in alsaaudio.card_indexes():
mixers = alsaaudio.mixers(cardindex=c)
for m in mixers:
mixer = alsaaudio.Mixer(m, cardindex=c)
mixer.close()
def testMixerAll(self):
"Run common Mixer methods on an open object"
mixers = alsaaudio.mixers()
mixer = alsaaudio.Mixer(mixers[0])
for m, a in MixerMethods:
f = alsaaudio.Mixer.__dict__[m]
if a is None:
f(mixer)
else:
f(mixer, *a)
mixer.close()
def testMixerClose(self):
"""Run common Mixer methods on a closed object and verify it raises an
error"""
mixers = alsaaudio.mixers()
mixer = alsaaudio.Mixer(mixers[0])
mixer.close()
for m, a in MixerMethods:
f = alsaaudio.Mixer.__dict__[m]
if a is None:
self.assertRaises(alsaaudio.ALSAAudioError, f, mixer)
else:
self.assertRaises(alsaaudio.ALSAAudioError, f, mixer, *a)
class PCMTest(unittest.TestCase):
"""Test PCM objects"""
def testPCM(self):
"Open a PCM object on every card"
for c in alsaaudio.card_indexes():
pcm = alsaaudio.PCM(cardindex=c)
pcm.close()
def testPCMAll(self):
"Run all PCM methods on an open object"
pcm = alsaaudio.PCM()
for m, a in PCMMethods:
f = alsaaudio.PCM.__dict__[m]
if a is None:
f(pcm)
else:
f(pcm, *a)
pcm.close()
def testPCMClose(self):
"Run all PCM methods on a closed object and verify it raises an error"
pcm = alsaaudio.PCM()
pcm.close()
for m, a in PCMMethods:
f = alsaaudio.PCM.__dict__[m]
if a is None:
self.assertRaises(alsaaudio.ALSAAudioError, f, pcm)
else:
self.assertRaises(alsaaudio.ALSAAudioError, f, pcm, *a)
def testPCMDeprecated(self):
with warnings.catch_warnings(record=True) as w:
# Cause all warnings to always be triggered.
warnings.simplefilter("always")
try:
pcm = alsaaudio.PCM(card='default')
except alsaaudio.ALSAAudioError:
pass
# Verify we got a DepreciationWarning
assert len(w) == 1
assert issubclass(w[-1].category, DeprecationWarning)
if __name__ == '__main__':
unittest.main()
-190
View File
@@ -1,190 +0,0 @@
#!/usr/bin/env python3
# -*- mode: python; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*-
# These are internal tests. They shouldn't fail, but they don't cover all
# of the ALSA API. Most importantly PCM.read and PCM.write are missing.
# These need to be tested by playbacktest.py and recordtest.py
# In case of a problem, run these tests. If they fail, file a bug report on
# http://github.com/larsimmisch/pyalsaaudio/issues
import unittest
import alsaaudio
import warnings
from contextlib import closing
MethodsType = list[tuple[str, tuple[int, ...] | None]]
# we can't test read and write well - these are tested otherwise
PCMMethods: MethodsType = [
('pcmtype', None),
('pcmmode', None),
('cardname', None)
]
PCMDeprecatedMethods: MethodsType = [
('setchannels', (2,)),
('setrate', (44100,)),
('setformat', (alsaaudio.PCM_FORMAT_S8,)),
('setperiodsize', (320,))
]
# A clever test would look at the Mixer capabilities and selectively run the
# omitted tests, but I am too tired for that.
MixerMethods: MethodsType = [
('cardname', None),
('mixer', None),
('mixerid', None),
('switchcap', None),
('volumecap', None),
('getvolume', None),
('getrange', None),
('getenum', None),
# ('getmute', None),
# ('getrec', None),
# ('setvolume', (60,)),
# ('setmute', (0,))
# ('setrec', (0')),
]
class MixerTest(unittest.TestCase):
"""Test Mixer objects"""
def testMixer(self):
"""Open the default Mixers and the Mixers on every card"""
for c in alsaaudio.card_indexes():
mixers = alsaaudio.mixers(cardindex=c)
for m in mixers:
mixer = alsaaudio.Mixer(m, cardindex=c)
mixer.close()
def testMixerAll(self):
"Run common Mixer methods on an open object"
mixers = alsaaudio.mixers()
mixer = alsaaudio.Mixer(mixers[0])
for m, a in MixerMethods:
f = alsaaudio.Mixer.__dict__[m]
if a is None:
f(mixer)
else:
f(mixer, *a)
mixer.close()
def testMixerClose(self):
"""Run common Mixer methods on a closed object and verify it raises an
error"""
mixers = alsaaudio.mixers()
mixer = alsaaudio.Mixer(mixers[0])
mixer.close()
for m, a in MixerMethods:
f = alsaaudio.Mixer.__dict__[m]
if a is None:
self.assertRaises(alsaaudio.ALSAAudioError, f, mixer)
else:
self.assertRaises(alsaaudio.ALSAAudioError, f, mixer, *a)
class PCMTest(unittest.TestCase):
"""Test PCM objects"""
def testPCM(self):
"Open a PCM object on every card"
for c in alsaaudio.card_indexes():
pcm = alsaaudio.PCM(cardindex=c)
pcm.close()
def testPCMAll(self):
"Run all PCM methods on an open object"
pcm = alsaaudio.PCM()
for m, a in PCMMethods:
f = alsaaudio.PCM.__dict__[m]
if a is None:
f(pcm)
else:
f(pcm, *a)
pcm.close()
def testPCMClose(self):
"Run all PCM methods on a closed object and verify it raises an error"
pcm = alsaaudio.PCM()
pcm.close()
for m, a in PCMMethods:
f = alsaaudio.PCM.__dict__[m]
if a is None:
self.assertRaises(alsaaudio.ALSAAudioError, f, pcm)
else:
self.assertRaises(alsaaudio.ALSAAudioError, f, pcm, *a)
def testPCMDeprecated(self):
with warnings.catch_warnings(record=True) as w:
# Cause all warnings to always be triggered.
warnings.simplefilter("always")
try:
pcm = alsaaudio.PCM(card='default')
except alsaaudio.ALSAAudioError:
pass
# Verify we got a DepreciationWarning
self.assertEqual(len(w), 1, "PCM(card='default') expected a warning" )
self.assertTrue(issubclass(w[-1].category, DeprecationWarning), "PCM(card='default') expected a DeprecationWarning")
for m, a in PCMDeprecatedMethods:
with warnings.catch_warnings(record=True) as w:
# Cause all warnings to always be triggered.
warnings.simplefilter("always")
pcm = alsaaudio.PCM()
f = alsaaudio.PCM.__dict__[m]
if a is None:
f(pcm)
else:
f(pcm, *a)
# Verify we got a DepreciationWarning
method = "%s%s" % (m, str(a))
self.assertEqual(len(w), 1, method + " expected a warning")
self.assertTrue(issubclass(w[-1].category, DeprecationWarning), method + " expected a DeprecationWarning")
class PollDescriptorArgsTest(unittest.TestCase):
'''Test invalid args for polldescriptors_revents (takes a list of tuples of 2 integers)'''
def testArgsNoList(self):
with closing(alsaaudio.PCM()) as pcm:
with self.assertRaises(TypeError):
pcm.polldescriptors_revents('foo') # pyright: ignore[reportArgumentType]
def testArgsListButNoTuples(self):
with closing(alsaaudio.PCM()) as pcm:
with self.assertRaises(TypeError):
pcm.polldescriptors_revents(['foo', 1]) # pyright: ignore[reportArgumentType]
def testArgsListButInvalidTuples(self):
with closing(alsaaudio.PCM()) as pcm:
with self.assertRaises(TypeError):
pcm.polldescriptors_revents([('foo', 'bar')]) # pyright: ignore[reportArgumentType]
def testArgsListTupleWrongLength(self):
with closing(alsaaudio.PCM()) as pcm:
with self.assertRaises(TypeError):
pcm.polldescriptors_revents([(1, )]) # pyright: ignore[reportArgumentType]
with self.assertRaises(TypeError):
pcm.polldescriptors_revents([(1, 2, 3)]) # pyright: ignore[reportArgumentType]
if __name__ == '__main__':
unittest.main()
Generated
-534
View File
@@ -1,534 +0,0 @@
version = 1
revision = 3
requires-python = ">=3.10"
resolution-markers = [
"python_full_version >= '3.11'",
"python_full_version < '3.11'",
]
[[package]]
name = "alabaster"
version = "1.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" },
]
[[package]]
name = "babel"
version = "2.17.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" },
]
[[package]]
name = "certifi"
version = "2025.8.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" },
]
[[package]]
name = "charset-normalizer"
version = "3.4.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818, upload-time = "2025-05-02T08:31:46.725Z" },
{ url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649, upload-time = "2025-05-02T08:31:48.889Z" },
{ url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045, upload-time = "2025-05-02T08:31:50.757Z" },
{ url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356, upload-time = "2025-05-02T08:31:52.634Z" },
{ url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471, upload-time = "2025-05-02T08:31:56.207Z" },
{ url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317, upload-time = "2025-05-02T08:31:57.613Z" },
{ url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368, upload-time = "2025-05-02T08:31:59.468Z" },
{ url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491, upload-time = "2025-05-02T08:32:01.219Z" },
{ url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695, upload-time = "2025-05-02T08:32:03.045Z" },
{ url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849, upload-time = "2025-05-02T08:32:04.651Z" },
{ url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091, upload-time = "2025-05-02T08:32:06.719Z" },
{ url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445, upload-time = "2025-05-02T08:32:08.66Z" },
{ url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782, upload-time = "2025-05-02T08:32:10.46Z" },
{ url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794, upload-time = "2025-05-02T08:32:11.945Z" },
{ url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846, upload-time = "2025-05-02T08:32:13.946Z" },
{ url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350, upload-time = "2025-05-02T08:32:15.873Z" },
{ url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657, upload-time = "2025-05-02T08:32:17.283Z" },
{ url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260, upload-time = "2025-05-02T08:32:18.807Z" },
{ url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164, upload-time = "2025-05-02T08:32:20.333Z" },
{ url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571, upload-time = "2025-05-02T08:32:21.86Z" },
{ url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952, upload-time = "2025-05-02T08:32:23.434Z" },
{ url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959, upload-time = "2025-05-02T08:32:24.993Z" },
{ url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030, upload-time = "2025-05-02T08:32:26.435Z" },
{ url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015, upload-time = "2025-05-02T08:32:28.376Z" },
{ url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106, upload-time = "2025-05-02T08:32:30.281Z" },
{ url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402, upload-time = "2025-05-02T08:32:32.191Z" },
{ url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" },
{ url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" },
{ url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" },
{ url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" },
{ url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" },
{ url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" },
{ url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" },
{ url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" },
{ url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" },
{ url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" },
{ url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" },
{ url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" },
{ url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" },
{ url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" },
{ url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" },
{ url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" },
{ url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" },
{ url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" },
{ url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" },
{ url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" },
{ url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" },
{ url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" },
{ url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" },
{ url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" },
{ url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" },
{ url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" },
{ url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "docutils"
version = "0.21.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" },
]
[[package]]
name = "idna"
version = "3.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
]
[[package]]
name = "imagesize"
version = "1.4.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026, upload-time = "2022-07-01T12:21:05.687Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769, upload-time = "2022-07-01T12:21:02.467Z" },
]
[[package]]
name = "jinja2"
version = "3.1.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
]
[[package]]
name = "markupsafe"
version = "3.0.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357, upload-time = "2024-10-18T15:20:51.44Z" },
{ url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393, upload-time = "2024-10-18T15:20:52.426Z" },
{ url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732, upload-time = "2024-10-18T15:20:53.578Z" },
{ url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866, upload-time = "2024-10-18T15:20:55.06Z" },
{ url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964, upload-time = "2024-10-18T15:20:55.906Z" },
{ url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977, upload-time = "2024-10-18T15:20:57.189Z" },
{ url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366, upload-time = "2024-10-18T15:20:58.235Z" },
{ url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091, upload-time = "2024-10-18T15:20:59.235Z" },
{ url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065, upload-time = "2024-10-18T15:21:00.307Z" },
{ url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514, upload-time = "2024-10-18T15:21:01.122Z" },
{ url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" },
{ url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" },
{ url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" },
{ url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload-time = "2024-10-18T15:21:06.495Z" },
{ url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload-time = "2024-10-18T15:21:07.295Z" },
{ url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload-time = "2024-10-18T15:21:08.073Z" },
{ url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload-time = "2024-10-18T15:21:09.318Z" },
{ url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload-time = "2024-10-18T15:21:10.185Z" },
{ url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload-time = "2024-10-18T15:21:11.005Z" },
{ url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload-time = "2024-10-18T15:21:12.911Z" },
{ url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" },
{ url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" },
{ url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" },
{ url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" },
{ url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" },
{ url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" },
{ url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" },
{ url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" },
{ url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" },
{ url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" },
{ url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" },
{ url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" },
{ url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" },
{ url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" },
{ url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" },
{ url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" },
{ url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" },
{ url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" },
{ url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" },
{ url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" },
{ url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" },
{ url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" },
{ url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" },
{ url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" },
{ url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" },
{ url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" },
{ url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" },
{ url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" },
{ url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" },
{ url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" },
]
[[package]]
name = "mypy"
version = "1.17.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mypy-extensions" },
{ name = "pathspec" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8e/22/ea637422dedf0bf36f3ef238eab4e455e2a0dcc3082b5cc067615347ab8e/mypy-1.17.1.tar.gz", hash = "sha256:25e01ec741ab5bb3eec8ba9cdb0f769230368a22c959c4937360efb89b7e9f01", size = 3352570, upload-time = "2025-07-31T07:54:19.204Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/77/a9/3d7aa83955617cdf02f94e50aab5c830d205cfa4320cf124ff64acce3a8e/mypy-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3fbe6d5555bf608c47203baa3e72dbc6ec9965b3d7c318aa9a4ca76f465bd972", size = 11003299, upload-time = "2025-07-31T07:54:06.425Z" },
{ url = "https://files.pythonhosted.org/packages/83/e8/72e62ff837dd5caaac2b4a5c07ce769c8e808a00a65e5d8f94ea9c6f20ab/mypy-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80ef5c058b7bce08c83cac668158cb7edea692e458d21098c7d3bce35a5d43e7", size = 10125451, upload-time = "2025-07-31T07:53:52.974Z" },
{ url = "https://files.pythonhosted.org/packages/7d/10/f3f3543f6448db11881776f26a0ed079865926b0c841818ee22de2c6bbab/mypy-1.17.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a580f8a70c69e4a75587bd925d298434057fe2a428faaf927ffe6e4b9a98df", size = 11916211, upload-time = "2025-07-31T07:53:18.879Z" },
{ url = "https://files.pythonhosted.org/packages/06/bf/63e83ed551282d67bb3f7fea2cd5561b08d2bb6eb287c096539feb5ddbc5/mypy-1.17.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd86bb649299f09d987a2eebb4d52d10603224500792e1bee18303bbcc1ce390", size = 12652687, upload-time = "2025-07-31T07:53:30.544Z" },
{ url = "https://files.pythonhosted.org/packages/69/66/68f2eeef11facf597143e85b694a161868b3b006a5fbad50e09ea117ef24/mypy-1.17.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a76906f26bd8d51ea9504966a9c25419f2e668f012e0bdf3da4ea1526c534d94", size = 12896322, upload-time = "2025-07-31T07:53:50.74Z" },
{ url = "https://files.pythonhosted.org/packages/a3/87/8e3e9c2c8bd0d7e071a89c71be28ad088aaecbadf0454f46a540bda7bca6/mypy-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:e79311f2d904ccb59787477b7bd5d26f3347789c06fcd7656fa500875290264b", size = 9507962, upload-time = "2025-07-31T07:53:08.431Z" },
{ url = "https://files.pythonhosted.org/packages/46/cf/eadc80c4e0a70db1c08921dcc220357ba8ab2faecb4392e3cebeb10edbfa/mypy-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad37544be07c5d7fba814eb370e006df58fed8ad1ef33ed1649cb1889ba6ff58", size = 10921009, upload-time = "2025-07-31T07:53:23.037Z" },
{ url = "https://files.pythonhosted.org/packages/5d/c1/c869d8c067829ad30d9bdae051046561552516cfb3a14f7f0347b7d973ee/mypy-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:064e2ff508e5464b4bd807a7c1625bc5047c5022b85c70f030680e18f37273a5", size = 10047482, upload-time = "2025-07-31T07:53:26.151Z" },
{ url = "https://files.pythonhosted.org/packages/98/b9/803672bab3fe03cee2e14786ca056efda4bb511ea02dadcedde6176d06d0/mypy-1.17.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70401bbabd2fa1aa7c43bb358f54037baf0586f41e83b0ae67dd0534fc64edfd", size = 11832883, upload-time = "2025-07-31T07:53:47.948Z" },
{ url = "https://files.pythonhosted.org/packages/88/fb/fcdac695beca66800918c18697b48833a9a6701de288452b6715a98cfee1/mypy-1.17.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e92bdc656b7757c438660f775f872a669b8ff374edc4d18277d86b63edba6b8b", size = 12566215, upload-time = "2025-07-31T07:54:04.031Z" },
{ url = "https://files.pythonhosted.org/packages/7f/37/a932da3d3dace99ee8eb2043b6ab03b6768c36eb29a02f98f46c18c0da0e/mypy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c1fdf4abb29ed1cb091cf432979e162c208a5ac676ce35010373ff29247bcad5", size = 12751956, upload-time = "2025-07-31T07:53:36.263Z" },
{ url = "https://files.pythonhosted.org/packages/8c/cf/6438a429e0f2f5cab8bc83e53dbebfa666476f40ee322e13cac5e64b79e7/mypy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:ff2933428516ab63f961644bc49bc4cbe42bbffb2cd3b71cc7277c07d16b1a8b", size = 9507307, upload-time = "2025-07-31T07:53:59.734Z" },
{ url = "https://files.pythonhosted.org/packages/17/a2/7034d0d61af8098ec47902108553122baa0f438df8a713be860f7407c9e6/mypy-1.17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:69e83ea6553a3ba79c08c6e15dbd9bfa912ec1e493bf75489ef93beb65209aeb", size = 11086295, upload-time = "2025-07-31T07:53:28.124Z" },
{ url = "https://files.pythonhosted.org/packages/14/1f/19e7e44b594d4b12f6ba8064dbe136505cec813549ca3e5191e40b1d3cc2/mypy-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b16708a66d38abb1e6b5702f5c2c87e133289da36f6a1d15f6a5221085c6403", size = 10112355, upload-time = "2025-07-31T07:53:21.121Z" },
{ url = "https://files.pythonhosted.org/packages/5b/69/baa33927e29e6b4c55d798a9d44db5d394072eef2bdc18c3e2048c9ed1e9/mypy-1.17.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89e972c0035e9e05823907ad5398c5a73b9f47a002b22359b177d40bdaee7056", size = 11875285, upload-time = "2025-07-31T07:53:55.293Z" },
{ url = "https://files.pythonhosted.org/packages/90/13/f3a89c76b0a41e19490b01e7069713a30949d9a6c147289ee1521bcea245/mypy-1.17.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03b6d0ed2b188e35ee6d5c36b5580cffd6da23319991c49ab5556c023ccf1341", size = 12737895, upload-time = "2025-07-31T07:53:43.623Z" },
{ url = "https://files.pythonhosted.org/packages/23/a1/c4ee79ac484241301564072e6476c5a5be2590bc2e7bfd28220033d2ef8f/mypy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c837b896b37cd103570d776bda106eabb8737aa6dd4f248451aecf53030cdbeb", size = 12931025, upload-time = "2025-07-31T07:54:17.125Z" },
{ url = "https://files.pythonhosted.org/packages/89/b8/7409477be7919a0608900e6320b155c72caab4fef46427c5cc75f85edadd/mypy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:665afab0963a4b39dff7c1fa563cc8b11ecff7910206db4b2e64dd1ba25aed19", size = 9584664, upload-time = "2025-07-31T07:54:12.842Z" },
{ url = "https://files.pythonhosted.org/packages/5b/82/aec2fc9b9b149f372850291827537a508d6c4d3664b1750a324b91f71355/mypy-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93378d3203a5c0800c6b6d850ad2f19f7a3cdf1a3701d3416dbf128805c6a6a7", size = 11075338, upload-time = "2025-07-31T07:53:38.873Z" },
{ url = "https://files.pythonhosted.org/packages/07/ac/ee93fbde9d2242657128af8c86f5d917cd2887584cf948a8e3663d0cd737/mypy-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15d54056f7fe7a826d897789f53dd6377ec2ea8ba6f776dc83c2902b899fee81", size = 10113066, upload-time = "2025-07-31T07:54:14.707Z" },
{ url = "https://files.pythonhosted.org/packages/5a/68/946a1e0be93f17f7caa56c45844ec691ca153ee8b62f21eddda336a2d203/mypy-1.17.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:209a58fed9987eccc20f2ca94afe7257a8f46eb5df1fb69958650973230f91e6", size = 11875473, upload-time = "2025-07-31T07:53:14.504Z" },
{ url = "https://files.pythonhosted.org/packages/9f/0f/478b4dce1cb4f43cf0f0d00fba3030b21ca04a01b74d1cd272a528cf446f/mypy-1.17.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:099b9a5da47de9e2cb5165e581f158e854d9e19d2e96b6698c0d64de911dd849", size = 12744296, upload-time = "2025-07-31T07:53:03.896Z" },
{ url = "https://files.pythonhosted.org/packages/ca/70/afa5850176379d1b303f992a828de95fc14487429a7139a4e0bdd17a8279/mypy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ffadfbe6994d724c5a1bb6123a7d27dd68fc9c059561cd33b664a79578e14", size = 12914657, upload-time = "2025-07-31T07:54:08.576Z" },
{ url = "https://files.pythonhosted.org/packages/53/f9/4a83e1c856a3d9c8f6edaa4749a4864ee98486e9b9dbfbc93842891029c2/mypy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:9a2b7d9180aed171f033c9f2fc6c204c1245cf60b0cb61cf2e7acc24eea78e0a", size = 9593320, upload-time = "2025-07-31T07:53:01.341Z" },
{ url = "https://files.pythonhosted.org/packages/38/56/79c2fac86da57c7d8c48622a05873eaab40b905096c33597462713f5af90/mypy-1.17.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:15a83369400454c41ed3a118e0cc58bd8123921a602f385cb6d6ea5df050c733", size = 11040037, upload-time = "2025-07-31T07:54:10.942Z" },
{ url = "https://files.pythonhosted.org/packages/4d/c3/adabe6ff53638e3cad19e3547268482408323b1e68bf082c9119000cd049/mypy-1.17.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:55b918670f692fc9fba55c3298d8a3beae295c5cded0a55dccdc5bbead814acd", size = 10131550, upload-time = "2025-07-31T07:53:41.307Z" },
{ url = "https://files.pythonhosted.org/packages/b8/c5/2e234c22c3bdeb23a7817af57a58865a39753bde52c74e2c661ee0cfc640/mypy-1.17.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:62761474061feef6f720149d7ba876122007ddc64adff5ba6f374fda35a018a0", size = 11872963, upload-time = "2025-07-31T07:53:16.878Z" },
{ url = "https://files.pythonhosted.org/packages/ab/26/c13c130f35ca8caa5f2ceab68a247775648fdcd6c9a18f158825f2bc2410/mypy-1.17.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c49562d3d908fd49ed0938e5423daed8d407774a479b595b143a3d7f87cdae6a", size = 12710189, upload-time = "2025-07-31T07:54:01.962Z" },
{ url = "https://files.pythonhosted.org/packages/82/df/c7d79d09f6de8383fe800521d066d877e54d30b4fb94281c262be2df84ef/mypy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:397fba5d7616a5bc60b45c7ed204717eaddc38f826e3645402c426057ead9a91", size = 12900322, upload-time = "2025-07-31T07:53:10.551Z" },
{ url = "https://files.pythonhosted.org/packages/b8/98/3d5a48978b4f708c55ae832619addc66d677f6dc59f3ebad71bae8285ca6/mypy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:9d6b20b97d373f41617bd0708fd46aa656059af57f2ef72aa8c7d6a2b73b74ed", size = 9751879, upload-time = "2025-07-31T07:52:56.683Z" },
{ url = "https://files.pythonhosted.org/packages/1d/f3/8fcd2af0f5b806f6cf463efaffd3c9548a28f84220493ecd38d127b6b66d/mypy-1.17.1-py3-none-any.whl", hash = "sha256:a9f52c0351c21fe24c21d8c0eb1f62967b262d6729393397b6f443c3b773c3b9", size = 2283411, upload-time = "2025-07-31T07:53:24.664Z" },
]
[[package]]
name = "mypy-extensions"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
]
[[package]]
name = "nodeenv"
version = "1.9.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" },
]
[[package]]
name = "packaging"
version = "25.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
]
[[package]]
name = "pathspec"
version = "0.12.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" },
]
[[package]]
name = "pyalsaaudio"
version = "0.11.0"
source = { editable = "." }
[package.dev-dependencies]
doc = [
{ name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
]
lint = [
{ name = "mypy" },
{ name = "pyright" },
]
[package.metadata]
[package.metadata.requires-dev]
doc = [{ name = "sphinx", specifier = ">=8.1.3" }]
lint = [
{ name = "mypy", specifier = ">=1.17.1" },
{ name = "pyright", specifier = ">=1.1.403" },
]
[[package]]
name = "pygments"
version = "2.19.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
]
[[package]]
name = "pyright"
version = "1.1.403"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nodeenv" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fe/f6/35f885264ff08c960b23d1542038d8da86971c5d8c955cfab195a4f672d7/pyright-1.1.403.tar.gz", hash = "sha256:3ab69b9f41c67fb5bbb4d7a36243256f0d549ed3608678d381d5f51863921104", size = 3913526, upload-time = "2025-07-09T07:15:52.882Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/49/b6/b04e5c2f41a5ccad74a1a4759da41adb20b4bc9d59a5e08d29ba60084d07/pyright-1.1.403-py3-none-any.whl", hash = "sha256:c0eeca5aa76cbef3fcc271259bbd785753c7ad7bcac99a9162b4c4c7daed23b3", size = 5684504, upload-time = "2025-07-09T07:15:50.958Z" },
]
[[package]]
name = "requests"
version = "2.32.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" },
]
[[package]]
name = "roman-numerals-py"
version = "3.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/30/76/48fd56d17c5bdbdf65609abbc67288728a98ed4c02919428d4f52d23b24b/roman_numerals_py-3.1.0.tar.gz", hash = "sha256:be4bf804f083a4ce001b5eb7e3c0862479d10f94c936f6c4e5f250aa5ff5bd2d", size = 9017, upload-time = "2025-02-22T07:34:54.333Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/53/97/d2cbbaa10c9b826af0e10fdf836e1bf344d9f0abb873ebc34d1f49642d3f/roman_numerals_py-3.1.0-py3-none-any.whl", hash = "sha256:9da2ad2fb670bcf24e81070ceb3be72f6c11c440d73bd579fbeca1e9f330954c", size = 7742, upload-time = "2025-02-22T07:34:52.422Z" },
]
[[package]]
name = "snowballstemmer"
version = "3.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" },
]
[[package]]
name = "sphinx"
version = "8.1.3"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.11'",
]
dependencies = [
{ name = "alabaster", marker = "python_full_version < '3.11'" },
{ name = "babel", marker = "python_full_version < '3.11'" },
{ name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" },
{ name = "docutils", marker = "python_full_version < '3.11'" },
{ name = "imagesize", marker = "python_full_version < '3.11'" },
{ name = "jinja2", marker = "python_full_version < '3.11'" },
{ name = "packaging", marker = "python_full_version < '3.11'" },
{ name = "pygments", marker = "python_full_version < '3.11'" },
{ name = "requests", marker = "python_full_version < '3.11'" },
{ name = "snowballstemmer", marker = "python_full_version < '3.11'" },
{ name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.11'" },
{ name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.11'" },
{ name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.11'" },
{ name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.11'" },
{ name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.11'" },
{ name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.11'" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611, upload-time = "2024-10-13T20:27:13.93Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/26/60/1ddff83a56d33aaf6f10ec8ce84b4c007d9368b21008876fceda7e7381ef/sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2", size = 3487125, upload-time = "2024-10-13T20:27:10.448Z" },
]
[[package]]
name = "sphinx"
version = "8.2.3"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.11'",
]
dependencies = [
{ name = "alabaster", marker = "python_full_version >= '3.11'" },
{ name = "babel", marker = "python_full_version >= '3.11'" },
{ name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" },
{ name = "docutils", marker = "python_full_version >= '3.11'" },
{ name = "imagesize", marker = "python_full_version >= '3.11'" },
{ name = "jinja2", marker = "python_full_version >= '3.11'" },
{ name = "packaging", marker = "python_full_version >= '3.11'" },
{ name = "pygments", marker = "python_full_version >= '3.11'" },
{ name = "requests", marker = "python_full_version >= '3.11'" },
{ name = "roman-numerals-py", marker = "python_full_version >= '3.11'" },
{ name = "snowballstemmer", marker = "python_full_version >= '3.11'" },
{ name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.11'" },
{ name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.11'" },
{ name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.11'" },
{ name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.11'" },
{ name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.11'" },
{ name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/38/ad/4360e50ed56cb483667b8e6dadf2d3fda62359593faabbe749a27c4eaca6/sphinx-8.2.3.tar.gz", hash = "sha256:398ad29dee7f63a75888314e9424d40f52ce5a6a87ae88e7071e80af296ec348", size = 8321876, upload-time = "2025-03-02T22:31:59.658Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/31/53/136e9eca6e0b9dc0e1962e2c908fbea2e5ac000c2a2fbd9a35797958c48b/sphinx-8.2.3-py3-none-any.whl", hash = "sha256:4405915165f13521d875a8c29c8970800a0141c14cc5416a38feca4ea5d9b9c3", size = 3589741, upload-time = "2025-03-02T22:31:56.836Z" },
]
[[package]]
name = "sphinxcontrib-applehelp"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" },
]
[[package]]
name = "sphinxcontrib-devhelp"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" },
]
[[package]]
name = "sphinxcontrib-htmlhelp"
version = "2.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" },
]
[[package]]
name = "sphinxcontrib-jsmath"
version = "1.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" },
]
[[package]]
name = "sphinxcontrib-qthelp"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" },
]
[[package]]
name = "sphinxcontrib-serializinghtml"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" },
]
[[package]]
name = "tomli"
version = "2.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" },
{ url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" },
{ url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" },
{ url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" },
{ url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" },
{ url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" },
{ url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" },
{ url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" },
{ url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" },
{ url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" },
{ url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" },
{ url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" },
{ url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" },
{ url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" },
{ url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" },
{ url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" },
{ url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" },
{ url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" },
{ url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" },
{ url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" },
{ url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" },
{ url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" },
{ url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" },
{ url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" },
{ url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" },
{ url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" },
{ url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" },
{ url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" },
{ url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" },
{ url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" },
{ url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" },
]
[[package]]
name = "typing-extensions"
version = "4.14.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" },
]
[[package]]
name = "urllib3"
version = "2.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
]