pax_global_header00006660000000000000000000000064151642115550014517gustar00rootroot0000000000000052 comment=e9025195a6c65f5f1d528e3abe77d20a5e16c09c pylibftdi-0.24.1/000077500000000000000000000000001516421155500135715ustar00rootroot00000000000000pylibftdi-0.24.1/.github/000077500000000000000000000000001516421155500151315ustar00rootroot00000000000000pylibftdi-0.24.1/.github/workflows/000077500000000000000000000000001516421155500171665ustar00rootroot00000000000000pylibftdi-0.24.1/.github/workflows/python-lint-and-test.yml000066400000000000000000000013761516421155500237220ustar00rootroot00000000000000name: Python package on: [push] jobs: build: runs-on: ubuntu-latest strategy: matrix: python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v4 - name: Install uv uses: astral-sh/setup-uv@v5 - name: Set up Python ${{ matrix.python-version }} run: uv python install ${{ matrix.python-version }} - name: Install dependencies run: uv sync --python ${{ matrix.python-version }} - name: Format check with ruff run: uv run ruff format --check . - name: Lint with ruff run: uv run ruff check src tests - name: Typecheck with mypy run: uv run mypy src/ tests/ - name: Test with pytest run: uv run pytest pylibftdi-0.24.1/.gitignore000066400000000000000000000001371516421155500155620ustar00rootroot00000000000000*.pyc *.pyo .idea .vscode .DS_Store build dist docs/_build *.egg-info .venv _build docout pylibftdi-0.24.1/.readthedocs.yaml000066400000000000000000000010661516421155500170230ustar00rootroot00000000000000# .readthedocs.yaml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Build all formats (HTML, epub, PDF) formats: all # Set the version of Python and other tools you might need build: os: ubuntu-24.04 tools: python: "3.12" # Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/conf.py python: install: - requirements: docs/requirements.txt # Install the package itself for importlib.metadata - method: pip path: . pylibftdi-0.24.1/CHANGES.txt000066400000000000000000000342141516421155500154060ustar00rootroot00000000000000pylibftdi changes ================= 0.24.1 ------ * No functional changes; CI & docs fixes only 0.24.0 ------ * Fix: `flush()` now uses `ftdi_tc[io]flush` functions (libftdi >= 1.5) rather than the deprecated `ftdi_usb_purge_*` functions - thanks @syha6821! * Build: switched from poetry to uv for development and packaging * Dropped support for Python < 3.10 0.23.0 ------ * Added: #12 - pid & vid selection when opening a new `Device` - thanks @maraxen! * Minor API change: * removed use of kwargs for Device() and Driver() arguments, replacing with keyword-only arguments. Tthis is unlikely to cause issues in most cases, and protects against typos or incorrect arguments. * one valid case requiring change is passing in a `libftdi_search` argument; if this is required, create a driver instance separately (with this parameter) and pass it to `Device()` using the new `driver` keyword-only parameter. * Various bumps and fixes to linting dependencies 0.22.0 ------ * Fix: #7 - support for Python3.12 * Fix: #6 - prevent BitBangDevice.port values exceeding 8 bits * Bumps to linting dependencies (ruff, mypy, black, pylint) 0.21.0 ------ * Breaking changes: * Python 3.7+ is now required for pylibftdi. * Driver.libusb_version() and libftdi_version() now return strings rather than bytestrings for textual version info * Development changes: * Type hinting has been added - thanks @WhatTheFuzz! * Moved to a `src` layout. See https://packaging.python.org/en/latest/discussions/src-layout-vs-flat-layout/ * Switched from `setuptools` to `poetry` for development. * Linting and style changes (Ruff & black) have been applied throughout. * Additional unit tests, especially covering library loading. * Added a Dockerfile and Makefile for development and testing. * Added GitHub Actions CI for linting and unittests. 0.20.0 ------ * Removed claims of Python2 support, added warnings that Python2 support will be removed in future releases. NOTE: Future releases will require Python 3.6 or later. * Fix to `ftdi_get_library_version`, broken on M1 Macs. See Github issue #2. Thanks to @wrigby for diagnosing and fixing this. 0.19.0 ------ * Moved to GitHub (and git, obviously!) following deprecation of Mercurial on bitbucket.org. No other changes beyond URL updates, docs pointing to GitHub etc. Note that references to old bitbucket issues will stop being helpful very soon, but there's not much I can do about this. 0.18.1 ~~~~~~ * fixed - list_devices example was broken for Python3 following 0.18.0 0.18.0 ~~~~~~ * fixed - memory leak in list_devices() (Bitbucket issue #34) * changed - examples now written for (and require) Python3 (Bitbucket issue #35) * new - more helpful platform-dependent error messages * added - __main__.py module to support `python -im pylibftdi` * added - `index` parameter to Device() constructor to allow indexing into the list returned from Driver.list_devices(). Useful to access e.g. the second device where PID differs between the first two devices and device_index would reset to zero for each PID/VID combination. * Removed the ftdi_osx_driver_reload/unload scripts. These complicated cross- platform packaging and error messages / docs have been updated to include appropriate instructions. 0.17.0 ~~~~~~ * new - added 0x6015 USB PID to the default PID list, supporting FT230 devices * fixed - updated ftdi_osx_driver_re/unload scripts to support Python3 * fixed - auto_detach bug on some systems (Bitbucket issue #27) * *API CHANGE* - Moved `auto_detach`, `chunk_size` and `lazy_open` to class attributes which can be easily overridden in subclasses, or in kwargs. This means that any program using these as *positional* arguments needs to change to use them as keyword parameters. Note: all but `device_id` and `mode` may become keyword-only arguments in the future. 0.16.1.2 ~~~~~~~~ * rebuilt to fix install issue on macOS 0.16.1.1 ~~~~~~~~ * changed / fixed - still getting segfault in some situations with combination of auto_detach=True and libftdi 0.x, so auto_detach is now only supported in libftdi 1.0+ and ignored otherwise. 0.16.1 ~~~~~~ * fixed - segfault opening `Device` instances when `auto_detach` used and running on early (pre 1.0) versions of `libftdi`. 0.16.0 ~~~~~~ * new/changed - Automatic driver detach. By default, calls `libusb_set_auto_detach_kernel_driver()` on newly opened devices, to restore kernel drivers after close. Note this only seems to have an effect on Linux, as far as I can tell. Disable with `Device(..., auto_detach=False)` * new - serial_transfer example which checks that pseudo-random data send over a serial connection is correctly transferred. * new - `modem_status` property on SerialDevice() object, 16-bit value where each bit represents a different value such as 'Overrun Error'. See docstring or documentation for ftdi_poll_modem_status function in libftdi. * fixed - reset FTDI latency timer to default (16ms) on device instantiation * changed - Python2.6 is no longer supported. * changed - now uses incremental codecs when doing text-based transfer. 0.15 ~~~~ * new - additional `pylibftdi.Driver` methods: `libusb_version` to report on the version of the libusb library * new - `info` example, which displays additional info about the enviroment pylibftdi is running in; run with `python -m pylibftdi.examples.info` or similar. * changed - `libftdi_search` argument to `Driver()` can now give a path name referencing the libftdi library. * changed - libftdi is now opened on first use, not immediately when creating a `Driver` instance. * changed - missing libraries now raise `pylibftdi.LibraryMissingError`. This subclasses `FtdiError`, so should remain compatible with existing usage, but allows this to be caught explicitly. * fixed - finding the libftdi library on Windows has been improved/fixed; find_library does not work consistently across Windows / Python versions... * More test cases covering bitbang usage 0.14.2 ~~~~~~ * new - optional device_index parameter to `Device()` constructor, select devices by index rather than (just) serial number / description as per device_id 0.14.1 ~~~~~~ * install - only install the ftdi_osx_driver_* scripts if we're installing to an OS X machine 0.14 ~~~~ * refactor - split the `Device` class from the driver.py module into its own 'device.py' module * refactor - fix tests so they work both standalone and when run via e.g. python -m unittest discover * change - now resets the flow control and baudrate during device initialisation * new - SerialDevice class - supports properties for various serial lines (CTS, DTS, RI as inputs; DTR, DSR as outputs) * new - ftdi_osx_driver_{re,un}load scripts to disable / re-enable the built-in drivers supplied on Mac OS X * new - midi_output example - using pylibftdi to output MIDI data 0.13 ~~~~ * When finding and opening a device, don't just present the last error (which may well be a 'device not found (-3)' error, but more specific ones. Avoids issues where errors opening a device were being masked as pylibftdi would continue looking for other devices to open, and report on failure to find any of them. * Don't error out on failure during string retrieval part of device enumeration (list_devices) on failure to read product manufacturer, description or serial number (bitbucket issue #10) * Fix Driver.libftdi_version() for older library versions (which don't report version information...) * USB_PID_LIST now defaults to 0x6001, 0x6010, 0x6011, 0x6014. The new entries are 0x6010 and 0x6011. Note that USB_PID has been removed, and USB_VID has been replaced with FTDI_VENDOR_ID * Updated documentation, specifically regarding use on OS X Mavericks * Added serial_loopback example test, for investigating ongoing issues with UB232H module serial loopback losses... 0.12 ~~~~ * Optimisation on accessing port property when direction is set to ALL_OUTPUTS - don't bother reading the pins first. Makes ``port`` identical to ``latch`` in this case. * Support for libftdi 1.x, used by preference over ftdi 0.x. The specific driver search can be given as a string (or tuple of strings) to the ``Driver`` constructor, e.g. the default is equivalent to:: >>> d = Driver() >>> # equivalent to... >>> d = Driver(('ftdi1', 'libftdi1', 'ftdi', 'libftdi')) >>> # and if we wanted to use ftdi 0.x on Linux: >>> d = Driver('ftdi') The driver in use can be determined by ``Driver.libftdi_version()`` method:: >>> from pylibftdi import Driver >>> Driver().libftdi_version() (1, 0, 0, '1.0', 'v1.0-6-gafb9082') >>> Driver('ftdi').libftdi_version() (0, 99, 0, '0.99', 'v0.17-305-g50d77f8') * Support for MS Windows (which requires the 'lib' prefix on ftdi searching - previously absent). * Minor bug fixes 0.11 ~~~~ * API changes * Device() parameter 'interface' has been renamed to 'interface_select', as it wasn't clear what this meant. Symbolic constants 'INTERFACE_ANY', 'INTERFACE_A'/B/C/D are provided in the pylibftdi package namespace. * Device() parameter 'buffer_size' has been renamed to 'chunk_size' because there isn't any buffering going on, so it was potentially misleading. * new 'latch' property on BitBangDevices reflects current state of output latch, for better defined behaviour on read-modify-write operations on mixed-direction ports. * fixed the bit_server example; run as python -m pylibftdi.examples.bit_server * docstring clean-ups * BitBangDevice now reads device pin state on startup, so read/rmw operations across process runs now work as expected; for example previously running:: >>> from pylibftdi import BitBangDevice >>> d = BitBangDevice() >>> d.port = 1 >>> ^D # restart interpreter >>> from pylibftdi import BitBangDevice >>> d = BitBangDevice() >>> d.port |= 2 would have cleared D0, but now keeps it high (so d.port == 3) * setup.py will now use setuptools/distribute if installed, allowing the 'develop' target. 0.10.3 ~~~~~~ * lots more documentation, cleaned up, on readthedocs. Still in progress though. * Experimental support for multi-device interfaces ``Device(interface=1)`` 0.10.2 ~~~~~~ * include examples subpackage * ensure examples work under Python3 * 'python -m pylibftdi.examples.led_flash' should now work and pulse D0 0.10.1 ~~~~~~ * maintenance build to fix installation issues * USB_VID_LIST/USB_PID_LIST exposed directly as pylibftdi attributes 0.10 ~~~~ * support for FT232H via iteration over lists USB_VID_LIST/USB_PID_LIST in driver.py (FT232H uses different PID) * support for 'buffering' - i.e. chunking reads and writes to allow interruption (i.e. KeyboardInterrupt) in what could otherwise be a very long uninterruptable extension code call. * reset device to serial mode on open() (previously could be left in bitbang mode) * moved docs -> /doc * moved pylibftdi/tests -> /tests - makes python -m unittest discover etc work better * removed legacy support for Driver() to acting as a Device() * API for Bus class now requires .device rather than .driver (this still needs improving at some point) 0.9 ~~~ * improved and extended tests * made a start on some Sphinx-based documentation * fixed long-standing issue with input from a BitBangDevice * allow the PID/VID to be changed * add new examples * basic web server to toggle / read IO bits * magic_candle.py - example of using input and output together * pin_read.py - read and match values on device pins * All of this needs some tidying, but it fixes some key issues and has been unreleased too long. 0.8.1 ~~~~~ * fix issue with bitbang following API changes in 0.8 * add tests for bitbang mode * refactor tests; fix text-based tests in Python3 * slight refactor Device() to improve testability (_read and _write methods) 0.8 ~~~ * added some unit tests * API changes: * when opening a device with a device_id parameter, this will now check against both serial number and (if that fails), the device description. Opening by device type (under the same proviso that an arbitrary device will be selected if multiple matching devices are attached as when no device_id is given) is frequently easier than matching by serial number. * added flush(), flush_input() and flush_output() operations. This is modelled after the pySerial API providing separate flush operations, and gets the Device API closer to that of files. * increased file-API compatibility, with line-oriented methods and iteration 0.7 ~~~ * support multiple attached devices * API changes: * separation between driver and device. Generally, now use Device() / BitBangDevice() rather than Driver / BitBangDriver(), though older code _should_ still work via some shims (though with Deprecation warnings). * first parameter in Device() and BitBangDevice() is a device ID - the serial number string of the target device. * open() is generally no longer required on Devices. This is to more closely model the file() API. 0.6 ~~~ * same source now works on both Python2.6+ and Python3. 0.5 ~~~ * fix various bugs found by pyflakes * improve module organisation, while providing interface compatibility 0.4.1 ~~~~~ * fix release issue 0.4 ~~~ * fixed embarrassing bug which caused things not to work on Linux (is now find_library('ftdi') instead of find_library('libftdi')) * lots of error checking, new FtdiError exception. Before it just tended to segfault if things weren't just-so. * get_error() is now get_error_string(). It's still early enough to change the API, and if I thought it was get_error_string multiple times when I wrote the error checking code, it probably should be the more natural thing. 0.3 ~~~ * added some examples * new Bus class in pylibftdi (though it probably belongs somewhere else) * first release on PyPI 0.2 ~~~ * fixed various bugs * added ftdi_fn and set_baudrate functions in Driver class * changed interface in BitBangDriver to direction/port properties rather than overriding the read/write functions, which are therefore still available as in the Driver class. 0.1 ~~~ * first release. Tested with libftdi 0.18 on Mac OS X 10.6 and Linux (stock EEEPC 701 Xandros Linux, Ubuntu 10.04) pylibftdi-0.24.1/Dockerfile000066400000000000000000000007451516421155500155710ustar00rootroot00000000000000# Defines an ubuntu-based dev container for development and testing of pylibftdi # As well as providing uv etc, this pre-installs the required libftdi1-2 # package, providing libftdi 1.5 (as of Ubuntu 24.04) and all its dependencies. FROM ubuntu:24.04 WORKDIR /app RUN \ apt-get update && \ apt-get install -y libftdi1-2 python3-minimal curl vim && \ curl -LsSf https://astral.sh/uv/install.sh | sh # Make uv available on PATH ENV PATH="/root/.local/bin:$PATH" CMD ["bash"] pylibftdi-0.24.1/LICENSE.txt000066400000000000000000000021051516421155500154120ustar00rootroot00000000000000Copyright (c) 2010-2026 Ben Bass Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. https://opensource.org/license/mit pylibftdi-0.24.1/Makefile000066400000000000000000000022631516421155500152340ustar00rootroot00000000000000# Basic development functions for pylibftdi. # Example: # make container && make lint && make test && make build .PHONY: all all: container test lint build .PHONY: container container: docker build -t pylibftdi-dev:latest . .PHONY: build build: docker run --rm -t -v $$PWD:/app -w /app pylibftdi-dev:latest uv build .PHONY: test test: docker run --rm -t -v $$PWD:/app -w /app pylibftdi-dev:latest uv run pytest .PHONY: lint lint: docker run --rm -t -v $$PWD:/app -w /app pylibftdi-dev:latest bash -c 'uv run ruff format --check . && uv run ruff check src tests && uv run mypy src' .PHONY: format format: docker run --rm -t -v $$PWD:/app -w /app pylibftdi-dev:latest bash -c 'uv run ruff format . && uv run ruff check --fix src tests' .PHONY: shell shell: # Drop into a shell where e.g. `uv run python -m pylibftdi.examples.list_devices` etc can be run docker run --rm -it -v $$PWD:/app -w /app pylibftdi-dev:latest bash .PHONY: docs docs: uv run --group docs sphinx-build -b html docs docout/html uv run python -m webbrowser "file://$(abspath docout/html/index.html)" .PHONY: clean clean: # Ideally this would remove all relevant dev containers too... rm -rf dist/ .venv/ docout/ pylibftdi-0.24.1/README.rst000066400000000000000000000066641516421155500152740ustar00rootroot00000000000000pylibftdi ========= pylibftdi is a minimal Pythonic interface to FTDI devices using libftdi_. .. _libftdi: https://www.intra2net.com/en/developer/libftdi/ :Features: - No dependencies beyond standard library and a `libftdi` install. - Supports parallel and serial devices - Support for multiple devices - File-like interface wherever appropriate - Cross-platform Usage ----- The primary interface is the ``Device`` class in the pylibftdi package; this gives serial access on relevant FTDI devices (e.g. the UM232R), providing a file-like interface (read, write). Baudrate is controlled with the ``baudrate`` property. If a Device instance is created with ``mode='t'`` (text mode) then read() and write() can use the given ``encoding`` (defaulting to latin-1). This allows easier integration with passing unicode strings between devices. Multiple devices are supported by passing the desired device serial number (as a string) in the ``device_id`` parameter - this is the first parameter in both Device() and BitBangDevice() constructors. Alternatively the device 'description' can be given, and an attempt will be made to match this if matching by serial number fails. Examples ~~~~~~~~ :: >>> from pylibftdi import Device >>> >>> with Device(mode='t') as dev: ... dev.baudrate = 115200 ... dev.write('Hello World') The pylibftdi.BitBangDevice wrapper provides access to the parallel IO mode of operation through the ``port`` and ``direction`` properties. These provide an 8 bit IO port including all the relevant bit operations to make things simple. :: >>> from pylibftdi import BitBangDevice >>> >>> with BitBangDevice('FTE00P4L') as bb: ... bb.direction = 0x0F # four LSB are output(1), four MSB are input(0) ... bb.port |= 2 # set bit 1 ... bb.port &= 0xFE # clear bit 0 There is support for a number of external devices and protocols, including interfacing with HD44780 LCDs using the 4-bit interface. History & Motivation -------------------- This package is the result of various bits of work using FTDI's devices, primarily for controlling external devices. Some of this is documented on the codedstructure blog, codedstructure.blogspot.com Several other open-source Python FTDI wrappers exist, and each may be best for some projects. Some aim at closely wrapping the libftdi interface, others use FTDI's own D2XX driver (ftd2xx_) or talk directly to USB via libusb or similar (such as pyftdi_). .. _ftd2xx: https://pypi.org/project/ftd2xx/ .. _pyftdi: https://github.com/eblot/pyftdi The aim for pylibftdi is to work with libftdi, but to provide a high-level Pythonic interface. Various wrappers and utility functions are also part of the distribution; following Python's batteries included approach, there are various interesting devices supported out-of-the-box - or at least there will be soon! Plans ----- * Add more examples: SPI devices, knight-rider effects, input devices, MIDI... * Perhaps add support for D2XX driver, though the name then becomes a slight liability ;) License ------- Copyright (c) 2010-2026 Ben Bass pylibftdi is released under the MIT licence; see the file "LICENSE.txt" for information. All trademarks referenced herein are property of their respective holders. libFTDI itself is developed by Intra2net AG. No association with Intra2net is claimed or implied, but I have found their library helpful and had fun with it... pylibftdi-0.24.1/docs/000077500000000000000000000000001516421155500145215ustar00rootroot00000000000000pylibftdi-0.24.1/docs/Makefile000066400000000000000000000127101516421155500161620ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pylibftdi.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pylibftdi.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/pylibftdi" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pylibftdi" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." pylibftdi-0.24.1/docs/advanced_usage.rst000066400000000000000000000077471516421155500202230ustar00rootroot00000000000000Advanced Usage ============== ``libftdi`` function access --------------------------- Three attributes of ``Device`` instances are documented which allow direct access to the underlying ``libftdi`` functionality. #. ``fdll`` - this is a reference to the loaded ``libftdi`` library, loaded via ctypes. This should be used with the normal ctypes protocols. #. ``ctx`` - this is a reference to the context of the current device context. It is managed as a raw ctypes byte-string, so can be modified if required at the byte-level using appropriate ``ctypes`` methods. #. ``ftdi_fn`` - a convenience function wrapper, this is the preferred method for accessing library functions for a specific device instance. This is a function forwarder to the local ``fdll`` attribute, but also wraps the device context and passes it as the first argument. In this way, using ``device.ftdi_fn.ft_xyz`` is more like the D2XX driver provided by FTDI, in which the device context is passed in at initialisation time and then the client no longer needs to care about it. A call to:: >>> device.ftdi_fn.ft_xyz(1, 2, 3) is equivalent to the following:: >>> device.fdll.ft_xyz(ctypes.byref(device.ctx), 1, 2, 3) but has the advantages of being shorter and not requiring ctypes to be in scope. incorrect operations using any of these attributes of devices are liable to crash the Python interpreter Examples ~~~~~~~~ The following example shows opening a device in serial mode, switching temporarily to bit-bang mode, then back to serial and writing a string. Why this would be wanted is anyone's guess ;-) :: >>> from pylibftdi import Device >>> >>> with Device() as dev: >>> dev.ftdi_fn.ftdi_set_bitmode(1, 0x01) >>> dev.write('\x00\x01\x00') >>> dev.ftdi_fn.ftdi_set_bitmode(0, 0x00) >>> dev.write('Hello World!!!') The libftdi_ documentation should be consulted in conjunction with the ctypes_ reference for guidance on using these features. .. _libftdi: https://www.intra2net.com/en/developer/libftdi/documentation/ .. _ctypes: https://docs.python.org/3/library/ctypes.html Selecting the underlying libftdi library ---------------------------------------- Since pylibftdi 0.12, the Driver exposes ``libftdi_version()`` and ``libusb_version()`` methods, which return a tuple whose first three entries correspond to major, minor, and micro versions of the libftdi driver being used. Note there are two major versions of `libftdi` - libftdi1 can coexist with the earlier 0.x versions - it is now possible to select which library to load when instantiating the Driver. Note on at least Ubuntu Linux, the `libftdi1` *OS package* actually refers to `libftdi 0.20` (or similar), whereas `libftdi1-2` refers to the more recent 1.x release (currently 1.5):: Python 3.10.6 (main, May 29 2023, 11:10:38) [GCC 11.3.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>> from pylibftdi import Driver >>> Driver().libftdi_version() libftdi_version(major=1, minor=5, micro=0, version_str='1.5', snapshot_str='unknown') >>> Driver("ftdi1").libftdi_version() libftdi_version(major=1, minor=5, micro=0, version_str='1.5', snapshot_str='unknown') >>> Driver("ftdi").libftdi_version() libftdi_version(major=0, minor=0, micro=0, version_str='< 1.0 - no ftdi_get_library_version()', snapshot_str='unknown') If both are installed, ``pylibftdi`` prefers libftdi1 (e.g. libftdi 1.5) over libftdi (e.g. 0.20). Since different OSs require different parameters to be given to find a library, the default search list given to ctypes.util.find_library is defined by the `Driver._lib_search` attribute, and this may be updated as appropriate. By default it is as follows:: _lib_search = { "libftdi": ["ftdi1", "libftdi1", "ftdi", "libftdi"], "libusb": ["usb-1.0", "libusb-1.0"], } This covers Windows (which requires the 'lib' prefix), Linux (which requires its absence), and Mac OS X, which is happy with either. pylibftdi-0.24.1/docs/basic_usage.rst000066400000000000000000000044031516421155500175210ustar00rootroot00000000000000Basic Usage =========== `pylibftdi` is a minimal Pythonic interface to FTDI devices using libftdi_. Rather than simply expose all the methods of the underlying library directly, it aims to provide a simpler API for the main use-cases of serial and parallel IO, while still allowing the use of the more advanced functions of the library. .. _libftdi: https://www.intra2net.com/en/developer/libftdi/ General ------- The primary interface is the ``Device`` class in the ``pylibftdi`` package; this gives serial access on relevant FTDI devices (e.g. the UM232R), providing a file-like interface (read, write). Baudrate is controlled with the ``baudrate`` property. If a Device instance is created with ``mode='t'`` (text mode) then read() and write() can use the given ``encoding`` (defaulting to latin-1). This allows easier integration with passing unicode strings between devices. Multiple devices are supported by passing the desired device serial number (as a string) in the ``device_id`` parameter - this is the first parameter in both Device() and BitBangDevice() constructors. Alternatively the device 'description' can be given, and an attempt will be made to match this if matching by serial number fails. In the event that multiple devices (perhaps of identical type) have the same description and serial number, the ``device_index`` parameter may be given to open matching devices by numerical index; this defaults to zero, meaning the first matching device. Examples ~~~~~~~~ :: >>> from pylibftdi import Device >>> >>> with Device(mode='t') as dev: ... dev.baudrate = 115200 ... dev.write('Hello World') The pylibftdi.BitBangDevice wrapper provides access to the parallel IO mode of operation through the ``port`` and ``direction`` properties. These provide an 8 bit IO port including all the relevant bit operations to make things simple. :: >>> from pylibftdi import BitBangDevice >>> >>> with BitBangDevice('FTE00P4L') as bb: ... bb.direction = 0x0F # four LSB are output(1), four MSB are input(0) ... bb.port |= 2 # set bit 1 ... bb.port &= 0xFE # clear bit 0 There is support for a number of external devices and protocols, specifically for interfacing with HD44780 LCDs using the 4-bit interface. pylibftdi-0.24.1/docs/bitbang.rst000066400000000000000000000052621516421155500166660ustar00rootroot00000000000000Bit-bang mode ============= Bit-bang mode allows the programmer direct access (both read and write) to the state of the IO lines from a compatible FTDI device. The interface provided by FTDI is intended to mirror the type of usage on a microcontroller, and is similar to the 'user port' on many old 8-bit computers such as the BBC Micro and Commodore 64. The basic model is to have two 8 bit ports - one for data, and one for 'direction'. The data port maps each of the 8 bits to 8 independent IO signals, each of which can be configured separately as an 'input' or an 'output'. In pylibftdi, the data port is given by the ``port`` attribute of a BitBangDevice instance, and the direction control is provided by the ``direction`` attribute. Both these attributes are implemented as Python properties, so no method calls are needed on them - simple read and write in Python-land converts to read and write in the physical world seen by the FTDI device. The direction register maps to where each bit maps to a separate digital signal, Read-Modify-Write ----------------- Port vs Latch Via the augmented assignment operations, pylibftdi ``BitBangDevice`` instances support read-modify-write operations, such as arithmetic (``+=`` etc), bitwise (``&=``), and other logical operations such as shift (``<<=``) Examples ~~~~~~~~ :: >>> from pylibftdi import BitBangDevice >>> >>> with BitBangDevice('FTE00P4L') as bb: ... bb.direction = 0x0F # four LSB are output(1), four MSB are input(0) ... bb.port |= 2 # set bit 1 ... bb.port &= 0xFE # clear bit 0 >>> with BitBangDevice() as bb: ... bb.port = 1 ... while True: ... # Rotate the value in bb.port ... bb.port = ((bb.port << 1) | ((bb.port >> 8) & 1)) & 0xFF ... time.sleep(1) The `Bus` class --------------- Dealing with bit masks and shifts gets messy quickly. Some languages such as C and C++ provide direct support for accessing bits - or series of consecutive bits - with bitfields. The ``Bus`` class provides the facility to provide a similar level of support to pylibftdi ``BitBangDevice`` classes. As an example, consider an HD44780 LCD display. These have a data channel of either 4 or 8 bits, and a number of additional status lines - ``rs`` which acts as a register select pin - indicating whether a data byte is a command (0) or data (1), and ``e`` - clock enable.:: class LCD(object): """ The UM232R/245R is wired to the LCD as follows: DB0..3 to LCD D4..D7 (pin 11..pin 14) DB6 to LCD 'RS' (pin 4) DB7 to LCD 'E' (pin 6) """ data = Bus(0, 4) rs = Bus(6) e = Bus(7) pylibftdi-0.24.1/docs/conf.py000066400000000000000000000017561516421155500160310ustar00rootroot00000000000000import os import sys from importlib.metadata import PackageNotFoundError, version as _version sys.path.insert(0, os.path.abspath("../src")) project = "pylibftdi" copyright = "2010-2026, Ben Bass" try: release = _version("pylibftdi") except PackageNotFoundError: # Package not installed; install it first (e.g. `pip install -e .`) # Once Python 3.11+ is required, tomllib could read pyproject.toml directly. import warnings warnings.warn( "pylibftdi package not found; install it before building docs. " "Version will be reported as 'unknown'.", stacklevel=1, ) release = "unknown" version = ".".join(release.split(".")[:2]) extensions = [ "sphinx.ext.autodoc", "sphinx.ext.viewcode", "sphinx_rtd_theme", ] autodoc_member_order = "groupwise" autodoc_default_options = { "special-members": "__init__", } source_suffix = ".rst" root_doc = "index" exclude_patterns = ["_build"] pygments_style = "sphinx" html_theme = "sphinx_rtd_theme" pylibftdi-0.24.1/docs/developing.rst000066400000000000000000000101661516421155500174130ustar00rootroot00000000000000Developing pylibftdi -------------------- How do I checkout and use the latest development version? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. note:: `pylibftdi` is currently developed on GitHub, though started out as a Mercurial repository on bitbucket.org. There may still be references to old bitbucket issues in the docs. `pylibftdi` is developed using uv_, and a Dockerfile plus Makefile make development tasks straightforward. In any case, start with a local clone of the repository:: $ git clone https://github.com/codedstructure/pylibftdi $ cd pylibftdi .. _uv: https://docs.astral.sh/uv/ Note that `pylibftdi` itself only requires the Python standard library for normal use. The development dependencies (``pytest``, ``ruff``, ``mypy``) are declared in the ``[dependency-groups]`` section of ``pyproject.toml`` and are only needed for development and testing. There are then two main approaches, though pick and mix the different elements to suit: **uv and docker** If `make` and `docker` are available in your environment, the easiest way to do development may be to simply run `make shell`. This creates an Ubuntu-based docker environment with `libftdi`, `uv`, and other requirements pre-installed, and drops into a shell where the current `pylibftdi` code is available. `make` on its own will run through all the unittests and linting available for `pylibftdi`, and is a useful check to make sure things haven't been broken. The downside of running in a docker container is that USB support to actual FTDI devices may be lacking... **local install with uv** With uv_ installed, run ``uv sync`` from the project root. This creates a ``.venv`` virtual environment, installs the development dependencies from ``uv.lock``, and installs ``pylibftdi`` itself in editable mode — no separate activation step is needed to run tools:: .../pylibftdi$ uv sync .../pylibftdi$ uv run pytest .../pylibftdi$ uv run ruff check src tests If you prefer to work inside an activated virtual environment:: .../pylibftdi$ uv sync .../pylibftdi$ source .venv/bin/activate (pylibftdi) .../pylibftdi$ pytest .. note:: The ``uv.lock`` file pins all dependency versions for reproducible installs. To upgrade all dependencies to their latest allowed versions, run ``uv lock --upgrade``, or target a single package with ``uv lock --upgrade-package pytest``. Follow either with ``uv sync`` to apply the changes to the virtual environment. How do I run the tests? ~~~~~~~~~~~~~~~~~~~~~~~ From the root directory of a cloned pylibftdi repository, run:: .../pylibftdi$ uv run pytest Or, with a virtualenv already activated:: (pylibftdi) .../pylibftdi$ pytest The standard ``unittest`` runner also works if preferred:: (pylibftdi) .../pylibftdi$ python3 -m unittest discover How do I make a PyPI release? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Ensure the working tree is clean, all tests pass, and ``CHANGES.txt`` is up to date. Update the version number in ``pyproject.toml``, sync uv, then tag the commit:: .../pylibftdi$ vim pyproject.toml CHANGES.txt # update with new version .../pylibftdi$ uv sync --upgrade .../pylibftdi$ make # ensure everything works .../pylibftdi$ git add pyproject.toml uv.lock CHANGES.txt .../pylibftdi$ git commit -m "Release 0.x.0" .../pylibftdi$ git tag 0.x.0 Build the distribution artifacts using ``uv build`` (or ``make build``, which runs the same command inside the docker container):: .../pylibftdi$ uv build This produces a source distribution and a wheel under ``dist/``. Publish to PyPI using ``uv publish``. A PyPI API token is required; either export it as an environment variable or pass it directly:: .../pylibftdi$ uv publish --token pypi-... # or via environment variable .../pylibftdi$ UV_PUBLISH_TOKEN=pypi-... uv publish .. note:: To do a dry run first, publish to TestPyPI:: .../pylibftdi$ uv publish --publish-url https://test.pypi.org/legacy/ --token pypi-... You can verify the result at https://test.pypi.org/project/pylibftdi/ Finally, push the tag:: .../pylibftdi$ git push origin main --tags pylibftdi-0.24.1/docs/how_to.rst000066400000000000000000000247521516421155500165640ustar00rootroot00000000000000pylibftdi questions =================== None of these are yet frequently asked, and perhaps they never will be... But they are still questions, and they relate to pylibftdi. Using pylibftdi - General ------------------------- Can I use pylibftdi with device XYZ? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If the device XYZ is (or uses as it's ) an FTDI device, then possibly. A large number of devices *will* work, but won't be recognised due to the limited USB Vendor and Product IDs which pylibftdi checks for. To see the vendor / product IDs which are supported, run the following:: >>> from pylibftdi import USB_VID_LIST, USB_PID_LIST >>> print(', '.join(hex(pid) for pid in USB_VID_LIST)) 0x403 >>> print(', '.join(hex(pid) for pid in USB_PID_LIST)) 0x6001, 0x6010, 0x6011, 0x6014, 0x6015 If a FTDI device with a VID / PID not matching the above is required, then the device's vendor ID and product ID can be specified with `vid` or `pid` keyword arguments passed to Device constructors. For example, to open a device with VID 0x1234 and PID 0x5678, use:: >>> from pylibftdi import Device >>> dev = Device(vid=0x1234, pid=0x5678) Alternatively, the device's vendor or product ID can be appended to the appropriate list after import:: >>> from pylibftdi import USB_PID_LIST, USB_VID_LIST, Device >>> USB_PID_LIST.append(0x1234) >>> >>> dev = Device() # will now recognise a device with PID 0x1234. Which devices are recommended? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ While I used to do a lot of soldering, I prefer the cleaner way of breadboarding nowadays. As such I can strongly recommend the FTDI DIP modules which plug into a breadboard nice and easy, can be self-powered from USB, and can be re-used for dozens of different projects. I've used (and test against) the following, all of which have 0.1" pin spacing in two rows 0.5" or 0.6" apart, so will sit across the central divide of any breadboard: UB232R a small 8 pin device with mini-USB port; serial and CBUS bit-bang. UM245R a 24-pin device with parallel FIFO modes. Full-size USB type B socket. UM232R a 24-pin device with serial and bit-bang modes. Full-size USB type B socket. UM232H this contains a more modern FT232H device, and libftdi support is fairly recent (requires 0.20 or later). Supports USB 2.0 Hi-Speed mode though, and lots of interesting modes (I2C, SPI, JTAG...) which I've not looked at yet. Mini-USB socket. Personally I'd go with the UM232R device for compatibility. It works great with both UART and bit-bang IO, which I target as the two main use-cases for pylibftdi. The UM232H is certainly feature-packed though, and I hope to support some of the more interesting modes in future. Using pylibftdi - Programming ----------------------------- How do I set the baudrate? ~~~~~~~~~~~~~~~~~~~~~~~~~~ In both serial and parallel mode, the internal baudrate generator (BRG) is set using the ``baudrate`` property of the ``Device`` instance. Reading this will show the current baudrate (which defaults to 9600); writing to it will attempt to set the BRG to that value. On failure to set the baudrate, it will remain at its previous setting. In parallel mode, the actual bytes-per-second rate of parallel data is 16x the programmed BRG value. This is an effect of the FTDI devices themselves, and is not hidden by pylibftdi. How do I send unicode over a serial connection? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If a ``Device`` instance is created with ``mode='t'``, then text-mode is activated. This is analogous to opening files; after all, the API is intentionally modelled on file objects whereever possible. When text-mode is used, an encoding can be specified. The default is ``latin-1`` for the very practical reason that it is transparent to 8-bit binary data; by default a text-mode serial connection looks just like a binary mode one. An alternative encoding can be used provided in the same constructor call used to instantiate the ``Device`` class, e.g.:: >>> dev = Device(mode='t', encoding='utf-8') Read and write operations will then return / take unicode values. Whether it is sensible to try and send unicode over a ftdi connection is a separate issue... At least consider doing codec operations at a higher level in your application. How do I use multiple-interface devices? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Some FTDI devices have multiple interfaces, for example the FT2232H has 2 and the FT4232H has four. In terms of accessing them, they can be considered as independent devices; once a connection is established to one of them, it is isolated from the other interfaces. To select which interface to use when opening a connection to a specific interface on a multiple-interface device, use the ``interface_select`` parameter of the Device (or BitBangDevice) class constructor. The value should be one of the following values. Symbolic constants are provided in the pylibftdi namespace. ==================== ============= ``interface_select`` Meaning -------------------- ------------- INTERFACE_ANY (0) Any interface INTERFACE_A (1) INTERFACE A INTERFACE_B (2) INTERFACE B INTERFACE_C (3) INTERFACE C INTERFACE_D (4) INTERFACE D ==================== ============= You should be able to open multiple ``Device``\s with different ``interface_select`` settings. *Thanks to Daniel Forer for testing multiple device support.* What is the difference between the ``port`` and ``latch`` BitBangDevice properties? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ `latch` reflects the current state of the output latch (i.e. the last value written to the port), while ``port`` reflects input states as well. Writing to either ``port`` or ``latch`` has an identical effect, so when pylibftdi is used only for output, there is no effective difference, and ``port`` is recommended for simplicity and consistency. The place where it does make a difference is during read-modify-write operations. Consider the following:: >>> dev = BitBangDevice() # 1 >>> dev.direction = 0x81 # 2 # set bits 0 and 7 are output >>> dev.port = 0 # 3 >>> for _ in range(255): # 4 >>> dev.port += 1 # 5 # read-modify-write operation In this (admittedly contrived!) scenario, if one of the input lines D1..D6 were held low, then they would cause the counter to effectively 'stop'. The ``+= 1`` operation would never actually set the bit as required (because it is an input at 0), and the highest output bit would never get set. Using ``dev.latch`` in lines 3 and 5 above would resolve this, as the read-modify-write operation on line 5 is simply working on the in-memory latch value, rather than reading the inputs, and it would simply count up from 0 to 255 in steps of one, writing the value to the device (which would be ignored in the case of input lines). Similar concepts exist in many microcontrollers, for example see https://stackoverflow.com/a/2623498 for a possibly better explanation, though in a slightly different context :) If you aren't using read-modify-write operations (e.g. augmented assignment), or you have a direction on the port of either ALL_INPUTS (0) or ALL_OUTPUTS (1), then just ignore this section and use ``port`` :) What is the purpose of the ``chunk_size`` parameter? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ While libftdi is performing I/O to the device, it is not really running Python code at all, but C library code via ctypes. If there is a significant amount of data, especially at low baud-rates, this can be a significant delay during which no Python bytecode is executed. The most obvious result of this is that no signals are delivered to the Python process during this time, and interrupt signals (Ctrl-C) will be ignored. Try the following:: >>> dev = Device() >>> dev.baudrate = 120 # nice and slow! >>> dev.write('helloworld' * 1000) This should take approximately 10 seconds prior to returning, and crucially, Ctrl-C interruptions will be deferred for all that time. By setting ``chunk_size`` on the device (which may be set either as a keyword parameter during ``Device`` instantiation, or at a later point as an attribute of the ``Device`` instance), the I/O operations are performed in chunks of at most the specified number of bytes. Setting it to 0, the default value, disables this chunking. Repeat the above command but prior to the write operation, set ``dev.chunk_size = 10``. A Ctrl-C interruption should now kick-in almost instantly. There is a performance trade-off however; if using ``chunk_size`` is required, set it as high as is reasonable for your application. Using pylibftdi - Interfacing ----------------------------- How do I control an LED? ~~~~~~~~~~~~~~~~~~~~~~~~ pylibftdi devices generally have sufficient output current to sink or source the 10mA or so which a low(ish) current LED will need. A series resistor is essential to protect both the LED and the FTDI device itself; a value between 220 and 470 ohms should be sufficient depending on required brightness / LED efficiency. How do I control a higher current device? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ FTDI devices will typically provide a few tens of milli-amps, but beyond that things either just won't work, or the device could be damaged. For medium current operation, a standard bipolar transistor switch will suffice; for larger loads a MOSFET or relay should be used. (Note a relay will require a low-power transistor switch anyway). Search online for something like 'mosfet logic switch' or 'transistor relay switch' for more details. What is the state of an unconnected input pin? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This depends on the device and the EEPROM configuration values. Most devices will have weak (typ. 200Kohm) pull-ups on input pins, so there is no harm leaving them floating. Consult the datasheet for your device for definitive information, but you can always just leave an (unconnected) device and read it's pins when set as inputs; chances are they will read 255 / 0xFF:: >>> dev = BitBangDevice(direction=0) >>> dev.port 255 While not recommended for anything serious, this does allow the possibility of reading a input switch state by simply connecting a switch between an input pin and ground (possibly with a low value - e.g. 100 ohm - series resistor to prevent accidents should it be set to an output and set high...). Note that with a normal push-to-make switch, the value will read '1' when the switch is not pressed; pressing it will set the input line value to '0'. pylibftdi-0.24.1/docs/index.rst000066400000000000000000000032471516421155500163700ustar00rootroot00000000000000Welcome to pylibftdi's documentation! ===================================== pylibftdi is a simple library interacting with FTDI devices to provide serial and parallel IO from Python. Examples:: >>> from pylibftdi import BitBangDevice >>> with BitBangDevice('FT0123') as dev: ... dev.port |= 1 :: >>> # Send a MIDI 'note on' message >>> from pylibftdi import Device >>> with Device() as dev: ... dev.baudrate = 31250 ... dev.write('\x90\x64\x64') The two main use cases it serves are: * the need to control or monitor external equipment, for which a FTDI module may be a cheap and reliable starting point. * the need to interact with existing devices which are known to contain FTDI chipsets for their USB interface. FTDI (https://www.ftdichip.com) create devices (chipsets, modules, cables etc) to interface devices to the USB port of your computer. libftdi (https://www.intra2net.com/en/developer/libftdi/) is an open source driver to communicate with these devices, and runs on top of libusb. It works on Windows, Linux, and Mac OS X, and likely other systems too. pylibftdi is a pure Python module which interfaces (via ctypes) to libftdi, exposing a simple file-like API to connected devices. It supports serial and parallel IO in a straight-forward way, and aims to be one of the simplest ways of interacting with the world outside your PC. Contents ======== .. toctree:: :maxdepth: 2 introduction quickstart installation basic_usage bitbang serial advanced_usage how_to troubleshooting developing pylibftdi Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` pylibftdi-0.24.1/docs/installation.rst000066400000000000000000000132531516421155500177600ustar00rootroot00000000000000Installation ============ Unsurprisingly, ``pylibftdi`` depends on ``libftdi``, and installing this varies according to your operating system. Chances are that following one of the following instructions will install the required prerequisites. If not, be aware that libftdi in turn relies on ``libusb``. Installing ``pylibftdi`` itself is straightforward - it is a pure Python package (using ``ctypes`` for bindings), and has no dependencies outside the Python standard library for installation. Don't expect it to work happily without ``libftdi`` installed though :-) :: $ pip install pylibftdi Depending on your environment, you may want to set up a `virtual environment`_. .. _virtual environment: https://packaging.python.org/guides/installing-using-pip-and-virtual-environments/ Windows ------- I perform only limited testing of pylibftdi on Windows, but it should work correctly provided the requirements of libftdi and libusb are correctly installed. Recent libftdi binaries for Windows seem to be available from the picusb_ project on Sourceforge. Download libftdi1-1.5_devkit_x86_x64_19July2020.zip or later from that site, which includes the required drivers, or search for 'libftdi windows' for other options. .. _picusb: https://sourceforge.net/projects/picusb/files/ Mac OS X -------- I suggest using homebrew_ to install libftdi:: $ brew install libftdi .. _homebrew: https://brew.sh/ On macOS, Apple does not include a built-in FTDI driver (it was removed in macOS Monterey), but FTDI's own VCP driver may be installed by other software and will conflict with libftdi. See the Troubleshooting_ section for details. .. _Troubleshooting: troubleshooting.html#macos Linux ----- There are two steps in getting a sensible installation in Linux systems: 1. Getting ``libftdi`` and its dependencies installed 2. Ensuring permissions allow access to the device without requiring root privileges. Symptoms of this not being done are programs only working properly when run with ``sudo``, giving '-4' or '-8' error codes in other cases. Each of these steps will be slightly different depending on the distribution in use. Debian (Raspberry Pi) / Ubuntu etc ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ On Debian-based systems (including Ubuntu, Raspberry Pi OS, etc), the runtime library package provides what pylibftdi needs:: $ sudo apt-get install libftdi1-2 The following works to give normal users access to FTDI devices without needing root permissions: 1. Create a file ``/etc/udev/rules.d/99-libftdi.rules``. You will need sudo access to create this file. 2. Put the following in the file:: SUBSYSTEMS=="usb", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", GROUP="dialout", MODE="0660" SUBSYSTEMS=="usb", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6010", GROUP="dialout", MODE="0660" SUBSYSTEMS=="usb", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6011", GROUP="dialout", MODE="0660" SUBSYSTEMS=="usb", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6014", GROUP="dialout", MODE="0660" SUBSYSTEMS=="usb", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6015", GROUP="dialout", MODE="0660" The list of USB product IDs above matches the default used by ``pylibftdi``, but some FTDI devices may use other USB PIDs. You could try removing the match on ``idProduct`` altogether, just matching on the FTDI vendor ID as follows:: SUBSYSTEMS=="usb", ATTRS{idVendor}=="0403", GROUP="dialout", MODE="0660" Or use ``lsusb`` or similar to determine the exact values to use (or try checking ``dmesg`` output on device insertion / removal). ``udevadm monitor --environment`` is also helpful, but note that the environment 'keys' it gives are different to the attributes (filenames within /sys/devices/...) which the ATTRS will match. Perhaps ENV{} matches work just as well, though I've only tried matching on ATTRS. Note that changed udev rules files will be picked up automatically by the udev daemon, but will only be acted upon on device actions, so unplug/plug in the device to check whether you're latest rules iteration actually works :-) Also note that the udev rules above assume that your user is in the 'dialout' group - if not, add it to your user with the following, though note that this will not apply immediately, not a full reboot may be needed on some systems:: sudo usermod -aG dialout $USER See https://wiki.debian.org/udev for more on writing udev rules. Arch Linux ~~~~~~~~~~ The ``libftdi`` package (sensibly enough) provides the ``libftdi`` library:: $ sudo pacman -S libftdi Similar udev rules to those above for Debian should be included (again in ``/etc/udev/rules.d/99-libftdi.rules`` or similar), though the GROUP directive should be changed to set the group to 'users':: SUBSYSTEMS=="usb", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", GROUP="users", MODE="0660" SUBSYSTEMS=="usb", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6010", GROUP="users", MODE="0660" (etc...) Testing installation -------------------- Connect your device, and run the following (as a regular user):: $ python3 -m pylibftdi.examples.list_devices If all goes well, the program should report information about each connected device. If no information is printed, but it is when run with ``sudo``, a possibility is permissions problems - see the section under Linux above regarding ``udev`` rules. If the above works correctly, then try the following:: $ python3 -m pylibftdi.examples.led_flash Even without any LED connected, this should 'work' without any error - quit with Ctrl-C. Likely errors at this point are either permissions problems (e.g. udev rules not working), or not finding the device at all - although the earlier stage is likely to have failed if this were the case. pylibftdi-0.24.1/docs/introduction.rst000066400000000000000000000060431516421155500177770ustar00rootroot00000000000000Introduction ============ pylibftdi is a minimal Pythonic interface to FTDI devices using libftdi_. .. _libftdi: https://www.intra2net.com/en/developer/libftdi/ :Features: - No dependencies beyond standard library and a `libftdi` install. - Supports parallel and serial devices - Support for multiple devices - File-like interface wherever appropriate - Cross-platform Usage ----- The primary interface is the ``Device`` class in the pylibftdi package; this gives serial access on relevant FTDI devices (e.g. the UM232R), providing a file-like interface (read, write). Baudrate is controlled with the ``baudrate`` property. If a Device instance is created with ``mode='t'`` (text mode) then read() and write() can use the given ``encoding`` (defaulting to latin-1). This allows easier integration with passing unicode strings between devices. Multiple devices are supported by passing the desired device serial number (as a string) in the ``device_id`` parameter - this is the first parameter in both Device() and BitBangDevice() constructors. Alternatively the device 'description' can be given, and an attempt will be made to match this if matching by serial number fails. Examples ~~~~~~~~ :: >>> from pylibftdi import Device >>> >>> with Device(mode='t') as dev: ... dev.baudrate = 115200 ... dev.write('Hello World') The pylibftdi.BitBangDevice wrapper provides access to the parallel IO mode of operation through the ``port`` and ``direction`` properties. These provide an 8 bit IO port including all the relevant bit operations to make things simple. :: >>> from pylibftdi import BitBangDevice >>> >>> with BitBangDevice('FTE00P4L') as bb: ... bb.direction = 0x0F # four LSB are output(1), four MSB are input(0) ... bb.port |= 2 # set bit 1 ... bb.port &= 0xFE # clear bit 0 There is support for a number of external devices and protocols, including interfacing with HD44780 LCDs using the 4-bit interface. History & Motivation -------------------- This package is the result of various bits of work using FTDI's devices, primarily for controlling external devices. Some of this is documented on the codedstructure blog, codedstructure.blogspot.com Several other open-source Python FTDI wrappers exist, and each may be best for some projects. Some aim at closely wrapping the libftdi interface, others use FTDI's own D2XX driver (ftd2xx_) or talk directly to USB via libusb or similar (such as pyftdi_). .. _ftd2xx: https://pypi.org/project/ftd2xx/ .. _pyftdi: https://github.com/eblot/pyftdi The aim for pylibftdi is to work with libftdi, but to provide a high-level Pythonic interface. License ------- Copyright (c) 2010-2026 Ben Bass pylibftdi is released under the MIT licence; see the file "LICENSE.txt" for information. All trademarks referenced herein are property of their respective holders. libFTDI itself is developed by Intra2net AG. No association with Intra2net is claimed or implied, but I have found their library helpful and had fun with it... pylibftdi-0.24.1/docs/make.bat000066400000000000000000000117561516421155500161400ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\pylibftdi.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\pylibftdi.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) :end pylibftdi-0.24.1/docs/pylibftdi.examples.rst000066400000000000000000000030341516421155500210560ustar00rootroot00000000000000examples Package ================ :mod:`examples` Package ----------------------- .. automodule:: pylibftdi.examples :members: :undoc-members: :show-inheritance: :mod:`bit_server` Module ------------------------ .. automodule:: pylibftdi.examples.bit_server :members: :undoc-members: :show-inheritance: :mod:`lcd` Module ----------------- .. automodule:: pylibftdi.examples.lcd :members: :undoc-members: :show-inheritance: :mod:`led_flash` Module ----------------------- .. automodule:: pylibftdi.examples.led_flash :members: :undoc-members: :show-inheritance: :mod:`list_devices` Module -------------------------- .. automodule:: pylibftdi.examples.list_devices :members: :undoc-members: :show-inheritance: :mod:`magic_candle` Module -------------------------- .. automodule:: pylibftdi.examples.magic_candle :members: :undoc-members: :show-inheritance: :mod:`midi_output` Module ------------------------- .. automodule:: pylibftdi.examples.midi_output :members: :undoc-members: :show-inheritance: :mod:`pin_read` Module ---------------------- .. automodule:: pylibftdi.examples.pin_read :members: :undoc-members: :show-inheritance: :mod:`serial_loopback` Module ----------------------------- .. automodule:: pylibftdi.examples.serial_loopback :members: :undoc-members: :show-inheritance: :mod:`info` Module ----------------------------- .. automodule:: pylibftdi.examples.info :members: :undoc-members: :show-inheritance: pylibftdi-0.24.1/docs/pylibftdi.rst000066400000000000000000000020501516421155500172360ustar00rootroot00000000000000pylibftdi Package ================= :mod:`pylibftdi` Package ------------------------ .. automodule:: pylibftdi.__init__ :members: :undoc-members: :show-inheritance: :mod:`_base` Module ------------------- .. automodule:: pylibftdi._base :members: :undoc-members: :show-inheritance: :mod:`device` Module -------------------- .. automodule:: pylibftdi.device :members: :undoc-members: :show-inheritance: :mod:`driver` Module -------------------- .. automodule:: pylibftdi.driver :members: :undoc-members: :show-inheritance: :mod:`bitbang` Module --------------------- .. automodule:: pylibftdi.bitbang :members: :undoc-members: :show-inheritance: :mod:`serial_device` Module --------------------------- .. automodule:: pylibftdi.serial_device :members: :undoc-members: :show-inheritance: :mod:`util` Module ------------------ .. automodule:: pylibftdi.util :members: :undoc-members: :show-inheritance: Subpackages ----------- .. toctree:: pylibftdi.examples pylibftdi-0.24.1/docs/quickstart.rst000066400000000000000000000037611516421155500174540ustar00rootroot00000000000000Quick Start =========== Install pylibftdi ----------------- See the installation_ instructions for more detailed requirements, but hopefully things will work by just running the following:: $ python3 -m pip install pylibftdi .. _installation: installation.html Connect and enumerate FTDI devices ---------------------------------- Connect the FTDI device to a free USB port. Run the ``list_devices`` example to enumerate connected FTDI devices:: $ python3 -m pylibftdi.examples.list_devices For each connected device, this will show manufacturer, model identifier, and serial number. With a single device connected, the output maybe something like the following: ``FTDI:UM232H:FTUBIOWF`` Though hopefully with a different serial number, or else you've either stolen mine, or you are me... Test some actual IO ------------------- Output example ~~~~~~~~~~~~~~ Connect an LED between D0 of your bit-bang capable device and ground, via a 330 - 1K ohm resistor as appropriate. Test the installation and functioning of pylibftdi with the following:: $ python3 -m pylibftdi.examples.led_flash The LED should now flash at approximately 1Hz. Input example ~~~~~~~~~~~~~ To test some input, remove any connections from the port lines initially, then run the following, which reads and prints the status of the input lines regularly:: $ python3 -m pylibftdi.examples.pin_read The ``pin_read`` example is a complete command line application which can be used to monitor for particular values on the attached device pins, and output an appropriate error code on match. Repeat the above with a trailing ``--help`` for info. Using ``pylibftdi`` from the REPL ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Since ``pylibftdi`` v0.18.0, a ``__main__.py`` module is included which imports all the exported constants, classes and functions from ``pylibftdi``. This allows quick interaction with FTDI devices from the Python REPL:: $ python3 -im pylibftdi >>> d = Device() >>> d.write('Hello World') >>> pylibftdi-0.24.1/docs/requirements.txt000066400000000000000000000000301516421155500177760ustar00rootroot00000000000000sphinx sphinx_rtd_theme pylibftdi-0.24.1/docs/serial.rst000066400000000000000000000072461516421155500165430ustar00rootroot00000000000000Serial mode =========== The default mode of pylibftdi devices is to behave as a serial UART device, similar to the 'COM1' device found on older PCs. Nowadays most PCs operate with serial devices over USB-serial adapters, which may often include their own FTDI chips. To remain compatible with the RS232 standard however, these adapters will often include level-shifting circuitry which is of no benefit in communicating with other circuits operating at the 3.3 or 5 volt levels the FTDI hardware uses. The default serial configuration is 9600 baud, 8 data bits, 1 stop bit and no parity (sometimes referred to as 8-N-1_). This is the default configuration of the old 'COM' devices back to the days of the original IBM PC and MS-DOS. .. _8-N-1: https://en.wikipedia.org/wiki/8-N-1 Setting line parameters ----------------------- Changing line parameters other than the baudrate is supported via use of the underlying FTDI function calls. The SerialDevice class ---------------------- While the standard ``Device`` class supports standard ``read`` and ``write`` methods, as well as a ``baudrate`` property, further functionality is provided by the ``SerialDevice`` class, available either as a top-level import from ``pylibftdi`` or through the ``serial_device`` module. This subclasses ``Device`` and adds additional properties to access various control and handshake lines. The following properties are available: ======== ==================== ========= property meaning direction -------- -------------------- --------- ``cts`` Clear To Send Input ``rts`` Ready To Send Output ``dsr`` Data Set Ready Input ``dtr`` Data Transmit Ready Output ``ri`` Ring Indicator Input ======== ==================== ========= Note that these lines are normally active-low, and ``pylibftdi`` makes no attempt to hide this from the user. It is impractical to try to 'undo' this inversion in any case, since it can be disabled in the EEPROM settings of the device. Just be aware if using these lines as GPIO that the electrical sense will be the opposite of the value read. The lines are intended to support handshaking rather than GPIO, so this is not normally an issue; if CTS is connected to RTS, then values written to RTS will be reflected in the value read from CTS. Subclassing `Device` - A MIDI device ------------------------------------ To abstract application code from the details of any particular interface, it may be helpful to subclass the ``Device`` class, providing the required configuration in the ``__init__`` method to act in a certain way. For example, the MIDI_ protocol used by electronic music devices is an asynchronous serial protocol operating at 31250 baud, and with the same 8-N-1 parameters which pylibftdi defaults to. .. _MIDI: https://www.midi.org Creating a ``MidiDevice`` subclass of ``Device`` is straightforward:: class MidiDevice(Device): "subclass of pylibftdi.Device configured for MIDI" def __init__(self, *o, **k): Device.__init__(self, *o, **k) self.baudrate = 31250 Note it is important that the superclass ``__init__`` is called first; calling it on an uninitialised ``Device`` would fail, and even if it succeeded, the superclass ``__init__`` method resets ``baudrate`` to 9600 anyway to ensure a consistent setup for devices which may have been previously used with different parameters. Use of the ``MidiDevice`` class is simple - as a pylibftdi Device instance, it provides a file-based API. Simply ``read()`` and ``write()`` the data to an instance of the class:: >>> m = MidiDevice() >>> m.write('\x90\x80\x80') >>> time.sleep(1) >>> m.write('\x80\x00') pylibftdi-0.24.1/docs/troubleshooting.rst000066400000000000000000000132141516421155500205030ustar00rootroot00000000000000pylibftdi troubleshooting ========================= Once up-and-running, pylibftdi is designed to be very simple, but sometimes getting it working in the first place can be more difficult. Error messages -------------- ``FtdiError: unable to claim usb device. Make sure the default FTDI driver is not in use (-5)`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This indicates a conflict with FTDI's own drivers, and is (as far as I know) mainly a problem on macOS, where they can be disabled (until reboot) by unloading the appropriate kernel module. .. _macos: macOS ~~~~~ Since macOS Monterey (12.0, 2021), Apple removed their built-in FTDI kernel driver (``com.apple.driver.AppleUSBFTDI``), so that is no longer a source of conflict. However, FTDI's own VCP driver (``com.FTDI.driver.FTDIUSBSerialDriver``) may have been installed by other software (e.g. the Arduino IDE) and will claim the device by default. Unload it to allow libftdi access:: sudo kextunload -bundle-id com.FTDI.driver.FTDIUSBSerialDriver To reload it:: sudo kextload -bundle-id com.FTDI.driver.FTDIUSBSerialDriver Earlier versions of ``pylibftdi`` (prior to 0.18.0) included scripts for macOS which automated this, but these were removed due to cross-platform packaging complexity. If you need to toggle the driver frequently (e.g. because other software also uses it), consider wrapping these commands in small shell scripts in your ``~/bin`` or ``/usr/local/bin``. Diagnosis --------- Getting a list of USB devices Mac OS X ~~~~~~~~ Start 'System Information', then select Hardware > USB, and look for your device. On the command line, ``system_profiler SPUSBDataType`` can be used. In the following example I've piped it into ``grep -C 7 FTDI``, to print 7 lines either side of a match on the string 'FTDI':: ben$ system_profiler SPUSBDataType | grep -C 7 FTDI UM232H: Product ID: 0x6014 Vendor ID: 0x0403 (Future Technology Devices International Limited) Version: 9.00 Serial Number: FTUBIOWF Speed: Up to 480 Mb/sec Manufacturer: FTDI Location ID: 0x24710000 / 7 Current Available (mA): 500 Current Required (mA): 90 USB Reader: Product ID: 0x4082 Linux ~~~~~ Use ``lsusb``. Example from my laptop:: ben@ben-laptop:~$ lsusb Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub Bus 002 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub Bus 003 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub Bus 004 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub Bus 005 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub Bus 006 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub Bus 007 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub Bus 008 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub Bus 008 Device 011: ID 0a5c:217f Broadcom Corp. Bluetooth Controller Bus 002 Device 009: ID 17ef:481d Lenovo Bus 002 Device 016: ID 0403:6014 Future Technology Devices International, Ltd FT232H Single HS USB-UART/FIFO IC Where did my ttyUSB devices go? ------------------------------- When a `pylibftdi.Device()` is opened, any kernel device which was previously present will become unavailable. On Linux for example, a serial-capable FTDI device will (via the `ftdi_sio` driver) create a device node such as `/dev/ttyUSB0` (or ttyUSB1,2,3 etc). This device allows use of the FTDI device as a simple file in the Linux filesystem which can be read and written. Various programs such as the Arduino IDE (at least when communicating with some board variants) and libraries such as `PySerial` will use this device. Once libftdi opens a device, the corresponding entry in /dev/ will disappear. Prior to `pylibftdi` version 0.16, the simplest way to get the device node to reappear would be to unplug and replug the USB device itself. Starting from 0.16, this should no longer be necessary as the kernel driver (which exports `/dev/ttyUSB...`) is reattached when the `pylibftdi` device is closed. This behaviour can be controlled by the `auto_detach` argument (which is defaulted to `True`) to the `Device` class; setting it to `False` reverts to the old behaviour. Note that on recent OS X, libftdi doesn't 'steal' the device, but instead refuses to open it. The kernel devices can be seen as `/dev/tty.usbserial-xxxxxxxx`, where `xxxxxxxx` is the device serial number. FTDI's Application Note AN134_ details this further (see section 'Using Apple-provided VCP or D2XX with OS X 10.9 & 10.10'). See the section above under Installation for further details on resolving this. .. _AN134: https://www.ftdichip.com/Support/Documents/AppNotes/AN_134_FTDI_Drivers_Installation_Guide_for_MAC_OSX.pdf Gathering information --------------------- Starting with pylibftdi version 0.15, an example script to gather system information is included, which will help in any diagnosis required. Run the following:: python3 -m pylibftdi.examples.info this will output a range of information related to the versions of libftdi libusb in use, as well as the system platform and Python version, for example:: pylibftdi version : 0.18.0 libftdi version : libftdi_version(major=1, minor=4, micro=0, version_str='1.4', snapshot_str='unknown') libftdi library name : libftdi1.so.2 libusb version : libusb_version(major=1, minor=0, micro=22, nano=11312, rc='', describe='http://libusb.info') libusb library name : libusb-1.0.so.0 Python version : 3.7.3 OS platform : Linux-5.0.0-32-generic-x86_64-with-Ubuntu-19.04-disco pylibftdi-0.24.1/pyproject.toml000066400000000000000000000033111516421155500165030ustar00rootroot00000000000000[project] name = "pylibftdi" version = "0.24.1" description = "Pythonic interface to FTDI devices using libftdi." license = "MIT" authors = [ {name = "Ben Bass", email = "benbass@codedstructure.net"}, ] readme = "README.rst" requires-python = ">=3.10" keywords = ["ftdi", "libftdi", "usb"] classifiers = [ "Development Status :: 4 - Beta", "Environment :: Console", "Intended Audience :: Developers", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Topic :: Scientific/Engineering", "Topic :: Software Development :: Embedded Systems", "Topic :: System :: Hardware", ] [project.urls] Homepage = "https://github.com/codedstructure/pylibftdi" Repository = "https://github.com/codedstructure/pylibftdi" Documentation = "https://pylibftdi.readthedocs.io/en/latest/" [build-system] requires = ["setuptools>=68.0"] build-backend = "setuptools.build_meta" [dependency-groups] dev = [ "ruff>=0.9", "pytest>=8.3", "mypy>=1.13", ] docs = [ "sphinx>=8.0", "sphinx-rtd-theme>=3.0", ] [tool.setuptools.packages.find] where = ["src"] [tool.ruff] target-version = "py310" [tool.ruff.lint] select = ["E", "W", "F", "B", "I", "UP", "PL"] ignore = [ "PLR2004", # Magic value used in comparison "PLR0913", # Too many arguments to function call ] [tool.pytest.ini_options] pythonpath = [ "src", ".", ] addopts = [ "--import-mode=importlib", ] pylibftdi-0.24.1/src/000077500000000000000000000000001516421155500143605ustar00rootroot00000000000000pylibftdi-0.24.1/src/pylibftdi/000077500000000000000000000000001516421155500163465ustar00rootroot00000000000000pylibftdi-0.24.1/src/pylibftdi/__init__.py000066400000000000000000000032661516421155500204660ustar00rootroot00000000000000""" pylibftdi - python wrapper for libftdi Copyright (c) 2010-2026 Ben Bass See LICENSE file for details and (absence of) warranty pylibftdi: https://github.com/codedstructure/pylibftdi libftdi can be found at: https://www.intra2net.com/en/developer/libftdi/ Neither libftdi nor Intra2net are associated with this project; if something goes wrong here, it's almost definitely my fault rather than a problem with the libftdi library. """ from importlib.metadata import PackageNotFoundError, version try: __VERSION__ = version("pylibftdi") except PackageNotFoundError: __VERSION__ = "0.23.0" # fallback for editable / uninstalled __AUTHOR__ = "Ben Bass" __all__ = [ "Driver", "Device", "BitBangDevice", "Bus", "FtdiError", "ALL_OUTPUTS", "ALL_INPUTS", "BB_OUTPUT", "BB_INPUT", "USB_VID_LIST", "USB_PID_LIST", ] from pylibftdi import _base, bitbang, device, driver, serial_device, util # Bring them in to package scope so we can treat pylibftdi # as a module if we want. FtdiError = _base.FtdiError LibraryMissingError = _base.LibraryMissingError Bus = util.Bus Driver = driver.Driver Device = device.Device SerialDevice = serial_device.SerialDevice BitBangDevice = bitbang.BitBangDevice USB_VID_LIST = driver.USB_VID_LIST USB_PID_LIST = driver.USB_PID_LIST ALL_OUTPUTS = bitbang.ALL_OUTPUTS ALL_INPUTS = bitbang.ALL_INPUTS BB_OUTPUT = bitbang.BB_OUTPUT BB_INPUT = bitbang.BB_INPUT FLUSH_BOTH = driver.FLUSH_BOTH FLUSH_INPUT = driver.FLUSH_INPUT FLUSH_OUTPUT = driver.FLUSH_OUTPUT # Use these for interface_select on multiple-interface devices INTERFACE_ANY = 0 INTERFACE_A = 1 INTERFACE_B = 2 INTERFACE_C = 3 INTERFACE_D = 4 pylibftdi-0.24.1/src/pylibftdi/__main__.py000066400000000000000000000007621516421155500204450ustar00rootroot00000000000000""" pylibftdi.__main__ This module exists to support `python -im pylibftdi` as a REPL with appropriate modules and constants already loaded. e.g. $ python3 -im pylibftdi >>> d = Device() >>> d.write('Hello World') """ # This should be aligned with `__all__` in pylibftdi/__init__.py from pylibftdi import ( # noqa ALL_INPUTS, ALL_OUTPUTS, BB_INPUT, BB_OUTPUT, USB_PID_LIST, USB_VID_LIST, BitBangDevice, Bus, Device, Driver, FtdiError, ) pylibftdi-0.24.1/src/pylibftdi/_base.py000066400000000000000000000007161516421155500177750ustar00rootroot00000000000000""" pylibftdi - python wrapper for libftdi Copyright (c) 2010-2020 Ben Bass See LICENSE file for details and (absence of) warranty pylibftdi: https://github.com/codedstructure/pylibftdi """ # This module contains things needed by at least one other # module so as to prevent circular imports. __ALL__ = ["FtdiError", "LibraryMissingError"] class FtdiError(Exception): pass class LibraryMissingError(FtdiError): pass pylibftdi-0.24.1/src/pylibftdi/bitbang.py000066400000000000000000000126351516421155500203350ustar00rootroot00000000000000""" pylibftdi - python wrapper for libftdi Copyright (c) 2010-2014 Ben Bass See LICENSE file for details and (absence of) warranty pylibftdi: https://github.com/codedstructure/pylibftdi """ from ctypes import byref, c_ubyte from pylibftdi._base import FtdiError from pylibftdi.device import Device from pylibftdi.driver import BITMODE_BITBANG ALL_OUTPUTS = 0xFF ALL_INPUTS = 0x00 BB_OUTPUT = 1 BB_INPUT = 0 class BitBangDevice(Device): """ simple subclass to support bit-bang mode Internally uses async mode at the moment, but provides a 'sync' flag (defaulting to True) which controls the behaviour of port reading and writing - if set, the FIFOs are ignored (read) or cleared (write) so operations will appear synchronous Adds three read/write properties to the base class: * `direction`: 8 bit input(0)/output(1) direction control. * `port`: 8 bit IO port, as defined by direction. * `latch`: 8 bit output value, allowing e.g. `bb.latch += 1` to make sense when there is a mix of input and output lines """ def __init__( self, device_id=None, direction=ALL_OUTPUTS, lazy_open=False, sync=True, bitbang_mode=BITMODE_BITBANG, interface_select=None, **kwargs, ): # initialise the super-class, but don't open yet. We really want # two-part initialisation here - set up all the instance variables # here in the super class, then open it after having set more # of our own variables. super().__init__( device_id=device_id, mode="b", lazy_open=True, interface_select=interface_select, **kwargs, ) self.direction = direction self.sync = sync self.bitbang_mode = bitbang_mode self._last_set_dir = None # latch is the latched state of output pins. # it is initialised to the read value of the pins # 'and'ed with those bits set to OUTPUT (1) self._latch = None if not lazy_open: self.open() def open(self): """open connection to a FTDI device""" # in case someone sets the direction before we are open()ed, # we intercept this call... super().open() if self.direction != self._last_set_dir: self.direction = self._direction return self def read_pins(self): """ read the current 'actual' state of the pins :return: 8-bit binary representation of pin state :rtype: int """ pin_byte = c_ubyte() res = self.ftdi_fn.ftdi_read_pins(byref(pin_byte)) if res != 0: raise FtdiError("Could not read device pins") return pin_byte.value @property def latch(self): """ latch property - the output latch (in-memory representation of output pin state) Note _latch is not masked by direction (except on initialisation), as otherwise a loop incrementing a mixed input/output port would not work, as it would 'stop' on input pins. This is the primary use case for 'latch'. It's basically a `port` which ignores input. :return: the state of the output latch """ if self._latch is None: self._latch = self.read_pins() & self.direction return self._latch @latch.setter def latch(self, value): self.port = value # this updates ._latch implicitly # direction property - 8 bit value determining whether an IO line # is output (if set to 1) or input (set to 0) @property def direction(self): """ get or set the direction of each of the IO lines. LSB=D0, MSB=D7 1 for output, 0 for input """ return self._direction @direction.setter def direction(self, new_dir): if not (0 <= new_dir <= 255): raise FtdiError("invalid direction bitmask") self._direction = new_dir if not self.closed: self.ftdi_fn.ftdi_set_bitmode(new_dir, self.bitbang_mode) self._last_set_dir = new_dir # port property - 8 bit read/write value @property def port(self): """ get or set the state of the IO lines. The value of output lines is persisted in this object for the purposes of reading, so read-modify-write operations (e.g. drv.port+=1) are valid. """ if self._direction == ALL_OUTPUTS: # a minor optimisation; no point reading from the port if # we have no input lines set result = self.latch else: if self.sync: result = self.read_pins() else: result = self.read(1)[0] # replace the 'output' bits with current value of self.latch - # the last written value. This makes read-modify-write # operations (e.g. 'drv.port |= 0x10') work as expected result = (result & ~self._direction) | ( # read input self.latch & self._direction ) # output latch return result @port.setter def port(self, value): # restrict to a single byte value &= 0xFF self._latch = value if self.sync: self.flush_output() # note to_bytes() gets these as default args in Python3.11+ return super().write(value.to_bytes(1, "big")) pylibftdi-0.24.1/src/pylibftdi/device.py000066400000000000000000000540271516421155500201670ustar00rootroot00000000000000""" pylibftdi.device - access to individual FTDI devices Copyright (c) 2010-2020 Ben Bass See LICENSE file for details and (absence of) warranty pylibftdi: https://github.com/codedstructure/pylibftdi """ from __future__ import annotations import codecs import functools import itertools import os import sys from ctypes import ( POINTER, Structure, byref, c_char_p, c_void_p, cast, create_string_buffer, ) from typing import no_type_check from pylibftdi._base import FtdiError from pylibftdi.driver import ( BITMODE_RESET, FLUSH_BOTH, FLUSH_INPUT, FLUSH_OUTPUT, FTDI_ERROR_DEVICE_NOT_FOUND, USB_PID_LIST, USB_VID_LIST, Driver, ) ERR_HELP_NOT_FOUND_FAIL = """ No device matching the given specification could be found. Is the device connected? Try running the following command to see if the device is listed: python3 -m pylibftdi.examples.list_devices """ ERR_HELP_LINUX_OPEN_FAIL = """ Could not access the FTDI device - this could be a permissions issue accessing the device. If the program works when run with root privileges (i.e. sudo) this is likely to be the issue. Running as a normal user should be possible by setting appropriate udev rules on the device. """ ERR_HELP_CLAIM_FAIL = """ Could not claim the FTDI USB device - either the device is already open, or another driver is preventing libftdi from claiming the device. """ ERR_HELP_LINUX_CLAIM_FAIL = ( ERR_HELP_CLAIM_FAIL + """ The Linux `ftdi_sio` driver is often the culprit here, and may be unloaded with `sudo rmmod ftdi_sio`. However in recent libftdi versions this should not be necessary, as a driver option to switch out the driver temporarily is applied (unless `auto_detach=False` is given in `Device` instantiation). """ ) ERR_HELP_MACOS_CLAIM_FAIL = ( ERR_HELP_CLAIM_FAIL + """ The following commands may be attempted in the terminal to unload the builtin drivers: sudo kextunload -bundle-id com.apple.driver.AppleUSBFTDI sudo kextunload -bundle-id com.FTDI.driver.FTDIUSBSerialDriver Reload these with the command 'kextload' replacing 'kextunload' above. Note the second of these will only be present if the FTDI-provided driver has been installed from their website: https://www.ftdichip.com/Drivers/VCP.htm """ ) # The only part of the ftdi context we need at this point is # libusb_device_handle, so we don't encode the entire structure. # Note the structure for 0.x is different (no libusb_context # member), but we don't support auto_detach on 0.x which is # the only case this is used. class ftdi_context_partial(Structure): # This is for libftdi 1.0+ _fields_ = [("libusb_context", c_void_p), ("libusb_device_handle", c_void_p)] class Device: """ Represents a connection to a single FTDI device """ # If false, don't open the device as part of instantiation lazy_open = False # chunk_size (if not 0) chunks the reads and writes # to allow interruption chunk_size = 0 # auto_detach is a flag to call libusb_set_auto_detach_kernel_driver # when we open the device auto_detach = True # defining softspace allows us to 'print' to this device softspace = 0 def __init__( self, device_id: str | None = None, mode: str = "b", encoding: str = "latin1", interface_select: int | None = None, device_index: int = 0, *, auto_detach: bool | None = None, lazy_open: bool | None = None, chunk_size: int | None = None, index: int | None = None, vid: int | None = None, pid: int | None = None, driver: Driver | None = None, ) -> None: """ Device([device_id[, mode, [OPTIONS ...]]) -> Device instance represents a single FTDI device accessible via the libftdi driver. Supports a basic file-like interface (open/close/read/write, context manager support). :param device_id: an optional serial number of the device to open. if omitted, this refers to the first device found, which is convenient if only one device is attached, but otherwise fairly useless. :param mode: either 'b' (binary) or 't' (text). This primarily affects Python 3 calls to read() and write(), which will accept/return unicode strings which will be encoded/decoded according to the given... :param encoding: the codec name to be used for text operations. :param interface_select: select interface to use on multi-interface devices :param device_index: optional index of the device to open, in the event of multiple matches for other parameters (PID, VID, device_id). Defaults to zero (the first device found). The following parameters are only available as keyword parameters and override class attributes, so may be specified in subclasses. :param lazy_open: if True, then the device will not be opened immediately - the user must perform an explicit open() call prior to other operations. :param chunk_size: if non-zero, split read and write operations into chunks of this size. With large or slow accesses, interruptions (i.e. KeyboardInterrupt) may not happen in a timely fashion. :param auto_detach: default True, whether to automatically re-attach the kernel driver on device close. :param index: optional index into list_devices() to open. Useful in the event that multiple devices of differing VID/PID are attached, where `device_index` is insufficient to select as device indexing restarts at 0 for each VID/PID combination. :param vid: optional vendor ID to open. If omitted, the default USB_VID_LIST is used to search for devices. :param pid: optional product ID to open. If omitted, the default USB_PID_LIST is used to search for devices. """ self._opened = False # These args allow overriding default class attributes, which can # also be overridden in subclasses. if auto_detach is not None: self.auto_detach = auto_detach if lazy_open is not None: self.lazy_open = lazy_open if chunk_size is not None: self.chunk_size = chunk_size self.driver = Driver() if driver is None else driver self.fdll = self.driver.fdll # device_id is an optional serial number of the requested device. self.device_id = device_id # mode can be either 'b' for binary, or 't' for text. # if set to text, the values returned from read() will # be decoded using encoding before being returned as # strings; for binary the raw bytes will be returned. self.mode = mode # when giving a str to Device.write(), it is encoded. # default is latin1, because it provides # a one-to-one correspondence for code points 0-FF self.encoding = encoding self.encoder = codecs.getincrementalencoder(self.encoding)() self.decoder = codecs.getincrementaldecoder(self.encoding)() # ftdi_usb_open_dev initialises the device baudrate # to 9600, which certainly seems to be a de-facto # standard for serial devices. self._baudrate = 9600 # interface can be set for devices which have multiple interface # ports (e.g. FT4232, FT2232) self.interface_select = interface_select # device_index is an optional integer index of device to choose self.device_index = device_index # list_index (from parameter `index`) is an optional integer index # into list_devices() entries. self.list_index = index self.vid = vid self.pid = pid # lazy_open tells us not to open immediately. if not self.lazy_open: self.open() def __del__(self) -> None: """free the ftdi_context resource""" if self._opened: self.close() def open(self) -> None: """ open connection to a FTDI device """ if self._opened: return if not self.device_id and self.list_index is not None: # Use serial number from list_index dev_list = self.driver.list_devices() try: # The third (index 2) field is serial number. self.device_id = dev_list[self.list_index][2] except IndexError: raise FtdiError( "index provided not in range of list_devices() entries" ) from None # create context for this device # Note I gave up on attempts to use ftdi_new/ftdi_free (just using # ctx instead of byref(ctx) in first param of most ftdi_* functions) as # (at least for 64-bit) they only worked if argtypes was declared # (c_void_p for ctx), and that's too much like hard work to maintain. # So I've reverted to using create_string_buffer for memory management, # byref(ctx) to pass in the context instance, and ftdi_init() / # ftdi_deinit() pair to manage the driver resources. It's very nice # how layered the libftdi code is, with access to each layer. self.ctx = create_string_buffer(1024) res = self.fdll.ftdi_init(byref(self.ctx)) if res != 0: msg = f"{self.get_error_string()} ({res})" del self.ctx raise FtdiError(msg) if self.interface_select is not None: res = self.fdll.ftdi_set_interface(byref(self.ctx), self.interface_select) if res != 0: msg = f"{self.get_error_string()} ({res})" del self.ctx raise FtdiError(msg) # Try to open the device. If this fails, reset things to how # they were, but we can't use self.close as that assumes things # have already been setup. res = self._open_device() if res != 0: msg = self.handle_open_error(res) # free the context self.fdll.ftdi_deinit(byref(self.ctx)) del self.ctx raise FtdiError(msg) if self.auto_detach and self.driver.libftdi_version().major > 0: # This doesn't reliably work on libftdi 0.x, so we ignore it ctx_p = cast(byref(self.ctx), POINTER(ftdi_context_partial)).contents dev = ctx_p.libusb_device_handle if dev: self.driver._libusb.libusb_set_auto_detach_kernel_driver( c_void_p(dev), 1 ) # explicitly reset the device to serial mode with standard settings # - no flow control, 9600 baud - in case it had previously been used # in bitbang mode (some later driver versions might do bits of this # automatically) self.ftdi_fn.ftdi_set_bitmode(0, BITMODE_RESET) self.ftdi_fn.ftdi_setflowctrl(0) self.baudrate = 9600 # reset the latency timer to 16ms (device default, but kernel device # drivers can set a different - e.g. 1ms - value) self.ftdi_fn.ftdi_set_latency_timer(16) self._opened = True def handle_open_error(self, errcode: int) -> str: """ return a (hopefully helpful) error message on a failed open() """ err_help = "" if errcode == -3: err_help = ERR_HELP_NOT_FOUND_FAIL elif errcode == -4 and sys.platform == "linux": err_help = ERR_HELP_LINUX_OPEN_FAIL elif errcode == -5: if sys.platform == "linux": err_help = ERR_HELP_LINUX_CLAIM_FAIL elif sys.platform == "darwin": err_help = ERR_HELP_MACOS_CLAIM_FAIL else: err_help = ERR_HELP_CLAIM_FAIL msg = f"{self.get_error_string()} ({errcode})\n{err_help}" return msg def _open_device(self) -> int: """ Actually open the target device :return: status of the open command (0 = success) :rtype: int """ # FTDI vendor/product ids required here. res: int = -1 vid_list = [self.vid] if self.vid is not None else USB_VID_LIST pid_list = [self.pid] if self.pid is not None else USB_PID_LIST for usb_vid, usb_pid in itertools.product(vid_list, pid_list): open_args = [byref(self.ctx), usb_vid, usb_pid, 0, 0, self.device_index] if self.device_id is None: res = self.fdll.ftdi_usb_open_desc_index(*tuple(open_args)) else: # attempt to match device_id to serial number open_args[-2] = c_char_p(self.device_id.encode("latin1")) res = self.fdll.ftdi_usb_open_desc_index(*tuple(open_args)) if res != 0: # swap (description, serial) parameters and try again # - attempt to match device_id to description open_args[-3], open_args[-2] = open_args[-2], open_args[-3] res = self.fdll.ftdi_usb_open_desc_index(*tuple(open_args)) if res != FTDI_ERROR_DEVICE_NOT_FOUND: # if we succeed (0) or get a specific error, don't continue # otherwise (-3) - look for another device break return res def close(self) -> None: """close our connection, free resources""" if self._opened: self.fdll.ftdi_usb_close(byref(self.ctx)) self.fdll.ftdi_deinit(byref(self.ctx)) del self.ctx self._opened = False @property def baudrate(self) -> int: """ get or set the baudrate of the FTDI device. Re-read after setting to ensure baudrate was accepted by the driver. """ return self._baudrate @baudrate.setter def baudrate(self, value: int) -> None: result = self.fdll.ftdi_set_baudrate(byref(self.ctx), value) if result == 0: self._baudrate = value def _read(self, length: int) -> bytes: """ actually do the low level reading :return: bytes read from the device :rtype: bytes """ buf = create_string_buffer(length) rlen = self.fdll.ftdi_read_data(byref(self.ctx), byref(buf), length) if rlen < 0: raise FtdiError(self.get_error_string()) byte_data = buf.raw[:rlen] return byte_data def read(self, length: int) -> str | bytes: """ read(length) -> bytes/string of up to `length` bytes. read upto `length` bytes from the FTDI device :param length: maximum number of bytes to read :return: value read from device :rtype: bytes if self.mode is 'b', else decode with self.encoding """ if not self._opened: raise FtdiError("read() on closed Device") # read the data if self.chunk_size != 0: remaining = length byte_data_list = [] while remaining > 0: rx_bytes = self._read(min(remaining, self.chunk_size)) if not rx_bytes: break byte_data_list.append(rx_bytes) remaining -= len(rx_bytes) byte_data = b"".join(byte_data_list) else: byte_data = self._read(length) if self.mode == "b": return byte_data else: return self.decoder.decode(byte_data) def _write(self, byte_data: bytes) -> int: """ actually do the low level writing :param byte_data: data to be written :type byte_data: bytes :return: number of bytes written """ buf = create_string_buffer(byte_data) written: int = self.fdll.ftdi_write_data( byref(self.ctx), byref(buf), len(byte_data) ) if written < 0: raise FtdiError(self.get_error_string()) return written def write(self, data: str | bytes) -> int: """ write(data) -> count of bytes actually written write given `data` string to the FTDI device :param data: string to be written :type data: string or bytes :return: count of bytes written, which may be less than `len(data)` """ if not self._opened: raise FtdiError("write() on closed Device") if not isinstance(data, bytes): data = self.encoder.encode(data) assert isinstance(data, bytes) # actually write it if self.chunk_size != 0: remaining = len(data) written = 0 while remaining > 0: start = written length = min(remaining, self.chunk_size) result = self._write(data[start : start + length]) if result == 0: # don't continue to try writing forever if nothing # is actually being written break else: written += result remaining -= result else: written = self._write(data) return written def flush(self, flush_what: int = FLUSH_BOTH) -> None: """ Instruct the FTDI device to flush its FIFO buffers By default both the input and output buffers will be flushed, but the caller can selectively chose to only flush the input or output buffers using `flush_what`: :param flush_what: select what to flush: `FLUSH_BOTH` (default); `FLUSH_INPUT` (just the rx buffer); `FLUSH_OUTPUT` (just the tx buffer) """ if flush_what == FLUSH_BOTH: fn = self.fdll.ftdi_tcioflush elif flush_what == FLUSH_INPUT: fn = self.fdll.ftdi_tciflush elif flush_what == FLUSH_OUTPUT: fn = self.fdll.ftdi_tcoflush else: raise ValueError( f"Invalid value passed to {self.__class__.__name__}.flush()" ) res = fn(byref(self.ctx)) if res != 0: msg = f"{self.get_error_string()} ({res})" raise FtdiError(msg) def flush_input(self) -> None: """ flush the device input buffer """ self.flush(FLUSH_INPUT) def flush_output(self) -> None: """ flush the device output buffer """ self.flush(FLUSH_OUTPUT) def get_error_string(self) -> str: """ :return: error string from libftdi driver """ return str(self.fdll.ftdi_get_error_string(byref(self.ctx))) @property def ftdi_fn(self): # type: ignore """ this allows the vast majority of libftdi functions which are called with a pointer to a ftdi_context struct as the first parameter to be called here preventing the need to leak self.ctx into the user code (and import byref from ctypes): >>> with Device() as dev: ... # set 8 bit data, 2 stop bits, no parity ... dev.ftdi_fn.ftdi_set_line_property(8, 2, 0) ... """ # note this class is constructed on each call, so this # won't be particularly quick. It does ensure that the # fdll and ctx objects in the closure are up-to-date, though. class FtdiForwarder: @no_type_check def __getattr__(innerself, key: str): return functools.partial(getattr(self.fdll, key), byref(self.ctx)) return FtdiForwarder() def __enter__(self) -> Device: """ support for context manager. Note the device is opened and closed automatically when used in a with statement, and the device object itself is returned: >>> with Device(mode='t') as dev: ... dev.write('Hello World!') ... """ self.open() return self @no_type_check def __exit__(self, exc_type, exc_val, tb) -> None: """support for context manager""" self.close() # # following are various properties and functions to make # this emulate a file-object more closely. # @property def closed(self) -> bool: """ The Python file API defines a read-only 'closed' attribute """ return not self._opened def readline(self, size: int = 0) -> str: """ readline() for file-like compatibility. :param size: maximum amount of data to read looking for a line :return: a line of text, or size bytes if no line-ending found This only works for mode='t' on Python3 """ lsl = len(os.linesep) line_buffer: list[str] = [] while True: next_char = self.read(1) if not isinstance(next_char, str): raise TypeError(".readline() only works for mode='t'") if not next_char or (0 < size < len(line_buffer)): break line_buffer.append(next_char) if len(line_buffer) >= lsl and line_buffer[-lsl:] == list(os.linesep): break return "".join(line_buffer) def readlines(self, sizehint: int | None = None) -> list[str]: """ readlines() for file-like compatibility. """ lines: list[str] = [] if sizehint is not None: string_blob = self.read(sizehint) if not isinstance(string_blob, str): raise TypeError(".readlines() only works for mode='t'") lines.extend(string_blob.splitlines()) while True: line = self.readline() if not line: break lines.append(line) return lines def writelines(self, lines: list[str | bytes]) -> None: """ writelines for file-like compatibility. :param lines: sequence of lines to write """ for line in lines: self.write(line) def __iter__(self) -> Device: return self def __next__(self) -> str: while True: line = self.readline() if line: return line else: raise StopIteration next = __next__ pylibftdi-0.24.1/src/pylibftdi/driver.py000066400000000000000000000245621516421155500202240ustar00rootroot00000000000000""" pylibftdi.driver - interface to the libftdi library Copyright (c) 2010-2014 Ben Bass See LICENSE file for details and (absence of) warranty pylibftdi: https://github.com/codedstructure/pylibftdi """ from __future__ import annotations import itertools from collections import namedtuple # be disciplined so pyflakes can check us... from ctypes import ( POINTER, Structure, byref, c_char_p, c_int, c_uint16, c_void_p, cast, cdll, create_string_buffer, ) from ctypes.util import find_library from typing import Any from pylibftdi._base import FtdiError, LibraryMissingError class libusb_version_struct(Structure): _fields_ = [ ("major", c_uint16), ("minor", c_uint16), ("micro", c_uint16), ("nano", c_uint16), ("rc", c_char_p), ("describe", c_char_p), ] libusb_version = namedtuple("libusb_version", "major minor micro nano rc describe") class ftdi_device_list(Structure): _fields_ = [("next", c_void_p), ("dev", c_void_p)] class ftdi_version_info(Structure): _fields_ = [ ("major", c_int), ("minor", c_int), ("micro", c_int), ("version_str", c_char_p), ("snapshot_str", c_char_p), ] libftdi_version = namedtuple( "libftdi_version", "major minor micro version_str snapshot_str" ) # These constants determine what type of flush operation to perform FLUSH_BOTH = 1 FLUSH_INPUT = 2 FLUSH_OUTPUT = 3 # Device Modes BITMODE_RESET = 0x00 BITMODE_BITBANG = 0x01 # Opening / searching for a device uses this list of IDs to search # by default. These can be extended directly after import if required. FTDI_VENDOR_ID = 0x0403 USB_VID_LIST = [FTDI_VENDOR_ID] USB_PID_LIST = [0x6001, 0x6010, 0x6011, 0x6014, 0x6015] FTDI_ERROR_DEVICE_NOT_FOUND = -3 class Driver: """ This is where it all happens... We load the libftdi library, and use it. """ # The default library names to search for. This can be overridden by # passing a library name or list of library names to the constructor. # Prefer libftdi1 if available. Windows uses 'lib' prefix. _lib_search = { "libftdi": ["ftdi1", "libftdi1", "ftdi", "libftdi"], "libusb": ["usb-1.0", "libusb-1.0"], } def __init__(self, libftdi_search: str | list[str] | None = None) -> None: """ :param libftdi_search: force a particular version of libftdi to be used can specify either library name(s) or path(s) :type libftdi_search: string or a list of strings """ if isinstance(libftdi_search, str): self._lib_search["libftdi"] = [libftdi_search] elif isinstance(libftdi_search, list): self._lib_search["libftdi"] = libftdi_search elif libftdi_search is not None: raise TypeError( f"libftdi_search type should be " f"Optional[str|list[str]], not {type(libftdi_search)}" ) # Library handles. self._fdll: Any = None self._libusb_dll: Any = None def _load_library(self, name: str, search_list: list[str] | None = None) -> Any: """ find and load the requested library :param name: library name :param search_list: an optional list of strings referring to library names library names or paths can be given :return: a CDLL object referring to the requested library """ # If no search list is given, use default library names from self._lib_search if search_list is None: search_list = self._lib_search.get(name, []) elif not isinstance(search_list, list): raise TypeError( f"search_list type should be list[str], not {type(search_list)}" ) lib = None for dll in search_list: try: # Windows in particular can have find_library # not find things which work fine directly on # cdll access. lib = getattr(cdll, dll) break # On DLL load fail, Python <3.12 raises OSError, 3.12+ AttributeError. except (OSError, AttributeError): lib_path = find_library(dll) if lib_path is not None: lib = getattr(cdll, lib_path) break if lib is None: raise LibraryMissingError( f"{name} library not found (search: {str(search_list)})" ) return lib @property def _libusb(self): # type: ignore """ ctypes DLL referencing the libusb library, if it exists Note this is not normally used directly by pylibftdi, and is available primarily for diagnostic purposes. """ if self._libusb_dll is None: self._libusb_dll = self._load_library("libusb") self._libusb_dll.libusb_get_version.restype = POINTER(libusb_version_struct) return self._libusb_dll def libusb_version(self) -> libusb_version: """ :return: namedtuple containing version info on libusb """ ver = self._libusb.libusb_get_version().contents return libusb_version( ver.major, ver.minor, ver.micro, ver.nano, ver.rc.decode(), ver.describe.decode(), ) @property def fdll(self) -> Any: """ ctypes DLL referencing the libftdi library This is the main interface to FTDI functionality. """ if self._fdll is None: self._fdll = self._load_library("libftdi") # most args/return types are fine with the implicit # int/void* which ctypes uses, but some need setting here self._fdll.ftdi_get_error_string.restype = c_char_p self._fdll.ftdi_usb_get_strings.argtypes = ( c_void_p, c_void_p, c_char_p, c_int, c_char_p, c_int, c_char_p, c_int, ) # library versions <1.0 don't provide ftdi_get_library_version, so # we need to check for it before setting the restype. if hasattr(self._fdll, "ftdi_get_library_version"): self._fdll.ftdi_get_library_version.restype = ftdi_version_info return self._fdll def libftdi_version(self) -> libftdi_version: """ :return: the version of the underlying library being used :rtype: tuple (major, minor, micro, version_string, snapshot_string) """ if hasattr(self.fdll, "ftdi_get_library_version"): version = self.fdll.ftdi_get_library_version() return libftdi_version( version.major, version.minor, version.micro, version.version_str.decode(), version.snapshot_str.decode(), ) else: # library versions <1.0 don't support this function... return libftdi_version( 0, 0, 0, "< 1.0 - no ftdi_get_library_version()", "unknown" ) def list_devices(self) -> list[tuple[str, str, str]]: """ :return: (manufacturer, description, serial#) for each attached device, e.g.: [('FTDI', 'UM232R USB <-> Serial', 'FTE4FFVQ'), ('FTDI', 'UM245R', 'FTE00P4L')] :rtype: a list of string triples the serial number can be used to open specific devices """ # ftdi_usb_find_all sets dev_list_ptr to a linked list # (*next/*usb_device) of usb_devices, each of which can # be passed to ftdi_usb_get_strings() to get info about # them. # this will contain the device info to return devices = [] manuf = create_string_buffer(128) desc = create_string_buffer(128) serial = create_string_buffer(128) devlistptrtype = POINTER(ftdi_device_list) dev_list_ptr = devlistptrtype() # create context for doing the enumeration ctx = create_string_buffer(1024) if self.fdll.ftdi_init(byref(ctx)) != 0: msg = self.fdll.ftdi_get_error_string(byref(ctx)) raise FtdiError(msg) def _s(s: bytes) -> str: """c_char_p -> str helper""" return s.decode() try: for usb_vid, usb_pid in itertools.product(USB_VID_LIST, USB_PID_LIST): res = self.fdll.ftdi_usb_find_all( byref(ctx), byref(dev_list_ptr), usb_vid, usb_pid ) if res < 0: err_msg = self.fdll.ftdi_get_error_string(byref(ctx)) msg = f"{err_msg} ({res})" raise FtdiError(msg) elif res > 0: # take a copy of the dev_list for subsequent list_free dev_list_base = byref(dev_list_ptr) # traverse the linked list... try: while dev_list_ptr: res = self.fdll.ftdi_usb_get_strings( byref(ctx), dev_list_ptr.contents.dev, manuf, 127, desc, 127, serial, 127, ) # don't error on failure to get all the data # error codes: -7: manuf, -8: desc, -9: serial if res < 0 and res not in (-7, -8, -9): err_msg = self.fdll.ftdi_get_error_string(byref(ctx)) msg = f"{err_msg} ({res})" raise FtdiError(msg) devices.append( (_s(manuf.value), _s(desc.value), _s(serial.value)) ) # step to next in linked-list dev_list_ptr = cast( dev_list_ptr.contents.next, devlistptrtype ) finally: self.fdll.ftdi_list_free(dev_list_base) finally: self.fdll.ftdi_deinit(byref(ctx)) return devices pylibftdi-0.24.1/src/pylibftdi/examples/000077500000000000000000000000001516421155500201645ustar00rootroot00000000000000pylibftdi-0.24.1/src/pylibftdi/examples/__init__.py000066400000000000000000000000311516421155500222670ustar00rootroot00000000000000"""pylibftdi examples""" pylibftdi-0.24.1/src/pylibftdi/examples/bit_server.py000066400000000000000000000062551516421155500227120ustar00rootroot00000000000000""" bit_server.py - remote HTTP interface to bit-bangged FTDI port This runs as a web server, connect to port 8008 Change HTTP_PORT for different port number or supply alternate as args[1] Requires: - pylibftdi """ import sys import threading import time import webbrowser from http.server import BaseHTTPRequestHandler, HTTPServer from io import BytesIO from socketserver import ThreadingMixIn from pylibftdi import BitBangDevice HTTP_PORT = 8008 class ThreadingServer(ThreadingMixIn, HTTPServer): pass def get_page(): port = switch.port page = f""" {port} - pylibftdi
""" for idx in range(8): bit = 7 - idx is_on = port & (1 << bit) color = "#00FF00" if is_on else "#FF0000" page += f"""
""" page += f"""
DATA={port} """ return page class ReqHandler(BaseHTTPRequestHandler): def do_GET(self): f = self.send_head() if f: self.wfile.write(f.read()) f.close() def do_POST(self): length = self.headers["content-length"] nbytes = int(length) query = self.rfile.read(nbytes).decode() # this is lazy and fragile - assumes only a single # query parameter XXX if query.startswith("bit"): bit = int(query[3]) value = 1 if query.rsplit("=", 1)[-1] == "true" else 0 if value: switch.port |= 1 << bit else: switch.port &= 255 ^ (1 << bit) f = self.send_head() if f: self.wfile.write(f.read()) f.close() def send_head(self): f = BytesIO() f.write(get_page().encode()) length = f.tell() f.seek(0) self.send_response(200) self.send_header("Content-type", "text/html") self.send_header("Content-Length", str(length)) self.end_headers() return f def runserver(port=HTTP_PORT): serveraddr = ("", port) srvr = ThreadingServer(serveraddr, ReqHandler) srvr.serve_forever() if __name__ == "__main__": switch = BitBangDevice() try: HTTP_PORT = int(sys.argv[1]) except (ValueError, TypeError): print("Usage: FtdiWebServer [portnumber]") except IndexError: pass t = threading.Thread(target=runserver, args=(HTTP_PORT,)) t.daemon = True t.start() print(f"Webserver running on localhost port {HTTP_PORT}") time.sleep(0.5) retry = 10 while retry: try: webbrowser.open(f"http://localhost:{HTTP_PORT}") except OSError: time.sleep(1) retry -= 1 else: break # wait for Ctrl-C try: while True: time.sleep(100) except KeyboardInterrupt: pass pylibftdi-0.24.1/src/pylibftdi/examples/info.py000066400000000000000000000032301516421155500214670ustar00rootroot00000000000000""" Report environment info relevant to pylibftdi example usage:: $ python3 -m pylibftdi.examples.info pylibftdi version : 0.15.0 libftdi version : libftdi_version(major=1, minor=1, micro=0, version_str='1.1', snapshot_str='unknown') libftdi library path : /usr/local/lib/libftdi1.dylib libusb version : libusb_version(major=1, minor=0, micro=19, nano=10903, rc='', describe='http://libusb.info') libusb library path : /usr/local/lib/libusb-1.0.dylib Python version : 3.4.0 OS platform : Darwin-14.1.0-x86_64-i386-64bit Copyright (c) 2015-2020 Ben Bass """ # noqa: E501 import platform from collections import OrderedDict import pylibftdi def ftdi_info(): """ Return (ordered) dictionary contianing pylibftdi environment info Designed for display purposes only; keys and value types may vary. """ info = OrderedDict() d = pylibftdi.Driver() info["pylibftdi version"] = pylibftdi.__VERSION__ try: info["libftdi version"] = d.libftdi_version() info["libftdi library name"] = d._load_library("libftdi")._name except pylibftdi.LibraryMissingError: info["libftdi library"] = "Missing" try: info["libusb version"] = d.libusb_version() info["libusb library name"] = d._load_library("libusb")._name except pylibftdi.LibraryMissingError: info["libusb library"] = "Missing" info["Python version"] = platform.python_version() info["OS platform"] = platform.platform() return info if __name__ == "__main__": for key, value in ftdi_info().items(): print(f"{key:22}: {value}") pylibftdi-0.24.1/src/pylibftdi/examples/lcd.py000066400000000000000000000051521516421155500213030ustar00rootroot00000000000000""" Write a string (argv[1] if run from command line) to a HD44780 LCD module connected via a FTDI UM232R/245R module using pylibftdi example usage: # while true; > do python lcd.py $( awk '{print $1}' /proc/loadavg); > sleep 5; > done Copyright (c) 2010-2014 Ben Bass All rights reserved. """ from pylibftdi import BitBangDevice, Bus class LCD: """ The UM232R/245R is wired to the LCD as follows: DB0..3 to LCD D4..D7 (pin 11..pin 14) DB6 to LCD 'RS' (pin 4) DB7 to LCD 'E' (pin 6) """ data = Bus(0, 4) rs = Bus(6) e = Bus(7) def __init__(self, device): # The Bus descriptor assumes we have a 'device' # attribute which provides a port self.device = device def _trigger(self): """generate a falling edge""" self.e = 1 self.e = 0 def init_four_bit(self): """ set the LCD's 4 bit mode, since we only have 8 data lines and need at least 2 to strobe data into the module and select between data and commands. """ self.rs = 0 self.data = 3 for _ in range(3): self._trigger() self.data = 2 self._trigger() def _write_raw(self, rs, x): # rs determines whether this is a command # or a data byte. Write the data as two # nibbles. Ahhh... nibbles. QBasic anyone? self.rs = rs self.data = x >> 4 self._trigger() self.data = x & 0x0F self._trigger() def write_cmd(self, x): self._write_raw(0, x) def write_data(self, x): self._write_raw(1, x) def display(string, device_id=None): """ Display the given string on an attached LCD an optional `device_id` can be given. """ with BitBangDevice(device_id) as bb: # These LCDs are quite slow - and the actual baudrate # is 16x this in bitbang mode... bb.baudrate = 60 lcd = LCD(bb) lcd.init_four_bit() # 001xxxxx - function set lcd.write_cmd(0x20) # 00000001 - clear display lcd.write_cmd(0x01) # 000001xx - entry mode set # bit 1: inc(1)/dec(0) # bit 0: shift display lcd.write_cmd(0x06) # 00001xxx - display config # bit 2: display on # bit 1: display cursor # bit 0: blinking cursor lcd.write_cmd(0x0C) for i in string: lcd.write_data(ord(i)) if __name__ == "__main__": import sys if len(sys.argv) == 2: display(sys.argv[1]) else: print("Usage: %s 'display string'") pylibftdi-0.24.1/src/pylibftdi/examples/led_flash.py000066400000000000000000000014061516421155500224600ustar00rootroot00000000000000""" Flash an LED connected via a FTDI UM232R/245R module using pylibftdi Optionally supply a flash rate (in Hz, default 1) as an argument Copyright (c) 2010-2014 Ben Bass All rights reserved. """ import sys import time from pylibftdi import BitBangDevice def flash_forever(rate): """toggle bit zero at rate Hz""" # put an LED with 1Kohm or similar series resistor # on D0 pin with BitBangDevice() as bb: while True: time.sleep(1.0 / (2 * rate)) bb.port ^= 1 def main(): if len(sys.argv) > 1: rate = float(sys.argv[1]) flash_forever(rate) else: flash_forever(1) if __name__ == "__main__": try: main() except KeyboardInterrupt: pass pylibftdi-0.24.1/src/pylibftdi/examples/list_devices.py000066400000000000000000000021761516421155500232210ustar00rootroot00000000000000""" Report connected FTDI devices. This may be useful in obtaining serial numbers to use as the device_id parameter of the Device() constructor to communicate with a specific device when more than one is present. example usage: $ python pylibftdi/examples/list_devices.py FTDI:UB232R:FTAS1UN5 FTDI:UM232R USB <-> Serial:FTE4FFVQ To open a device specifically to communicate with the second of these devices, the following would be used: >>> from pylibftdi import Device >>> dev = Device(device_id="FTE4FFVQ") >>> Copyright (c) 2011-2014 Ben Bass All rights reserved. """ from pylibftdi import Driver def get_ftdi_device_list(): """ return a list of lines, each a colon-separated vendor:product:serial summary of detected devices """ dev_list = [] for device in Driver().list_devices(): # device must always be this triple vendor, product, serial = device dev_list.append(f"{vendor}:{product}:{serial}") return dev_list def main(): for device in get_ftdi_device_list(): print(device) if __name__ == "__main__": main() pylibftdi-0.24.1/src/pylibftdi/examples/magic_candle.py000066400000000000000000000014601516421155500231250ustar00rootroot00000000000000""" Magic Candle - light falling on the LDR turns on the LED, which due to arrangement keeps the LED on until LDR/LED path is blocked LDR (via a transistor switch - dark = '1') - D0 LED (via series resistor) - D1 pylibftdi - codedstructure 2013-2014 """ import time from pylibftdi import BitBangDevice from pylibftdi.util import Bus class Candle: is_dark = Bus(0) # D0 be_light = Bus(1) # D1 def __init__(self): # make the device connection, this is used # in the Bus descriptors. Also set direction # appropriately. self.device = BitBangDevice(direction=0xFE) def run(self): while True: time.sleep(0.05) self.be_light = not self.is_dark if __name__ == "__main__": c = Candle() c.run() pylibftdi-0.24.1/src/pylibftdi/examples/midi_output.py000066400000000000000000000015471516421155500231070ustar00rootroot00000000000000import time from pylibftdi import Device class MidiDevice(Device): def __init__(self, *o, **k): Device.__init__(self, *o, **k) self.baudrate = 31250 MAJOR_INTERVAL = [2, 2, 1, 2, 2, 2, 1, 2] MINOR_INTERVAL = [2, 1, 2, 2, 1, 2, 2, 2] START_NOTE = 48 def volume(beat): return 100 if beat % 2 else 127 def scale(): midi = MidiDevice() note = START_NOTE for i in range(8): midi.write(f"\x90{chr(note)}{chr(volume(i))}") time.sleep(0.25) midi.write(f"\x90{chr(note)}\x00") note += MAJOR_INTERVAL[i] time.sleep(0.125) time.sleep(0.5) for i in range(8): note -= MINOR_INTERVAL[7 - i] midi.write(f"\x90{chr(note)}{chr(volume(i))}") time.sleep(0.35) midi.write(f"\x90{chr(note)}\x00") time.sleep(0.125) if __name__ == "__main__": scale() pylibftdi-0.24.1/src/pylibftdi/examples/morse.py000066400000000000000000000040011516421155500216560ustar00rootroot00000000000000""" `morse` - pylibftdi example to generate morse code signals Requires a bitbang-capable device with appropriate actuator (LED, buzzer etc) connected to bit 0. """ from os import isatty from time import sleep from pylibftdi import BitBangDevice # See https://en.wikipedia.org/wiki/Morse_code morse_code = """ a.- b-... c-.-. d-.. e. f..-. g--. h.... i.. j.--- k-.- l.-.. m-- n-. o--- p.--. q--.- r.-. s... t- u..- v...- w.-- x-..- y-.-- z--.. 1.---- 2..--- 3...-- 4....- 5..... 6-.... 7--... 8---.. 9----. 0----- """ morse_map = {m[0]: m[1:] for m in morse_code.split()} def output(s, device, wpm=12): """ output given string `s` as a Morse code pattern to given BitBangDevice :param s: string to render in morse code :param device: open bitbangdevice :param wpm: words-per-minute rate. (default 12) """ # Assume 5 letters per word, 13 units (e.g. 5 dots plus space) per letter delay = 60 / (5 * 13 * wpm) for word in s.split(): for letter in word: morse = morse_map.get(letter) if not morse: # completely ignore unknown characters continue for symbol in morse: if symbol == ".": device.port = 1 sleep(delay) device.port = 0 else: device.port = 1 sleep(3 * delay) device.port = 0 sleep(delay) # inter-symbol delay; 1 unit (same as dot) # inter-letter delay; 3 units sleep(3 * delay) # inter-word delay; 7 units sleep(7 * delay) def main(wpm=12): """ :param wpm: words per minute """ while True: try: s = input("Morse:> " if isatty(0) else "").lower().strip() except EOFError: break with BitBangDevice() as device: output(s, device, wpm=wpm) if isatty(0): print("Bye!") if __name__ == "__main__": main() pylibftdi-0.24.1/src/pylibftdi/examples/pin_read.py000077500000000000000000000065101516421155500223240ustar00rootroot00000000000000#!/usr/bin/env python3 """ Display values on input pins of a BitBangDevice. TODO: * ANSI colours / display differences in bold example - beep on pin 1 going high: $ pylibftdi/examples/pin_read.py -n 0.01 -m 1 -k 1 && beep Copyright (c) 2011-2014 Ben Bass All rights reserved. """ import argparse import itertools import sys import time from pylibftdi import ALL_INPUTS, BitBangDevice def get_value(): """ get the value of the pins """ if not getattr(get_value, "dev", None): get_value.dev = BitBangDevice(direction=ALL_INPUTS) dev = get_value.dev return dev.port def display_value(value): """ display the given value """ sys.stdout.write("\b" * 32) for n in range(8): sys.stdout.write("1 " if value & (1 << (7 - n)) else "0 ") sys.stdout.write(f" ({value}/0x{value:02X})") sys.stdout.flush() def display_loop(interval=1, count=0, match=None, mask=0xFF): """ display and compare the value :param interval: polling interval in seconds :param count: number of polls to do, or infinite if 0 :param match: value to look for to exit early :param mask: mask of read value before comparing to match :return: 'ok'. either a match was made or none was requested :rtype: bool """ if not count: count_iter = itertools.count() else: count_iter = range(count) try: for _ in count_iter: value = get_value() display_value(value) if match is not None and (value & mask == match): return True time.sleep(interval) return False except KeyboardInterrupt: pass finally: sys.stdout.write("\n") if match is not None: # we quit early while looking for a match return False else: # no match to do; no problem. return True def main(args=None): parser = argparse.ArgumentParser() parser.add_argument( "-n", "--interval", dest="interval", default=1, type=float, help="refresh interval, default 1 second", ) parser.add_argument( "-c", "--count", dest="count", default=0, type=int, help="number of cycles to run for (0 = no limit - the default)", ) parser.add_argument( "-m", "--match", dest="match", help="value to match against (e.g. 0x1F, 7, etc)" ) parser.add_argument( "-k", "--mask", dest="mask", help="mask to match with (e.g. 0x07, 2, etc) - default 0xFF", ) args = parser.parse_args(args) if args.interval < 0.001: parser.error("interval must be >= 0.001") if args.count < 0: parser.error("count must be >= 0") mask = match = None if args.mask: if not args.match: parser.error("Must specify --match with mask") try: mask = int(args.mask, 0) except ValueError: parser.error("Could not interpret given mask") else: mask = 0xFF if args.match: try: match = int(args.match, 0) except ValueError: parser.error("Could not interpret given mask") ok = display_loop(args.interval, args.count, match, mask) sys.exit(0 if ok else 1) if __name__ == "__main__": main() pylibftdi-0.24.1/src/pylibftdi/examples/serial_loopback.py000066400000000000000000000033431516421155500236720ustar00rootroot00000000000000#!/usr/bin/env python3 -u """ test serial loopback; assumes Rx and Tx are connected Copyright (c) 2010-2020 Ben Bass All rights reserved. """ import os import sys import time from pylibftdi import SerialDevice def test_string(length): return os.urandom(length) class LoopbackTester: def __init__(self): self.device = SerialDevice(chunk_size=16) def test_loopback(self, length): test_str = test_string(length) if self.device.write(test_str) != len(test_str): sys.stdout.write("*") time.sleep(0.1) result = "" for _ in range(5): result = self.device.read(length) time.sleep(0.1) if result: break if result != test_str: self.device.flush() time.sleep(0.25) return result == test_str def test_iter(self, lengths): self.device.flush() time.sleep(0.1) for length in lengths: yield self.test_loopback(length) def bisect(self): xmin, xmax = 1, 5000 last_test = None while True: test = (xmin + xmax) // 2 if test == last_test: break if self.test_loopback(test): xmin = test else: xmax = test last_test = test return test def main(self): print("Determining largest non-streamed buffer size") for bd in [9600, 31250, 115200, 1152000]: print(f"Baudrate: {bd}") self.device.baudrate = bd result = self.bisect() print(f"Buffer size: {result}") if __name__ == "__main__": tester = LoopbackTester() tester.main() pylibftdi-0.24.1/src/pylibftdi/examples/serial_transfer.py000066400000000000000000000153251516421155500237270ustar00rootroot00000000000000#!/usr/bin/env python3 -u """ test serial transfer between two devices This module assumes two devices (e.g. FT232R based) are connected to the host computer, with RX and TX of the two devices wired to each other so they can communicate. It launches threads to send and receive traffic and check that a random stream sent from one device is correctly received at the other: Testing 9600 baud Half duplex d1->d2... Bytes TX: 10000 RX: 10000 Checksum TX: ad7e985fdddfbc04e398daa781a9fad0 RX: ad7e985fdddfbc04e398daa781a9fad0 SUCCESS Half duplex d2->d1... Bytes TX: 10000 RX: 10000 Checksum TX: 61338c11fe18642a07f196094646295f RX: 61338c11fe18642a07f196094646295f SUCCESS Full duplex d1<=>d2... Bytes TX: 10000 RX: 10000 Checksum TX: 7dcc7ed3b89e46592c777ec42c330fd8 RX: 7dcc7ed3b89e46592c777ec42c330fd8 SUCCESS Bytes TX: 10000 RX: 10000 Checksum TX: 1a957192b8219aa02ad374dd518e37fd RX: 1a957192b8219aa02ad374dd518e37fd SUCCESS Copyright (c) 2015-2020 Ben Bass All rights reserved. """ import hashlib import random import threading import time from itertools import islice from pylibftdi import Device, FtdiError class RandomStream: """ Infinite iterator of random data which can be queried at any point for the checksum of the data already yielded """ def __init__(self, block_size=1024): self._block_size = block_size # Note: `reset()` sets the initial attributes self.reset() @staticmethod def _rand_gen(size): """Return a `bytes` instance of `size` random byte""" return bytes(bytearray(random.getrandbits(8) for _ in range(size))) def reset(self): self.stream_hash = hashlib.md5() self.rand_buf = self._rand_gen(self._block_size) self.chk_tail = self.chk_head = 0 self.bytecount = 0 def _update_checksum(self): self.stream_hash.update(self.rand_buf[self.chk_tail : self.chk_head]) self.chk_tail = self.chk_head def checksum(self): self._update_checksum() return self.stream_hash.hexdigest() def __iter__(self): while True: # Use slice rather than index to avoid bytes->int conversion # in Python3 data = self.rand_buf[self.chk_head : self.chk_head + 1 :] self.chk_head += 1 self.bytecount += 1 yield data if self.chk_head == self._block_size: self._update_checksum() self.rand_buf = self._rand_gen(self._block_size) self.chk_head = self.chk_tail = 0 def test_rs(): r = RandomStream() prev_checksum = 0 stream_bytes = [] for _i in range(30): stream_bytes.append(b"".join(islice(r, 500))) assert r.checksum() != prev_checksum assert r.checksum() == r.checksum() assert hashlib.md5(b"".join(stream_bytes)).hexdigest() == r.checksum() class HalfDuplexTransfer: """ Test streaming bytes from one device to another """ def __init__(self, source, dest, baudrate=9600, block_size=500): """ Prepare for half-duplex transmission from source device to dest """ self.source = source self.dest = dest self.source.baudrate = baudrate self.dest.baudrate = baudrate self.target = [] self.wait_signal = threading.Event() self.running = threading.Event() self.rs = RandomStream() self.block_size = block_size self.test_duration = 10 self.t1 = None self.t2 = None self.done = False def reader(self): # Tell writer we're ready for the deluge... self.wait_signal.set() # if we've just finished reading when self.done get's set by the # writer, we won't get the 'last' packet. But if we assume there's # always one more after done gets set, we'll get some ReadTimeouts.... # Probably best to try one more time but catch & ignore ReadTimeout. while not self.done: data = self.dest.read(1024) self.target.append(data) try: data = self.dest.read(1024) self.target.append(data) except FtdiError: pass def writer(self): self.running.set() self.wait_signal.wait() end_time = time.time() + self.test_duration while time.time() < end_time: x = b"".join(list(islice(self.rs, self.block_size))) self.source.write(x) # Wait for the reader to catch up time.sleep(0.01) self.done = True def go(self, test_duration=None): if test_duration is not None: self.test_duration = test_duration self.t1 = threading.Thread(target=self.writer) self.t1.daemon = True self.t1.start() # We wait for the writer to be actually running (but not yet # writing anything) before we start the reader. self.running.wait() self.t2 = threading.Thread(target=self.reader) self.t2.daemon = True self.t2.start() def join(self): # Use of a timeout allows Ctrl-C interruption self.t1.join(timeout=1e6) self.t2.join(timeout=1e6) def results(self): result = b"".join(self.target) print(f" Bytes TX: {self.rs.bytecount} RX: {len(result)}") rx_chksum = hashlib.md5(b"".join(self.target)).hexdigest() print(f" Checksum TX: {self.rs.checksum()} RX: {rx_chksum}") if len(result) == self.rs.bytecount and self.rs.checksum() == rx_chksum: print(" SUCCESS") else: print(" FAIL") def test_half_duplex_transfer(d1, d2, baudrate=9600): """ Test half-duplex stream from d1 to d2, report on status """ hd = HalfDuplexTransfer(d1, d2, baudrate) hd.go() hd.join() hd.results() def test_full_duplex_transfer(d1, d2, baudrate=9600): """ Start two half-duplex streams in opposite directions at the same time, check both are OK """ hd1 = HalfDuplexTransfer(d1, d2, baudrate) hd1.go() hd2 = HalfDuplexTransfer(d2, d1, baudrate) hd2.go() hd1.join() hd1.results() hd2.join() hd2.results() def main(): d1 = Device(device_index=0) d2 = Device(device_index=1) for b in 9600, 38400, 115200: print(f"Testing {b} baud") d1.flush() d2.flush() print("Half duplex d1->d2...") test_half_duplex_transfer(d1, d2, baudrate=b) d1.flush() d2.flush() print("Half duplex d2->d1...") test_half_duplex_transfer(d2, d1, baudrate=b) d1.flush() d2.flush() print("Full duplex d1<=>d2...") test_full_duplex_transfer(d1, d2, baudrate=b) if __name__ == "__main__": test_rs() main() pylibftdi-0.24.1/src/pylibftdi/serial_device.py000066400000000000000000000057751516421155500215340ustar00rootroot00000000000000""" pylibftdi - python wrapper for libftdi Copyright (c) 2010-2014 Ben Bass See LICENSE file for details and (absence of) warranty pylibftdi: https://github.com/codedstructure/pylibftdi """ from ctypes import byref, c_uint16 from pylibftdi.device import Device CTS_MASK = 1 << 4 DSR_MASK = 1 << 5 RI_MASK = 1 << 6 class SerialDevice(Device): """ simple subclass to support serial(rs232) lines cts, dsr, ri - input dtr, rts - output modem_status - return a two byte bitfield of various values Note: These lines are all active-low by default, though this can be changed in the EEPROM settings. pylibftdi does not attempt to hide these settings, and simply writes out the given values (i.e. '1' will typically make an output line 'active' - and therefore low) """ _dtr = None _rts = None @property def dtr(self): """ set (or get the previous set) state of the DTR line :return: the state of the DTR line; None if not previously set """ return self._dtr @dtr.setter def dtr(self, value): value &= 1 if value != self._dtr: self.ftdi_fn.ftdi_setdtr(value) self._dtr = value @property def rts(self): """ set (or get the previous set) state of the RTS line :return: the state of the RTS line; None if not previously set """ return self._rts @rts.setter def rts(self, value): value &= 1 if value != self._rts: self.ftdi_fn.ftdi_setrts(value) self._rts = value @property def modem_status(self): """ Layout of the first byte: B0..B3 - must be 0 B4 Clear to send (CTS) 0 = inactive 1 = active B5 Data set ready (DTS) 0 = inactive 1 = active B6 Ring indicator (RI) 0 = inactive 1 = active B7 Receive line signal detect (RLSD) 0 = inactive 1 = active Layout of the second byte: B0 Data ready (DR) B1 Overrun error (OE) B2 Parity error (PE) B3 Framing error (FE) B4 Break interrupt (BI) B5 Transmitter holding register (THRE) B6 Transmitter empty (TEMT) B7 Error in RCVR FIFO '{:016b}'.format(d.modem_status) '0110000000000001' - b5,b6 set in MSB ('2nd byte'), b0 set in first byte (despite the libftdi docs saying this shouldn't be set) """ status = c_uint16() self.ftdi_fn.ftdi_poll_modem_status(byref(status)) return status.value @property def cts(self): """ get the state of CTS (1 = 'active') """ return int(bool(self.modem_status & CTS_MASK)) @property def dsr(self): """ get the state of DSR (1 = 'active') """ return int(bool(self.modem_status & DSR_MASK)) @property def ri(self): """ get the state of RI (1 = 'active') """ return int(bool(self.modem_status & RI_MASK)) pylibftdi-0.24.1/src/pylibftdi/util.py000066400000000000000000000026161516421155500177020ustar00rootroot00000000000000""" pylibftdi - python wrapper for libftdi Copyright (c) 2010-2014 Ben Bass See LICENSE file for details and (absence of) warranty pylibftdi: https://github.com/codedstructure/pylibftdi """ # The Bus descriptor class is probably useful outside of # pylibftdi. It tries to be to Python what bitfields are # to C. Its only requirement (which is fairly pylibftdi-ish) # is a 'device' attribute on the object this is attached # to, which has a 'port' property which is readable and # writable. class Bus: """ This class is a descriptor for a bus of a given width starting at a given offset (0 = LSB). The device which does the actual reading and writing is assumed to be a BitBangDevice instance in the 'device' attribute of the object to which this is attached. """ def __init__(self, offset, width=1): self.offset = offset self.width = width self._mask = (1 << width) - 1 def __get__(self, obj, type_): val = obj.device.port return (val >> self.offset) & self._mask def __set__(self, obj, value): value = value & self._mask # in a multi-threaded environment, would # want to ensure following was locked, eg # by acquiring a device lock val = obj.device.port val &= ~(self._mask << self.offset) val |= value << self.offset obj.device.port = val pylibftdi-0.24.1/tests/000077500000000000000000000000001516421155500147335ustar00rootroot00000000000000pylibftdi-0.24.1/tests/__init__.py000066400000000000000000000001331516421155500170410ustar00rootroot00000000000000"""tests for pylibftdi""" # this file must exist for python3 -m unittest discover to work pylibftdi-0.24.1/tests/call_log.py000066400000000000000000000004701516421155500170620ustar00rootroot00000000000000from __future__ import annotations __author__ = "ben" class CallLog: fn_log: list[str] = [] @classmethod def reset(cls): cls.fn_log.clear() @classmethod def append(cls, value): cls.fn_log.append(value) @classmethod def get(cls): return cls.fn_log.copy() pylibftdi-0.24.1/tests/test_bitbang.py000066400000000000000000000121741516421155500177570ustar00rootroot00000000000000""" pylibftdi - python wrapper for libftdi Copyright (c) 2010-2014 Ben Bass See LICENSE file for details and (absence of) warranty pylibftdi: https://github.com/codedstructure/pylibftdi This module contains some basic tests for the higher-level functionality without requiring an actual hardware device to be attached. """ import unittest from pylibftdi import FtdiError from pylibftdi.bitbang import BitBangDevice from tests.test_common import CallCheckMixin, LoopDevice class LoopBitBangDevice(BitBangDevice, LoopDevice): pass BitBangDevice = LoopBitBangDevice # type: ignore # and now some test cases... class BitBangFunctions(CallCheckMixin, unittest.TestCase): def testContextManager(self): def _(): with BitBangDevice(): pass self.assertCallsExact( _, [ "ftdi_init", "ftdi_usb_open_desc_index", "ftdi_set_bitmode", "ftdi_setflowctrl", "ftdi_set_baudrate", "ftdi_set_latency_timer", "ftdi_set_bitmode", "ftdi_usb_close", "ftdi_deinit", ], ) def testOpen(self): """ check same opening things as a normal Device still work for BitBangDevice """ # a lazy_open open() shouldn't do anything self.assertCallsExact(lambda: BitBangDevice(lazy_open=True), []) # a non-lazy_open open() should open the port... self.assertCalls(BitBangDevice, "ftdi_usb_open_desc_index") # and set the bit mode self.assertCalls(BitBangDevice, "ftdi_set_bitmode") # and given a device_id, it should do a open_desc self.assertCalls(lambda: BitBangDevice("bogus"), "ftdi_usb_open_desc_index") def testInitDirection(self): # check that a direction can be given on open and is honoured for dir_test in (0, 1, 4, 12, 120, 255): dev = BitBangDevice(direction=dir_test) self.assertEqual(dev.direction, dir_test) self.assertEqual(dev._direction, dir_test) self.assertEqual(dev._last_set_dir, dir_test) # check an invalid direction on open gives error self.assertRaises(FtdiError, lambda: BitBangDevice(direction=300)) def testDirection(self): dev = BitBangDevice() # check that a direction can be given on open and is honoured for dir_test in (0, 1, 4, 12, 120, 255): def assign_dir(dir_test=dir_test): dev.direction = dir_test self.assertCalls(assign_dir, "ftdi_set_bitmode") self.assertEqual(dev.direction, dir_test) self.assertEqual(dev._direction, dir_test) self.assertEqual(dev._last_set_dir, dir_test) # check an invalid direction on open gives error def _(): # noqa dev.direction = 300 self.assertRaises(FtdiError, _) def testPort(self): dev = BitBangDevice() # check that a direction can be given on open and is honoured for port_test in (0, 1, 4, 12, 120, 255): def assign_port(port_test=port_test): dev.port = port_test self.assertCalls(assign_port, "ftdi_write_data") self.assertEqual(dev._latch, port_test) self.assertEqual(dev.port, port_test) def testBitAccess(self): dev = BitBangDevice(direction=0xF0) _ = dev.latch # absorb the first ftdi_read_pins self.assertCallsExact(lambda: dev.port | 2, ["ftdi_read_pins"]) self.assertCallsExact(lambda: dev.port & 2, ["ftdi_read_pins"]) def testLatchReadModifyWrite(self): dev = BitBangDevice(direction=0x55) self.assertCallsExact(lambda: dev.latch, ["ftdi_read_pins"]) self.assertCallsExact(lambda: dev.latch, []) def x(): dev.latch += 1 dev.latch += 1 dev.latch += 1 self.assertCallsExact( x, [ "ftdi_tcoflush", "ftdi_write_data", "ftdi_tcoflush", "ftdi_write_data", "ftdi_tcoflush", "ftdi_write_data", ], ) def testAsyncLatchReadModifyWrite(self): dev = BitBangDevice(direction=0x55, sync=False) self.assertCallsExact(lambda: dev.latch, ["ftdi_read_pins"]) self.assertCallsExact(lambda: dev.latch, []) def x(): dev.latch += 1 dev.latch += 1 dev.latch += 1 self.assertCallsExact( x, ["ftdi_write_data", "ftdi_write_data", "ftdi_write_data"] ) def testAugmentedAccess(self): dev = BitBangDevice(direction=0xAA) _ = dev.latch # absorb the first ftdi_read_pins def _1(): dev.port &= 2 def _2(): dev.port |= 2 self.assertCallsExact( _1, ["ftdi_read_pins", "ftdi_tcoflush", "ftdi_write_data"] ) self.assertCallsExact( _2, ["ftdi_read_pins", "ftdi_tcoflush", "ftdi_write_data"] ) if __name__ == "__main__": unittest.main() pylibftdi-0.24.1/tests/test_bus.py000066400000000000000000000026751516421155500171470ustar00rootroot00000000000000""" pylibftdi - python wrapper for libftdi Copyright (c) 2010-2014 Ben Bass See LICENSE file for details and (absence of) warranty pylibftdi: https://github.com/codedstructure/pylibftdi This module contains some basic tests for the higher-level functionality without requiring an actual hardware device to be attached. """ import unittest from pylibftdi.util import Bus class TestBus(unittest.TestCase): class MockDevice: port = 0 class Bus1: a = Bus(0, 2) b = Bus(2, 1) c = Bus(3, 5) def __init__(self): self.device = TestBus.MockDevice() def test_bus_write(self): test_bus = TestBus.Bus1() # test writing to the bus self.assertEqual(test_bus.device.port, 0) test_bus.a = 3 test_bus.b = 1 test_bus.c = 31 self.assertEqual(test_bus.device.port, 255) test_bus.b = 0 self.assertEqual(test_bus.device.port, 251) test_bus.c = 16 self.assertEqual(test_bus.device.port, 131) def test_bus_read(self): test_bus = TestBus.Bus1() # test reading from the bus test_bus.device.port = 0x55 assert test_bus.a == 1 assert test_bus.b == 1 assert test_bus.c == 10 test_bus.device.port = 0xAA assert test_bus.a == 2 assert test_bus.b == 0 assert test_bus.c == 21 if __name__ == "__main__": unittest.main() pylibftdi-0.24.1/tests/test_common.py000066400000000000000000000053041516421155500176360ustar00rootroot00000000000000""" pylibftdi - python wrapper for libftdi Copyright (c) 2010-2014 Ben Bass See LICENSE file for details and (absence of) warranty pylibftdi: https://github.com/codedstructure/pylibftdi This module contains some basic tests for the higher-level functionality without requiring an actual hardware device to be attached. """ import gc import logging import sys from tests.call_log import CallLog class SimpleMock: """ This is a simple mock plugin for fdll which logs any calls made through it to fn_log, which is currently rather ugly global state. """ def __init__(self, name=""): self.__name = name def __getattr__(self, key): return SimpleMock(key) def __call__(self, *o, **k): CallLog.append(self.__name) logging.debug(f"{self.__name}(*{o}, **{k})") return 0 class CallCheckMixin: """ this should be used as a mixin for unittest.TestCase classes, where it allows the calls through the MockDriver to be checked this does not support multi-threading. """ def setUp(self): super().setUp() def assertCalls(self, fn, methodname): CallLog.reset() fn() self.assertIn(methodname, CallLog.get()) def assertNotCalls(self, fn, methodname): CallLog.reset() fn() self.assertNotIn(methodname, CallLog.get()) def assertCallsExact(self, fn, call_list): # ensure any pending Device.__del__ calls get triggered before running fn gc.collect() CallLog.reset() fn() self.assertEqual(call_list, CallLog.get()) import pylibftdi.driver # noqa # monkey patch the Driver class to be the mock thing above. class MockDriver: def __init__(self, *o, **k): self.fdll = SimpleMock() def libftdi_version(self): return pylibftdi.driver.libftdi_version(1, 2, 3, 0, 0) # importing this _does_ things... pylibftdi.device.Driver = MockDriver # type: ignore from pylibftdi.device import Device # noqa class LoopDevice(Device): """ a mock device object which overrides read and write to operate as an unbounded loopback pair """ def __init__(self, *o, **k): super().__init__(*o, **k) self.__buffer = [] def _read(self, size): super()._read(size) # discard result result = bytes(bytearray(self.__buffer[:size])) self.__buffer = self.__buffer[size:] return result def _write(self, data): super()._write(data) # discard result self.__buffer.extend(bytearray(data)) return len(data) verbose = {"-v", "--verbose"} & set(sys.argv) logging.basicConfig(level=logging.DEBUG if verbose else logging.INFO) pylibftdi-0.24.1/tests/test_device.py000066400000000000000000000110251516421155500176020ustar00rootroot00000000000000""" pylibftdi - python wrapper for libftdi Copyright (c) 2010-2014 Ben Bass See LICENSE file for details and (absence of) warranty pylibftdi: https://github.com/codedstructure/pylibftdi This module contains some basic tests for the higher-level functionality without requiring an actual hardware device to be attached. """ import unittest from pylibftdi import FtdiError from pylibftdi.device import Device from tests.test_common import CallCheckMixin, LoopDevice # and now some test cases... class DeviceFunctions(CallCheckMixin, unittest.TestCase): def testContextManager(self): def _(): with Device(): pass self.assertCallsExact( _, [ "ftdi_init", "ftdi_usb_open_desc_index", "ftdi_set_bitmode", "ftdi_setflowctrl", "ftdi_set_baudrate", "ftdi_set_latency_timer", "ftdi_usb_close", "ftdi_deinit", ], ) def testOpen(self): # a lazy_open open() shouldn't do anything self.assertCallsExact(lambda: Device(lazy_open=True), []) # a non-lazy_open open() should open the port... self.assertCalls(Device, "ftdi_usb_open_desc_index") # should be the same with device_id... self.assertCalls(lambda: Device("bogus"), "ftdi_usb_open_desc_index") # should be the same with device_id... self.assertCalls(lambda: Device(device_index=2), "ftdi_usb_open_desc_index") def testOpenInterface(self): self.assertCalls(lambda: Device(interface_select=1), "ftdi_set_interface") # check that opening a specific interface does that self.assertNotCalls(Device, "ftdi_set_interface") def testReadWrite(self): with Device() as dev: self.assertCalls(lambda: dev.write("xxx"), "ftdi_write_data") self.assertCalls(lambda: dev.read(10), "ftdi_read_data") def testFlush(self): with Device() as dev: self.assertCalls(dev.flush_input, "ftdi_tciflush") self.assertCalls(dev.flush_output, "ftdi_tcoflush") self.assertCalls(dev.flush, "ftdi_tcioflush") def testClose(self): d = Device() d.close() self.assertRaises(FtdiError, d.write, "hello") d = Device() d.close() self.assertRaises(FtdiError, d.read, 1) class LoopbackTest(unittest.TestCase): """ these all require mode='t' """ def testPrint(self): d = LoopDevice(mode="t") d.write("Hello") d.write(" World\n") d.write("Bye") self.assertEqual(d.readline(), "Hello World\n") self.assertEqual(d.readline(), "Bye") def testPrintBytes(self): d = LoopDevice(mode="t") d.write(b"Hello") d.write(b" World\n") d.write(b"Bye") self.assertEqual(d.readline(), "Hello World\n") self.assertEqual(d.readline(), "Bye") def testLines(self): d = LoopDevice(mode="t") lines = ["Hello\n", "World\n", "And\n", "Goodbye\n"] d.writelines(lines) self.assertEqual(d.readlines(), lines) def testLinesBytes(self): d = LoopDevice(mode="t") lines = [b"Hello\n", b"World\n", b"And\n", b"Goodbye\n"] d.writelines(lines) self.assertEqual(d.readlines(), [str(line, "ascii") for line in lines]) def testIterate(self): d = LoopDevice(mode="t") lines = ["Hello\n", "World\n", "And\n", "Goodbye\n"] d.writelines(lines) for idx, line in enumerate(d): self.assertEqual(line, lines[idx]) def testBuffer(self): d = LoopDevice(mode="t", chunk_size=3) d.write("Hello") d.write(" World\n") d.write("Bye") self.assertEqual(d.readline(), "Hello World\n") self.assertEqual(d.readline(), "Bye") def testReadLineBytes(self): """ Device.readline() when in byte mode should raise a TypeError. This method should only be used in text mode. """ d = LoopDevice(mode="b") d.write(b"Hello\n") with self.assertRaises(TypeError): d.readline() def testReadLinesBytes(self): """ Device.readlines() when in byte mode should raise a TypeError. This method should only be used in text mode. """ d = LoopDevice(mode="b") d.write(b"Hello\n") with self.assertRaises(TypeError): d.readlines() if __name__ == "__main__": unittest.main() pylibftdi-0.24.1/tests/test_driver.py000066400000000000000000000103051516421155500176360ustar00rootroot00000000000000""" pylibftdi - python wrapper for libftdi Copyright (c) 2010-2014 Ben Bass See LICENSE file for details and (absence of) warranty pylibftdi: https://github.com/codedstructure/pylibftdi This module contains some basic tests for Driver class. """ import unittest from pylibftdi import LibraryMissingError from pylibftdi.driver import Driver class DriverTest(unittest.TestCase): """ Test to ensure the Driver class accepts the correct arguments. """ def setUp(self): """The default library names are stored in the Driver class as a class variable. This method will run before each unit test to reset the default library names. If other class variables are added to the Driver class, they should also be added here. """ Driver._lib_search = { "libftdi": ["ftdi1", "libftdi1", "ftdi", "libftdi"], "libusb": ["usb-1.0", "libusb-1.0"], } def testNoneLibrary(self): """ The Driver class can accept no library names passed in to the constructor. This uses the default libraries specified in the Driver class. This is the default and most typical behavior. """ driver = Driver(libftdi_search=None) self.assertListEqual( driver._lib_search["libftdi"], ["ftdi1", "libftdi1", "ftdi", "libftdi"] ) def testNoExplicitParameters(self): """ The Driver class can accept no explicit parameters. Ensures that libftdi_search is set to None by default. """ driver = Driver() self.assertListEqual( driver._lib_search["libftdi"], ["ftdi1", "libftdi1", "ftdi", "libftdi"] ) def testStringLibrary(self): """ The Driver class can accept a string library name and store the value in a list with a single element. You might use this when you know the exact name of the library (perhaps custom). """ driver = Driver(libftdi_search="libftdi") self.assertListEqual(driver._lib_search["libftdi"], ["libftdi"]) def testListLibrary(self): """ The Driver class can accept a list of library names and store the values in a list. You might use this to support a limited number of platforms with different library names. """ driver = Driver(libftdi_search=["ftdi1", "libftdi1"]) self.assertListEqual(driver._lib_search["libftdi"], ["ftdi1", "libftdi1"]) def testLoadLibrarySearchListEmpty(self): """ If a Driver object calls _load_library where search_list is an empty list, LibraryMissingError will be raised. """ # Use the default library names. driver = Driver(libftdi_search=None) # Try and find the library names for libftdi. with self.assertRaises(expected_exception=LibraryMissingError): driver._load_library(name="libftdi", search_list=[]) def testLoadLibraryMissingLibraryName(self): """ If a Driver object calls _load_library with with a name not in the default library names (Driver._lib_search), LibraryMissingError will be raised. """ driver = Driver(libftdi_search=None) with self.assertRaises(expected_exception=LibraryMissingError): driver._load_library(name="non-existent-library", search_list=None) def testLoadLibrarySearchListNone(self): """ If a Driver object calls _load_library with with a valid name (the key exists in Driver._lib_search) and search_list is None, the proper library will be returned. """ driver = Driver(libftdi_search=None) try: # Assert that Driver can find both of the defaults. libftdi = driver._load_library(name="libftdi", search_list=None) libusb = driver._load_library(name="libusb", search_list=None) self.assertIsNotNone(obj=libftdi, msg="libftdi library not found") self.assertIsNotNone(obj=libusb, msg="libusb library not found") except LibraryMissingError: self.fail("LibraryMissingError raised for default library names.") if __name__ == "__main__": unittest.main() pylibftdi-0.24.1/tests/test_serial.py000066400000000000000000000034631516421155500176310ustar00rootroot00000000000000""" pylibftdi - python wrapper for libftdi Copyright (c) 2010-2014 Ben Bass See LICENSE file for details and (absence of) warranty pylibftdi: https://github.com/codedstructure/pylibftdi This module contains some basic tests for the higher-level functionality without requiring an actual hardware device to be attached. """ import unittest from pylibftdi.serial_device import SerialDevice from tests.test_common import CallCheckMixin, LoopDevice class LoopSerialDevice(SerialDevice, LoopDevice): pass SerialDevice = LoopSerialDevice # type: ignore # and now some test cases... class SerialFunctions(CallCheckMixin, unittest.TestCase): def setUp(self): self.sd = SerialDevice() def _read_test(self, item): # check we ask for the modem status to get this self.assertCalls(lambda: getattr(self.sd, item), "ftdi_poll_modem_status") # check it isn't settable self.assertRaises(AttributeError, lambda: setattr(self.sd, "dsr", 1)) def _write_test(self, item): # check writes call appropriate libftdi function self.assertCalls(lambda: setattr(self.sd, item, 1), "ftdi_set" + item) # check reads don't call anything self.assertCallsExact(lambda: getattr(self.sd, item), []) def test_cts(self): """check setting and getting cts""" self._read_test("cts") def test_dsr(self): """check setting and getting dsr""" self._read_test("dsr") def test_ri(self): """check setting and getting ri""" self._read_test("ri") def test_dtr(self): """check setting and getting dtr""" self._write_test("dtr") def test_rts(self): """check setting and getting rts""" self._write_test("rts") if __name__ == "__main__": unittest.main() pylibftdi-0.24.1/uv.lock000066400000000000000000004435331516421155500151110ustar00rootroot00000000000000version = 1 revision = 3 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.12'", "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.18.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, ] [[package]] name = "certifi" version = "2026.2.25" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, ] [[package]] name = "charset-normalizer" version = "3.4.7" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/26/08/0f303cb0b529e456bb116f2d50565a482694fbb94340bf56d44677e7ed03/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d", size = 315182, upload-time = "2026-04-02T09:25:40.673Z" }, { url = "https://files.pythonhosted.org/packages/24/47/b192933e94b546f1b1fe4df9cc1f84fcdbf2359f8d1081d46dd029b50207/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8", size = 209329, upload-time = "2026-04-02T09:25:42.354Z" }, { url = "https://files.pythonhosted.org/packages/c2/b4/01fa81c5ca6141024d89a8fc15968002b71da7f825dd14113207113fabbd/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790", size = 231230, upload-time = "2026-04-02T09:25:44.281Z" }, { url = "https://files.pythonhosted.org/packages/20/f7/7b991776844dfa058017e600e6e55ff01984a063290ca5622c0b63162f68/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc", size = 225890, upload-time = "2026-04-02T09:25:45.475Z" }, { url = "https://files.pythonhosted.org/packages/20/e7/bed0024a0f4ab0c8a9c64d4445f39b30c99bd1acd228291959e3de664247/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393", size = 216930, upload-time = "2026-04-02T09:25:46.58Z" }, { url = "https://files.pythonhosted.org/packages/e2/ab/b18f0ab31cdd7b3ddb8bb76c4a414aeb8160c9810fdf1bc62f269a539d87/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153", size = 202109, upload-time = "2026-04-02T09:25:48.031Z" }, { url = "https://files.pythonhosted.org/packages/82/e5/7e9440768a06dfb3075936490cb82dbf0ee20a133bf0dd8551fa096914ec/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af", size = 214684, upload-time = "2026-04-02T09:25:49.245Z" }, { url = "https://files.pythonhosted.org/packages/71/94/8c61d8da9f062fdf457c80acfa25060ec22bf1d34bbeaca4350f13bcfd07/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34", size = 212785, upload-time = "2026-04-02T09:25:50.671Z" }, { url = "https://files.pythonhosted.org/packages/66/cd/6e9889c648e72c0ab2e5967528bb83508f354d706637bc7097190c874e13/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1", size = 203055, upload-time = "2026-04-02T09:25:51.802Z" }, { url = "https://files.pythonhosted.org/packages/92/2e/7a951d6a08aefb7eb8e1b54cdfb580b1365afdd9dd484dc4bee9e5d8f258/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752", size = 232502, upload-time = "2026-04-02T09:25:53.388Z" }, { url = "https://files.pythonhosted.org/packages/58/d5/abcf2d83bf8e0a1286df55cd0dc1d49af0da4282aa77e986df343e7de124/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53", size = 214295, upload-time = "2026-04-02T09:25:54.765Z" }, { url = "https://files.pythonhosted.org/packages/47/3a/7d4cd7ed54be99973a0dc176032cba5cb1f258082c31fa6df35cff46acfc/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616", size = 227145, upload-time = "2026-04-02T09:25:55.904Z" }, { url = "https://files.pythonhosted.org/packages/1d/98/3a45bf8247889cf28262ebd3d0872edff11565b2a1e3064ccb132db3fbb0/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a", size = 218884, upload-time = "2026-04-02T09:25:57.074Z" }, { url = "https://files.pythonhosted.org/packages/ad/80/2e8b7f8915ed5c9ef13aa828d82738e33888c485b65ebf744d615040c7ea/charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374", size = 148343, upload-time = "2026-04-02T09:25:58.199Z" }, { url = "https://files.pythonhosted.org/packages/35/1b/3b8c8c77184af465ee9ad88b5aea46ea6b2e1f7b9dc9502891e37af21e30/charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943", size = 159174, upload-time = "2026-04-02T09:25:59.322Z" }, { url = "https://files.pythonhosted.org/packages/be/c1/feb40dca40dbb21e0a908801782d9288c64fc8d8e562c2098e9994c8c21b/charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008", size = 147805, upload-time = "2026-04-02T09:26:00.756Z" }, { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, ] [[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" } resolution-markers = [ "python_full_version < '3.11'", ] 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 = "docutils" version = "0.22.4" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.12'", "python_full_version == '3.11.*'", ] sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, ] [[package]] name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, ] [[package]] name = "idna" version = "3.11" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] [[package]] name = "imagesize" version = "2.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/6c/e6/7bf14eeb8f8b7251141944835abd42eb20a658d89084b7e1f3e5fe394090/imagesize-2.0.0.tar.gz", hash = "sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3", size = 1773045, upload-time = "2026-03-03T14:18:29.941Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl", hash = "sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96", size = 9441, upload-time = "2026-03-03T14:18:27.892Z" }, ] [[package]] name = "iniconfig" version = "2.3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] [[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 = "librt" version = "0.8.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/56/9c/b4b0c54d84da4a94b37bd44151e46d5e583c9534c7e02250b961b1b6d8a8/librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73", size = 177471, upload-time = "2026-02-17T16:13:06.101Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7c/5f/63f5fa395c7a8a93558c0904ba8f1c8d1b997ca6a3de61bc7659970d66bf/librt-0.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:81fd938344fecb9373ba1b155968c8a329491d2ce38e7ddb76f30ffb938f12dc", size = 65697, upload-time = "2026-02-17T16:11:06.903Z" }, { url = "https://files.pythonhosted.org/packages/ff/e0/0472cf37267b5920eff2f292ccfaede1886288ce35b7f3203d8de00abfe6/librt-0.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5db05697c82b3a2ec53f6e72b2ed373132b0c2e05135f0696784e97d7f5d48e7", size = 68376, upload-time = "2026-02-17T16:11:08.395Z" }, { url = "https://files.pythonhosted.org/packages/c8/be/8bd1359fdcd27ab897cd5963294fa4a7c83b20a8564678e4fd12157e56a5/librt-0.8.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d56bc4011975f7460bea7b33e1ff425d2f1adf419935ff6707273c77f8a4ada6", size = 197084, upload-time = "2026-02-17T16:11:09.774Z" }, { url = "https://files.pythonhosted.org/packages/e2/fe/163e33fdd091d0c2b102f8a60cc0a61fd730ad44e32617cd161e7cd67a01/librt-0.8.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdc0f588ff4b663ea96c26d2a230c525c6fc62b28314edaaaca8ed5af931ad0", size = 207337, upload-time = "2026-02-17T16:11:11.311Z" }, { url = "https://files.pythonhosted.org/packages/01/99/f85130582f05dcf0c8902f3d629270231d2f4afdfc567f8305a952ac7f14/librt-0.8.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:97c2b54ff6717a7a563b72627990bec60d8029df17df423f0ed37d56a17a176b", size = 219980, upload-time = "2026-02-17T16:11:12.499Z" }, { url = "https://files.pythonhosted.org/packages/6f/54/cb5e4d03659e043a26c74e08206412ac9a3742f0477d96f9761a55313b5f/librt-0.8.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f1125e6bbf2f1657d9a2f3ccc4a2c9b0c8b176965bb565dd4d86be67eddb4b6", size = 212921, upload-time = "2026-02-17T16:11:14.484Z" }, { url = "https://files.pythonhosted.org/packages/b1/81/a3a01e4240579c30f3487f6fed01eb4bc8ef0616da5b4ebac27ca19775f3/librt-0.8.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8f4bb453f408137d7581be309b2fbc6868a80e7ef60c88e689078ee3a296ae71", size = 221381, upload-time = "2026-02-17T16:11:17.459Z" }, { url = "https://files.pythonhosted.org/packages/08/b0/fc2d54b4b1c6fb81e77288ff31ff25a2c1e62eaef4424a984f228839717b/librt-0.8.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c336d61d2fe74a3195edc1646d53ff1cddd3a9600b09fa6ab75e5514ba4862a7", size = 216714, upload-time = "2026-02-17T16:11:19.197Z" }, { url = "https://files.pythonhosted.org/packages/96/96/85daa73ffbd87e1fb287d7af6553ada66bf25a2a6b0de4764344a05469f6/librt-0.8.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:eb5656019db7c4deacf0c1a55a898c5bb8f989be904597fcb5232a2f4828fa05", size = 214777, upload-time = "2026-02-17T16:11:20.443Z" }, { url = "https://files.pythonhosted.org/packages/12/9c/c3aa7a2360383f4bf4f04d98195f2739a579128720c603f4807f006a4225/librt-0.8.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c25d9e338d5bed46c1632f851babf3d13c78f49a225462017cf5e11e845c5891", size = 237398, upload-time = "2026-02-17T16:11:22.083Z" }, { url = "https://files.pythonhosted.org/packages/61/19/d350ea89e5274665185dabc4bbb9c3536c3411f862881d316c8b8e00eb66/librt-0.8.1-cp310-cp310-win32.whl", hash = "sha256:aaab0e307e344cb28d800957ef3ec16605146ef0e59e059a60a176d19543d1b7", size = 54285, upload-time = "2026-02-17T16:11:23.27Z" }, { url = "https://files.pythonhosted.org/packages/4f/d6/45d587d3d41c112e9543a0093d883eb57a24a03e41561c127818aa2a6bcc/librt-0.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:56e04c14b696300d47b3bc5f1d10a00e86ae978886d0cee14e5714fafb5df5d2", size = 61352, upload-time = "2026-02-17T16:11:24.207Z" }, { url = "https://files.pythonhosted.org/packages/1d/01/0e748af5e4fee180cf7cd12bd12b0513ad23b045dccb2a83191bde82d168/librt-0.8.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:681dc2451d6d846794a828c16c22dc452d924e9f700a485b7ecb887a30aad1fd", size = 65315, upload-time = "2026-02-17T16:11:25.152Z" }, { url = "https://files.pythonhosted.org/packages/9d/4d/7184806efda571887c798d573ca4134c80ac8642dcdd32f12c31b939c595/librt-0.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3b4350b13cc0e6f5bec8fa7caf29a8fb8cdc051a3bae45cfbfd7ce64f009965", size = 68021, upload-time = "2026-02-17T16:11:26.129Z" }, { url = "https://files.pythonhosted.org/packages/ae/88/c3c52d2a5d5101f28d3dc89298444626e7874aa904eed498464c2af17627/librt-0.8.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ac1e7817fd0ed3d14fd7c5df91daed84c48e4c2a11ee99c0547f9f62fdae13da", size = 194500, upload-time = "2026-02-17T16:11:27.177Z" }, { url = "https://files.pythonhosted.org/packages/d6/5d/6fb0a25b6a8906e85b2c3b87bee1d6ed31510be7605b06772f9374ca5cb3/librt-0.8.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:747328be0c5b7075cde86a0e09d7a9196029800ba75a1689332348e998fb85c0", size = 205622, upload-time = "2026-02-17T16:11:28.242Z" }, { url = "https://files.pythonhosted.org/packages/b2/a6/8006ae81227105476a45691f5831499e4d936b1c049b0c1feb17c11b02d1/librt-0.8.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0af2bd2bc204fa27f3d6711d0f360e6b8c684a035206257a81673ab924aa11e", size = 218304, upload-time = "2026-02-17T16:11:29.344Z" }, { url = "https://files.pythonhosted.org/packages/ee/19/60e07886ad16670aae57ef44dada41912c90906a6fe9f2b9abac21374748/librt-0.8.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d480de377f5b687b6b1bc0c0407426da556e2a757633cc7e4d2e1a057aa688f3", size = 211493, upload-time = "2026-02-17T16:11:30.445Z" }, { url = "https://files.pythonhosted.org/packages/9c/cf/f666c89d0e861d05600438213feeb818c7514d3315bae3648b1fc145d2b6/librt-0.8.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d0ee06b5b5291f609ddb37b9750985b27bc567791bc87c76a569b3feed8481ac", size = 219129, upload-time = "2026-02-17T16:11:32.021Z" }, { url = "https://files.pythonhosted.org/packages/8f/ef/f1bea01e40b4a879364c031476c82a0dc69ce068daad67ab96302fed2d45/librt-0.8.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9e2c6f77b9ad48ce5603b83b7da9ee3e36b3ab425353f695cba13200c5d96596", size = 213113, upload-time = "2026-02-17T16:11:33.192Z" }, { url = "https://files.pythonhosted.org/packages/9b/80/cdab544370cc6bc1b72ea369525f547a59e6938ef6863a11ab3cd24759af/librt-0.8.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:439352ba9373f11cb8e1933da194dcc6206daf779ff8df0ed69c5e39113e6a99", size = 212269, upload-time = "2026-02-17T16:11:34.373Z" }, { url = "https://files.pythonhosted.org/packages/9d/9c/48d6ed8dac595654f15eceab2035131c136d1ae9a1e3548e777bb6dbb95d/librt-0.8.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:82210adabbc331dbb65d7868b105185464ef13f56f7f76688565ad79f648b0fe", size = 234673, upload-time = "2026-02-17T16:11:36.063Z" }, { url = "https://files.pythonhosted.org/packages/16/01/35b68b1db517f27a01be4467593292eb5315def8900afad29fabf56304ba/librt-0.8.1-cp311-cp311-win32.whl", hash = "sha256:52c224e14614b750c0a6d97368e16804a98c684657c7518752c356834fff83bb", size = 54597, upload-time = "2026-02-17T16:11:37.544Z" }, { url = "https://files.pythonhosted.org/packages/71/02/796fe8f02822235966693f257bf2c79f40e11337337a657a8cfebba5febc/librt-0.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:c00e5c884f528c9932d278d5c9cbbea38a6b81eb62c02e06ae53751a83a4d52b", size = 61733, upload-time = "2026-02-17T16:11:38.691Z" }, { url = "https://files.pythonhosted.org/packages/28/ad/232e13d61f879a42a4e7117d65e4984bb28371a34bb6fb9ca54ec2c8f54e/librt-0.8.1-cp311-cp311-win_arm64.whl", hash = "sha256:f7cdf7f26c2286ffb02e46d7bac56c94655540b26347673bea15fa52a6af17e9", size = 52273, upload-time = "2026-02-17T16:11:40.308Z" }, { url = "https://files.pythonhosted.org/packages/95/21/d39b0a87ac52fc98f621fb6f8060efb017a767ebbbac2f99fbcbc9ddc0d7/librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a", size = 66516, upload-time = "2026-02-17T16:11:41.604Z" }, { url = "https://files.pythonhosted.org/packages/69/f1/46375e71441c43e8ae335905e069f1c54febee63a146278bcee8782c84fd/librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9", size = 68634, upload-time = "2026-02-17T16:11:43.268Z" }, { url = "https://files.pythonhosted.org/packages/0a/33/c510de7f93bf1fa19e13423a606d8189a02624a800710f6e6a0a0f0784b3/librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb", size = 198941, upload-time = "2026-02-17T16:11:44.28Z" }, { url = "https://files.pythonhosted.org/packages/dd/36/e725903416409a533d92398e88ce665476f275081d0d7d42f9c4951999e5/librt-0.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d", size = 209991, upload-time = "2026-02-17T16:11:45.462Z" }, { url = "https://files.pythonhosted.org/packages/30/7a/8d908a152e1875c9f8eac96c97a480df425e657cdb47854b9efaa4998889/librt-0.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7", size = 224476, upload-time = "2026-02-17T16:11:46.542Z" }, { url = "https://files.pythonhosted.org/packages/a8/b8/a22c34f2c485b8903a06f3fe3315341fe6876ef3599792344669db98fcff/librt-0.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440", size = 217518, upload-time = "2026-02-17T16:11:47.746Z" }, { url = "https://files.pythonhosted.org/packages/79/6f/5c6fea00357e4f82ba44f81dbfb027921f1ab10e320d4a64e1c408d035d9/librt-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9", size = 225116, upload-time = "2026-02-17T16:11:49.298Z" }, { url = "https://files.pythonhosted.org/packages/f2/a0/95ced4e7b1267fe1e2720a111685bcddf0e781f7e9e0ce59d751c44dcfe5/librt-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972", size = 217751, upload-time = "2026-02-17T16:11:50.49Z" }, { url = "https://files.pythonhosted.org/packages/93/c2/0517281cb4d4101c27ab59472924e67f55e375bc46bedae94ac6dc6e1902/librt-0.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921", size = 218378, upload-time = "2026-02-17T16:11:51.783Z" }, { url = "https://files.pythonhosted.org/packages/43/e8/37b3ac108e8976888e559a7b227d0ceac03c384cfd3e7a1c2ee248dbae79/librt-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0", size = 241199, upload-time = "2026-02-17T16:11:53.561Z" }, { url = "https://files.pythonhosted.org/packages/4b/5b/35812d041c53967fedf551a39399271bbe4257e681236a2cf1a69c8e7fa1/librt-0.8.1-cp312-cp312-win32.whl", hash = "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a", size = 54917, upload-time = "2026-02-17T16:11:54.758Z" }, { url = "https://files.pythonhosted.org/packages/de/d1/fa5d5331b862b9775aaf2a100f5ef86854e5d4407f71bddf102f4421e034/librt-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444", size = 62017, upload-time = "2026-02-17T16:11:55.748Z" }, { url = "https://files.pythonhosted.org/packages/c7/7c/c614252f9acda59b01a66e2ddfd243ed1c7e1deab0293332dfbccf862808/librt-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d", size = 52441, upload-time = "2026-02-17T16:11:56.801Z" }, { url = "https://files.pythonhosted.org/packages/c5/3c/f614c8e4eaac7cbf2bbdf9528790b21d89e277ee20d57dc6e559c626105f/librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35", size = 66529, upload-time = "2026-02-17T16:11:57.809Z" }, { url = "https://files.pythonhosted.org/packages/ab/96/5836544a45100ae411eda07d29e3d99448e5258b6e9c8059deb92945f5c2/librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583", size = 68669, upload-time = "2026-02-17T16:11:58.843Z" }, { url = "https://files.pythonhosted.org/packages/06/53/f0b992b57af6d5531bf4677d75c44f095f2366a1741fb695ee462ae04b05/librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c", size = 199279, upload-time = "2026-02-17T16:11:59.862Z" }, { url = "https://files.pythonhosted.org/packages/f3/ad/4848cc16e268d14280d8168aee4f31cea92bbd2b79ce33d3e166f2b4e4fc/librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04", size = 210288, upload-time = "2026-02-17T16:12:00.954Z" }, { url = "https://files.pythonhosted.org/packages/52/05/27fdc2e95de26273d83b96742d8d3b7345f2ea2bdbd2405cc504644f2096/librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363", size = 224809, upload-time = "2026-02-17T16:12:02.108Z" }, { url = "https://files.pythonhosted.org/packages/7a/d0/78200a45ba3240cb042bc597d6f2accba9193a2c57d0356268cbbe2d0925/librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0", size = 218075, upload-time = "2026-02-17T16:12:03.631Z" }, { url = "https://files.pythonhosted.org/packages/af/72/a210839fa74c90474897124c064ffca07f8d4b347b6574d309686aae7ca6/librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012", size = 225486, upload-time = "2026-02-17T16:12:04.725Z" }, { url = "https://files.pythonhosted.org/packages/a3/c1/a03cc63722339ddbf087485f253493e2b013039f5b707e8e6016141130fa/librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb", size = 218219, upload-time = "2026-02-17T16:12:05.828Z" }, { url = "https://files.pythonhosted.org/packages/58/f5/fff6108af0acf941c6f274a946aea0e484bd10cd2dc37610287ce49388c5/librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b", size = 218750, upload-time = "2026-02-17T16:12:07.09Z" }, { url = "https://files.pythonhosted.org/packages/71/67/5a387bfef30ec1e4b4f30562c8586566faf87e47d696768c19feb49e3646/librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d", size = 241624, upload-time = "2026-02-17T16:12:08.43Z" }, { url = "https://files.pythonhosted.org/packages/d4/be/24f8502db11d405232ac1162eb98069ca49c3306c1d75c6ccc61d9af8789/librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a", size = 54969, upload-time = "2026-02-17T16:12:09.633Z" }, { url = "https://files.pythonhosted.org/packages/5c/73/c9fdf6cb2a529c1a092ce769a12d88c8cca991194dfe641b6af12fa964d2/librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79", size = 62000, upload-time = "2026-02-17T16:12:10.632Z" }, { url = "https://files.pythonhosted.org/packages/d3/97/68f80ca3ac4924f250cdfa6e20142a803e5e50fca96ef5148c52ee8c10ea/librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0", size = 52495, upload-time = "2026-02-17T16:12:11.633Z" }, { url = "https://files.pythonhosted.org/packages/c9/6a/907ef6800f7bca71b525a05f1839b21f708c09043b1c6aa77b6b827b3996/librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f", size = 66081, upload-time = "2026-02-17T16:12:12.766Z" }, { url = "https://files.pythonhosted.org/packages/1b/18/25e991cd5640c9fb0f8d91b18797b29066b792f17bf8493da183bf5caabe/librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c", size = 68309, upload-time = "2026-02-17T16:12:13.756Z" }, { url = "https://files.pythonhosted.org/packages/a4/36/46820d03f058cfb5a9de5940640ba03165ed8aded69e0733c417bb04df34/librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc", size = 196804, upload-time = "2026-02-17T16:12:14.818Z" }, { url = "https://files.pythonhosted.org/packages/59/18/5dd0d3b87b8ff9c061849fbdb347758d1f724b9a82241aa908e0ec54ccd0/librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c", size = 206907, upload-time = "2026-02-17T16:12:16.513Z" }, { url = "https://files.pythonhosted.org/packages/d1/96/ef04902aad1424fd7299b62d1890e803e6ab4018c3044dca5922319c4b97/librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3", size = 221217, upload-time = "2026-02-17T16:12:17.906Z" }, { url = "https://files.pythonhosted.org/packages/6d/ff/7e01f2dda84a8f5d280637a2e5827210a8acca9a567a54507ef1c75b342d/librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14", size = 214622, upload-time = "2026-02-17T16:12:19.108Z" }, { url = "https://files.pythonhosted.org/packages/1e/8c/5b093d08a13946034fed57619742f790faf77058558b14ca36a6e331161e/librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7", size = 221987, upload-time = "2026-02-17T16:12:20.331Z" }, { url = "https://files.pythonhosted.org/packages/d3/cc/86b0b3b151d40920ad45a94ce0171dec1aebba8a9d72bb3fa00c73ab25dd/librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6", size = 215132, upload-time = "2026-02-17T16:12:21.54Z" }, { url = "https://files.pythonhosted.org/packages/fc/be/8588164a46edf1e69858d952654e216a9a91174688eeefb9efbb38a9c799/librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071", size = 215195, upload-time = "2026-02-17T16:12:23.073Z" }, { url = "https://files.pythonhosted.org/packages/f5/f2/0b9279bea735c734d69344ecfe056c1ba211694a72df10f568745c899c76/librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78", size = 237946, upload-time = "2026-02-17T16:12:24.275Z" }, { url = "https://files.pythonhosted.org/packages/e9/cc/5f2a34fbc8aeb35314a3641f9956fa9051a947424652fad9882be7a97949/librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023", size = 50689, upload-time = "2026-02-17T16:12:25.766Z" }, { url = "https://files.pythonhosted.org/packages/a0/76/cd4d010ab2147339ca2b93e959c3686e964edc6de66ddacc935c325883d7/librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730", size = 57875, upload-time = "2026-02-17T16:12:27.465Z" }, { url = "https://files.pythonhosted.org/packages/84/0f/2143cb3c3ca48bd3379dcd11817163ca50781927c4537345d608b5045998/librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3", size = 48058, upload-time = "2026-02-17T16:12:28.556Z" }, { url = "https://files.pythonhosted.org/packages/d2/0e/9b23a87e37baf00311c3efe6b48d6b6c168c29902dfc3f04c338372fd7db/librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1", size = 68313, upload-time = "2026-02-17T16:12:29.659Z" }, { url = "https://files.pythonhosted.org/packages/db/9a/859c41e5a4f1c84200a7d2b92f586aa27133c8243b6cac9926f6e54d01b9/librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee", size = 70994, upload-time = "2026-02-17T16:12:31.516Z" }, { url = "https://files.pythonhosted.org/packages/4c/28/10605366ee599ed34223ac2bf66404c6fb59399f47108215d16d5ad751a8/librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7", size = 220770, upload-time = "2026-02-17T16:12:33.294Z" }, { url = "https://files.pythonhosted.org/packages/af/8d/16ed8fd452dafae9c48d17a6bc1ee3e818fd40ef718d149a8eff2c9f4ea2/librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040", size = 235409, upload-time = "2026-02-17T16:12:35.443Z" }, { url = "https://files.pythonhosted.org/packages/89/1b/7bdf3e49349c134b25db816e4a3db6b94a47ac69d7d46b1e682c2c4949be/librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e", size = 246473, upload-time = "2026-02-17T16:12:36.656Z" }, { url = "https://files.pythonhosted.org/packages/4e/8a/91fab8e4fd2a24930a17188c7af5380eb27b203d72101c9cc000dbdfd95a/librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732", size = 238866, upload-time = "2026-02-17T16:12:37.849Z" }, { url = "https://files.pythonhosted.org/packages/b9/e0/c45a098843fc7c07e18a7f8a24ca8496aecbf7bdcd54980c6ca1aaa79a8e/librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624", size = 250248, upload-time = "2026-02-17T16:12:39.445Z" }, { url = "https://files.pythonhosted.org/packages/82/30/07627de23036640c952cce0c1fe78972e77d7d2f8fd54fa5ef4554ff4a56/librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4", size = 240629, upload-time = "2026-02-17T16:12:40.889Z" }, { url = "https://files.pythonhosted.org/packages/fb/c1/55bfe1ee3542eba055616f9098eaf6eddb966efb0ca0f44eaa4aba327307/librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382", size = 239615, upload-time = "2026-02-17T16:12:42.446Z" }, { url = "https://files.pythonhosted.org/packages/2b/39/191d3d28abc26c9099b19852e6c99f7f6d400b82fa5a4e80291bd3803e19/librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994", size = 263001, upload-time = "2026-02-17T16:12:43.627Z" }, { url = "https://files.pythonhosted.org/packages/b9/eb/7697f60fbe7042ab4e88f4ee6af496b7f222fffb0a4e3593ef1f29f81652/librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a", size = 51328, upload-time = "2026-02-17T16:12:45.148Z" }, { url = "https://files.pythonhosted.org/packages/7c/72/34bf2eb7a15414a23e5e70ecb9440c1d3179f393d9349338a91e2781c0fb/librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4", size = 58722, upload-time = "2026-02-17T16:12:46.85Z" }, { url = "https://files.pythonhosted.org/packages/b2/c8/d148e041732d631fc76036f8b30fae4e77b027a1e95b7a84bb522481a940/librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61", size = 48755, upload-time = "2026-02-17T16:12:47.943Z" }, ] [[package]] name = "markupsafe" version = "3.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] [[package]] name = "mypy" version = "1.20.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, { name = "mypy-extensions" }, { name = "pathspec" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f8/5c/b0089fe7fef0a994ae5ee07029ced0526082c6cfaaa4c10d40a10e33b097/mypy-1.20.0.tar.gz", hash = "sha256:eb96c84efcc33f0b5e0e04beacf00129dd963b67226b01c00b9dfc8affb464c3", size = 3815028, upload-time = "2026-03-31T16:55:14.959Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/4d/a2/a965c8c3fcd4fa8b84ba0d46606181b0d0a1d50f274c67877f3e9ed4882c/mypy-1.20.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d99f515f95fd03a90875fdb2cca12ff074aa04490db4d190905851bdf8a549a8", size = 14430138, upload-time = "2026-03-31T16:52:37.843Z" }, { url = "https://files.pythonhosted.org/packages/53/6e/043477501deeb8eabbab7f1a2f6cac62cfb631806dc1d6862a04a7f5011b/mypy-1.20.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bd0212976dc57a5bfeede7c219e7cd66568a32c05c9129686dd487c059c1b88a", size = 13311282, upload-time = "2026-03-31T16:55:11.021Z" }, { url = "https://files.pythonhosted.org/packages/65/aa/bd89b247b83128197a214f29f0632ff3c14f54d4cd70d144d157bd7d7d6e/mypy-1.20.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f8426d4d75d68714abc17a4292d922f6ba2cfb984b72c2278c437f6dae797865", size = 13750889, upload-time = "2026-03-31T16:52:02.909Z" }, { url = "https://files.pythonhosted.org/packages/fa/9d/2860be7355c45247ccc0be1501c91176318964c2a137bd4743f58ce6200e/mypy-1.20.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02cca0761c75b42a20a2757ae58713276605eb29a08dd8a6e092aa347c4115ca", size = 14619788, upload-time = "2026-03-31T16:50:48.928Z" }, { url = "https://files.pythonhosted.org/packages/75/7f/3ef3e360c91f3de120f205c8ce405e9caf9fc52ef14b65d37073e322c114/mypy-1.20.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b3a49064504be59e59da664c5e149edc1f26c67c4f8e8456f6ba6aba55033018", size = 14918849, upload-time = "2026-03-31T16:51:10.478Z" }, { url = "https://files.pythonhosted.org/packages/ae/72/af970dfe167ef788df7c5e6109d2ed0229f164432ce828bc9741a4250e64/mypy-1.20.0-cp310-cp310-win_amd64.whl", hash = "sha256:ebea00201737ad4391142808ed16e875add5c17f676e0912b387739f84991e13", size = 10822007, upload-time = "2026-03-31T16:50:25.268Z" }, { url = "https://files.pythonhosted.org/packages/93/94/ba9065c2ebe5421619aff684b793d953e438a8bfe31a320dd6d1e0706e81/mypy-1.20.0-cp310-cp310-win_arm64.whl", hash = "sha256:e80cf77847d0d3e6e3111b7b25db32a7f8762fd4b9a3a72ce53fe16a2863b281", size = 9756158, upload-time = "2026-03-31T16:48:36.213Z" }, { url = "https://files.pythonhosted.org/packages/6e/1c/74cb1d9993236910286865679d1c616b136b2eae468493aa939431eda410/mypy-1.20.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4525e7010b1b38334516181c5b81e16180b8e149e6684cee5a727c78186b4e3b", size = 14343972, upload-time = "2026-03-31T16:49:04.887Z" }, { url = "https://files.pythonhosted.org/packages/d5/0d/01399515eca280386e308cf57901e68d3a52af18691941b773b3380c1df8/mypy-1.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a17c5d0bdcca61ce24a35beb828a2d0d323d3fcf387d7512206888c900193367", size = 13225007, upload-time = "2026-03-31T16:50:08.151Z" }, { url = "https://files.pythonhosted.org/packages/56/ac/b4ba5094fb2d7fe9d2037cd8d18bbe02bcf68fd22ab9ff013f55e57ba095/mypy-1.20.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f75ff57defcd0f1d6e006d721ccdec6c88d4f6a7816eb92f1c4890d979d9ee62", size = 13663752, upload-time = "2026-03-31T16:49:26.064Z" }, { url = "https://files.pythonhosted.org/packages/db/a7/460678d3cf7da252d2288dad0c602294b6ec22a91932ec368cc11e44bb6e/mypy-1.20.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b503ab55a836136b619b5fc21c8803d810c5b87551af8600b72eecafb0059cb0", size = 14532265, upload-time = "2026-03-31T16:53:55.077Z" }, { url = "https://files.pythonhosted.org/packages/a3/3e/051cca8166cf0438ae3ea80e0e7c030d7a8ab98dffc93f80a1aa3f23c1a2/mypy-1.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1973868d2adbb4584a3835780b27436f06d1dc606af5be09f187aaa25be1070f", size = 14768476, upload-time = "2026-03-31T16:50:34.587Z" }, { url = "https://files.pythonhosted.org/packages/be/66/8e02ec184f852ed5c4abb805583305db475930854e09964b55e107cdcbc4/mypy-1.20.0-cp311-cp311-win_amd64.whl", hash = "sha256:2fcedb16d456106e545b2bfd7ef9d24e70b38ec252d2a629823a4d07ebcdb69e", size = 10818226, upload-time = "2026-03-31T16:53:15.624Z" }, { url = "https://files.pythonhosted.org/packages/13/4b/383ad1924b28f41e4879a74151e7a5451123330d45652da359f9183bcd45/mypy-1.20.0-cp311-cp311-win_arm64.whl", hash = "sha256:379edf079ce44ac8d2805bcf9b3dd7340d4f97aad3a5e0ebabbf9d125b84b442", size = 9750091, upload-time = "2026-03-31T16:54:12.162Z" }, { url = "https://files.pythonhosted.org/packages/be/dd/3afa29b58c2e57c79116ed55d700721c3c3b15955e2b6251dd165d377c0e/mypy-1.20.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:002b613ae19f4ac7d18b7e168ffe1cb9013b37c57f7411984abbd3b817b0a214", size = 14509525, upload-time = "2026-03-31T16:55:01.824Z" }, { url = "https://files.pythonhosted.org/packages/54/eb/227b516ab8cad9f2a13c5e7a98d28cd6aa75e9c83e82776ae6c1c4c046c7/mypy-1.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9336b5e6712f4adaf5afc3203a99a40b379049104349d747eb3e5a3aa23ac2e", size = 13326469, upload-time = "2026-03-31T16:51:41.23Z" }, { url = "https://files.pythonhosted.org/packages/57/d4/1ddb799860c1b5ac6117ec307b965f65deeb47044395ff01ab793248a591/mypy-1.20.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f13b3e41bce9d257eded794c0f12878af3129d80aacd8a3ee0dee51f3a978651", size = 13705953, upload-time = "2026-03-31T16:48:55.69Z" }, { url = "https://files.pythonhosted.org/packages/c5/b7/54a720f565a87b893182a2a393370289ae7149e4715859e10e1c05e49154/mypy-1.20.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9804c3ad27f78e54e58b32e7cb532d128b43dbfb9f3f9f06262b821a0f6bd3f5", size = 14710363, upload-time = "2026-03-31T16:53:26.948Z" }, { url = "https://files.pythonhosted.org/packages/b2/2a/74810274848d061f8a8ea4ac23aaad43bd3d8c1882457999c2e568341c57/mypy-1.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:697f102c5c1d526bdd761a69f17c6070f9892eebcb94b1a5963d679288c09e78", size = 14947005, upload-time = "2026-03-31T16:50:17.591Z" }, { url = "https://files.pythonhosted.org/packages/77/91/21b8ba75f958bcda75690951ce6fa6b7138b03471618959529d74b8544e2/mypy-1.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:0ecd63f75fdd30327e4ad8b5704bd6d91fc6c1b2e029f8ee14705e1207212489", size = 10880616, upload-time = "2026-03-31T16:52:19.986Z" }, { url = "https://files.pythonhosted.org/packages/8a/15/3d8198ef97c1ca03aea010cce4f1d4f3bc5d9849e8c0140111ca2ead9fdd/mypy-1.20.0-cp312-cp312-win_arm64.whl", hash = "sha256:f194db59657c58593a3c47c6dfd7bad4ef4ac12dbc94d01b3a95521f78177e33", size = 9813091, upload-time = "2026-03-31T16:53:44.385Z" }, { url = "https://files.pythonhosted.org/packages/d6/a7/f64ea7bd592fa431cb597418b6dec4a47f7d0c36325fec7ac67bc8402b94/mypy-1.20.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b20c8b0fd5877abdf402e79a3af987053de07e6fb208c18df6659f708b535134", size = 14485344, upload-time = "2026-03-31T16:49:16.78Z" }, { url = "https://files.pythonhosted.org/packages/bb/72/8927d84cfc90c6abea6e96663576e2e417589347eb538749a464c4c218a0/mypy-1.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:367e5c993ba34d5054d11937d0485ad6dfc60ba760fa326c01090fc256adf15c", size = 13327400, upload-time = "2026-03-31T16:53:08.02Z" }, { url = "https://files.pythonhosted.org/packages/ab/4a/11ab99f9afa41aa350178d24a7d2da17043228ea10f6456523f64b5a6cf6/mypy-1.20.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f799d9db89fc00446f03281f84a221e50018fc40113a3ba9864b132895619ebe", size = 13706384, upload-time = "2026-03-31T16:52:28.577Z" }, { url = "https://files.pythonhosted.org/packages/42/79/694ca73979cfb3535ebfe78733844cd5aff2e63304f59bf90585110d975a/mypy-1.20.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:555658c611099455b2da507582ea20d2043dfdfe7f5ad0add472b1c6238b433f", size = 14700378, upload-time = "2026-03-31T16:48:45.527Z" }, { url = "https://files.pythonhosted.org/packages/84/24/a022ccab3a46e3d2cdf2e0e260648633640eb396c7e75d5a42818a8d3971/mypy-1.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:efe8d70949c3023698c3fca1e94527e7e790a361ab8116f90d11221421cd8726", size = 14932170, upload-time = "2026-03-31T16:49:36.038Z" }, { url = "https://files.pythonhosted.org/packages/d8/9b/549228d88f574d04117e736f55958bd4908f980f9f5700a07aeb85df005b/mypy-1.20.0-cp313-cp313-win_amd64.whl", hash = "sha256:f49590891d2c2f8a9de15614e32e459a794bcba84693c2394291a2038bbaaa69", size = 10888526, upload-time = "2026-03-31T16:50:59.827Z" }, { url = "https://files.pythonhosted.org/packages/91/17/15095c0e54a8bc04d22d4ff06b2139d5f142c2e87520b4e39010c4862771/mypy-1.20.0-cp313-cp313-win_arm64.whl", hash = "sha256:76a70bf840495729be47510856b978f1b0ec7d08f257ca38c9d932720bf6b43e", size = 9816456, upload-time = "2026-03-31T16:49:59.537Z" }, { url = "https://files.pythonhosted.org/packages/4e/0e/6ca4a84cbed9e62384bc0b2974c90395ece5ed672393e553996501625fc5/mypy-1.20.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0f42dfaab7ec1baff3b383ad7af562ab0de573c5f6edb44b2dab016082b89948", size = 14483331, upload-time = "2026-03-31T16:52:57.999Z" }, { url = "https://files.pythonhosted.org/packages/7d/c5/5fe9d8a729dd9605064691816243ae6c49fde0bd28f6e5e17f6a24203c43/mypy-1.20.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:31b5dbb55293c1bd27c0fc813a0d2bb5ceef9d65ac5afa2e58f829dab7921fd5", size = 13342047, upload-time = "2026-03-31T16:54:21.555Z" }, { url = "https://files.pythonhosted.org/packages/4c/33/e18bcfa338ca4e6b2771c85d4c5203e627d0c69d9de5c1a2cf2ba13320ba/mypy-1.20.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49d11c6f573a5a08f77fad13faff2139f6d0730ebed2cfa9b3d2702671dd7188", size = 13719585, upload-time = "2026-03-31T16:51:53.89Z" }, { url = "https://files.pythonhosted.org/packages/6b/8d/93491ff7b79419edc7eabf95cb3b3f7490e2e574b2855c7c7e7394ff933f/mypy-1.20.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d3243c406773185144527f83be0e0aefc7bf4601b0b2b956665608bf7c98a83", size = 14685075, upload-time = "2026-03-31T16:54:04.464Z" }, { url = "https://files.pythonhosted.org/packages/b5/9d/d924b38a4923f8d164bf2b4ec98bf13beaf6e10a5348b4b137eadae40a6e/mypy-1.20.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a79c1eba7ac4209f2d850f0edd0a2f8bba88cbfdfefe6fb76a19e9d4fe5e71a2", size = 14919141, upload-time = "2026-03-31T16:54:51.785Z" }, { url = "https://files.pythonhosted.org/packages/59/98/1da9977016678c0b99d43afe52ed00bb3c1a0c4c995d3e6acca1a6ebb9b4/mypy-1.20.0-cp314-cp314-win_amd64.whl", hash = "sha256:00e047c74d3ec6e71a2eb88e9ea551a2edb90c21f993aefa9e0d2a898e0bb732", size = 11050925, upload-time = "2026-03-31T16:51:30.758Z" }, { url = "https://files.pythonhosted.org/packages/5e/e3/ba0b7a3143e49a9c4f5967dde6ea4bf8e0b10ecbbcca69af84027160ee89/mypy-1.20.0-cp314-cp314-win_arm64.whl", hash = "sha256:931a7630bba591593dcf6e97224a21ff80fb357e7982628d25e3c618e7f598ef", size = 10001089, upload-time = "2026-03-31T16:49:43.632Z" }, { url = "https://files.pythonhosted.org/packages/12/28/e617e67b3be9d213cda7277913269c874eb26472489f95d09d89765ce2d8/mypy-1.20.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:26c8b52627b6552f47ff11adb4e1509605f094e29815323e487fc0053ebe93d1", size = 15534710, upload-time = "2026-03-31T16:52:12.506Z" }, { url = "https://files.pythonhosted.org/packages/6e/0c/3b5f2d3e45dc7169b811adce8451679d9430399d03b168f9b0489f43adaa/mypy-1.20.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:39362cdb4ba5f916e7976fccecaab1ba3a83e35f60fa68b64e9a70e221bb2436", size = 14393013, upload-time = "2026-03-31T16:54:41.186Z" }, { url = "https://files.pythonhosted.org/packages/a3/49/edc8b0aa145cc09c1c74f7ce2858eead9329931dcbbb26e2ad40906daa4e/mypy-1.20.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34506397dbf40c15dc567635d18a21d33827e9ab29014fb83d292a8f4f8953b6", size = 15047240, upload-time = "2026-03-31T16:54:31.955Z" }, { url = "https://files.pythonhosted.org/packages/42/37/a946bb416e37a57fa752b3100fd5ede0e28df94f92366d1716555d47c454/mypy-1.20.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:555493c44a4f5a1b58d611a43333e71a9981c6dbe26270377b6f8174126a0526", size = 15858565, upload-time = "2026-03-31T16:53:36.997Z" }, { url = "https://files.pythonhosted.org/packages/2f/99/7690b5b5b552db1bd4ff362e4c0eb3107b98d680835e65823fbe888c8b78/mypy-1.20.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2721f0ce49cb74a38f00c50da67cb7d36317b5eda38877a49614dc018e91c787", size = 16087874, upload-time = "2026-03-31T16:52:48.313Z" }, { url = "https://files.pythonhosted.org/packages/aa/76/53e893a498138066acd28192b77495c9357e5a58cc4be753182846b43315/mypy-1.20.0-cp314-cp314t-win_amd64.whl", hash = "sha256:47781555a7aa5fedcc2d16bcd72e0dc83eb272c10dd657f9fb3f9cc08e2e6abb", size = 12572380, upload-time = "2026-03-31T16:49:52.454Z" }, { url = "https://files.pythonhosted.org/packages/76/9c/6dbdae21f01b7aacddc2c0bbf3c5557aa547827fdf271770fe1e521e7093/mypy-1.20.0-cp314-cp314t-win_arm64.whl", hash = "sha256:c70380fe5d64010f79fb863b9081c7004dd65225d2277333c219d93a10dad4dd", size = 10381174, upload-time = "2026-03-31T16:51:20.179Z" }, { url = "https://files.pythonhosted.org/packages/21/66/4d734961ce167f0fd8380769b3b7c06dbdd6ff54c2190f3f2ecd22528158/mypy-1.20.0-py3-none-any.whl", hash = "sha256:a6e0641147cbfa7e4e94efdb95c2dab1aff8cfc159ded13e07f308ddccc8c48e", size = 2636365, upload-time = "2026-03-31T16:51:44.911Z" }, ] [[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 = "packaging" version = "26.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] [[package]] name = "pathspec" version = "1.0.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, ] [[package]] name = "pluggy" version = "1.6.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] [[package]] name = "pygments" version = "2.20.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] [[package]] name = "pylibftdi" version = "0.24.1" source = { editable = "." } [package.dev-dependencies] dev = [ { name = "mypy" }, { name = "pytest" }, { name = "ruff" }, ] docs = [ { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "sphinx-rtd-theme" }, ] [package.metadata] [package.metadata.requires-dev] dev = [ { name = "mypy", specifier = ">=1.13" }, { name = "pytest", specifier = ">=8.3" }, { name = "ruff", specifier = ">=0.9" }, ] docs = [ { name = "sphinx", specifier = ">=8.0" }, { name = "sphinx-rtd-theme", specifier = ">=3.0" }, ] [[package]] name = "pytest" version = "9.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, ] [[package]] name = "requests" version = "2.33.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "charset-normalizer" }, { name = "idna" }, { name = "urllib3" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, ] [[package]] name = "roman-numerals" version = "4.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/41dc953bbeb056c17d5f7a519f50fdf010bd0553be2d630bc69d1e022703/roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2", size = 9077, upload-time = "2025-12-17T18:25:34.381Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" }, ] [[package]] name = "ruff" version = "0.15.9" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e6/97/e9f1ca355108ef7194e38c812ef40ba98c7208f47b13ad78d023caa583da/ruff-0.15.9.tar.gz", hash = "sha256:29cbb1255a9797903f6dde5ba0188c707907ff44a9006eb273b5a17bfa0739a2", size = 4617361, upload-time = "2026-04-02T18:17:20.829Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0b/1f/9cdfd0ac4b9d1e5a6cf09bedabdf0b56306ab5e333c85c87281273e7b041/ruff-0.15.9-py3-none-linux_armv6l.whl", hash = "sha256:6efbe303983441c51975c243e26dff328aca11f94b70992f35b093c2e71801e1", size = 10511206, upload-time = "2026-04-02T18:16:41.574Z" }, { url = "https://files.pythonhosted.org/packages/3d/f6/32bfe3e9c136b35f02e489778d94384118bb80fd92c6d92e7ccd97db12ce/ruff-0.15.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4965bac6ac9ea86772f4e23587746f0b7a395eccabb823eb8bfacc3fa06069f7", size = 10923307, upload-time = "2026-04-02T18:17:08.645Z" }, { url = "https://files.pythonhosted.org/packages/ca/25/de55f52ab5535d12e7aaba1de37a84be6179fb20bddcbe71ec091b4a3243/ruff-0.15.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eaf05aad70ca5b5a0a4b0e080df3a6b699803916d88f006efd1f5b46302daab8", size = 10316722, upload-time = "2026-04-02T18:16:44.206Z" }, { url = "https://files.pythonhosted.org/packages/48/11/690d75f3fd6278fe55fff7c9eb429c92d207e14b25d1cae4064a32677029/ruff-0.15.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9439a342adb8725f32f92732e2bafb6d5246bd7a5021101166b223d312e8fc59", size = 10623674, upload-time = "2026-04-02T18:16:50.951Z" }, { url = "https://files.pythonhosted.org/packages/bd/ec/176f6987be248fc5404199255522f57af1b4a5a1b57727e942479fec98ad/ruff-0.15.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c5e6faf9d97c8edc43877c3f406f47446fc48c40e1442d58cfcdaba2acea745", size = 10351516, upload-time = "2026-04-02T18:16:57.206Z" }, { url = "https://files.pythonhosted.org/packages/b2/fc/51cffbd2b3f240accc380171d51446a32aa2ea43a40d4a45ada67368fbd2/ruff-0.15.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b34a9766aeec27a222373d0b055722900fbc0582b24f39661aa96f3fe6ad901", size = 11150202, upload-time = "2026-04-02T18:17:06.452Z" }, { url = "https://files.pythonhosted.org/packages/d6/d4/25292a6dfc125f6b6528fe6af31f5e996e19bf73ca8e3ce6eb7fa5b95885/ruff-0.15.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89dd695bc72ae76ff484ae54b7e8b0f6b50f49046e198355e44ea656e521fef9", size = 11988891, upload-time = "2026-04-02T18:17:18.575Z" }, { url = "https://files.pythonhosted.org/packages/13/e1/1eebcb885c10e19f969dcb93d8413dfee8172578709d7ee933640f5e7147/ruff-0.15.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce187224ef1de1bd225bc9a152ac7102a6171107f026e81f317e4257052916d5", size = 11480576, upload-time = "2026-04-02T18:16:52.986Z" }, { url = "https://files.pythonhosted.org/packages/ff/6b/a1548ac378a78332a4c3dcf4a134c2475a36d2a22ddfa272acd574140b50/ruff-0.15.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b0c7c341f68adb01c488c3b7d4b49aa8ea97409eae6462d860a79cf55f431b6", size = 11254525, upload-time = "2026-04-02T18:17:02.041Z" }, { url = "https://files.pythonhosted.org/packages/42/aa/4bb3af8e61acd9b1281db2ab77e8b2c3c5e5599bf2a29d4a942f1c62b8d6/ruff-0.15.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:55cc15eee27dc0eebdfcb0d185a6153420efbedc15eb1d38fe5e685657b0f840", size = 11204072, upload-time = "2026-04-02T18:17:13.581Z" }, { url = "https://files.pythonhosted.org/packages/69/48/d550dc2aa6e423ea0bcc1d0ff0699325ffe8a811e2dba156bd80750b86dc/ruff-0.15.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a6537f6eed5cda688c81073d46ffdfb962a5f29ecb6f7e770b2dc920598997ed", size = 10594998, upload-time = "2026-04-02T18:16:46.369Z" }, { url = "https://files.pythonhosted.org/packages/63/47/321167e17f5344ed5ec6b0aa2cff64efef5f9e985af8f5622cfa6536043f/ruff-0.15.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6d3fcbca7388b066139c523bda744c822258ebdcfbba7d24410c3f454cc9af71", size = 10359769, upload-time = "2026-04-02T18:17:10.994Z" }, { url = "https://files.pythonhosted.org/packages/67/5e/074f00b9785d1d2c6f8c22a21e023d0c2c1817838cfca4c8243200a1fa87/ruff-0.15.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:058d8e99e1bfe79d8a0def0b481c56059ee6716214f7e425d8e737e412d69677", size = 10850236, upload-time = "2026-04-02T18:16:48.749Z" }, { url = "https://files.pythonhosted.org/packages/76/37/804c4135a2a2caf042925d30d5f68181bdbd4461fd0d7739da28305df593/ruff-0.15.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8e1ddb11dbd61d5983fa2d7d6370ef3eb210951e443cace19594c01c72abab4c", size = 11358343, upload-time = "2026-04-02T18:16:55.068Z" }, { url = "https://files.pythonhosted.org/packages/88/3d/1364fcde8656962782aa9ea93c92d98682b1ecec2f184e625a965ad3b4a6/ruff-0.15.9-py3-none-win32.whl", hash = "sha256:bde6ff36eaf72b700f32b7196088970bf8fdb2b917b7accd8c371bfc0fd573ec", size = 10583382, upload-time = "2026-04-02T18:17:04.261Z" }, { url = "https://files.pythonhosted.org/packages/4c/56/5c7084299bd2cacaa07ae63a91c6f4ba66edc08bf28f356b24f6b717c799/ruff-0.15.9-py3-none-win_amd64.whl", hash = "sha256:45a70921b80e1c10cf0b734ef09421f71b5aa11d27404edc89d7e8a69505e43d", size = 11744969, upload-time = "2026-04-02T18:16:59.611Z" }, { url = "https://files.pythonhosted.org/packages/03/36/76704c4f312257d6dbaae3c959add2a622f63fcca9d864659ce6d8d97d3d/ruff-0.15.9-py3-none-win_arm64.whl", hash = "sha256:0694e601c028fd97dc5c6ee244675bc241aeefced7ef80cd9c6935a871078f53", size = 11005870, upload-time = "2026-04-02T18:17:15.773Z" }, ] [[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", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, 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 = "9.0.4" 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", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, 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", 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/42/50/a8c6ccc36d5eacdfd7913ddccd15a9cee03ecafc5ee2bc40e1f168d85022/sphinx-9.0.4.tar.gz", hash = "sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3", size = 8710502, upload-time = "2025-12-04T07:45:27.343Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c6/3f/4bbd76424c393caead2e1eb89777f575dee5c8653e2d4b6afd7a564f5974/sphinx-9.0.4-py3-none-any.whl", hash = "sha256:5bebc595a5e943ea248b99c13814c1c5e10b3ece718976824ffa7959ff95fffb", size = 3917713, upload-time = "2025-12-04T07:45:24.944Z" }, ] [[package]] name = "sphinx" version = "9.1.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.12'", ] dependencies = [ { name = "alabaster", marker = "python_full_version >= '3.12'" }, { name = "babel", marker = "python_full_version >= '3.12'" }, { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "imagesize", marker = "python_full_version >= '3.12'" }, { name = "jinja2", marker = "python_full_version >= '3.12'" }, { name = "packaging", marker = "python_full_version >= '3.12'" }, { name = "pygments", marker = "python_full_version >= '3.12'" }, { name = "requests", marker = "python_full_version >= '3.12'" }, { name = "roman-numerals", marker = "python_full_version >= '3.12'" }, { name = "snowballstemmer", marker = "python_full_version >= '3.12'" }, { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.12'" }, { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.12'" }, { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.12'" }, { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.12'" }, { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.12'" }, { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl", hash = "sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978", size = 3921742, upload-time = "2025-12-31T15:09:25.561Z" }, ] [[package]] name = "sphinx-rtd-theme" version = "3.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "sphinxcontrib-jquery" }, ] sdist = { url = "https://files.pythonhosted.org/packages/84/68/a1bfbf38c0f7bccc9b10bbf76b94606f64acb1552ae394f0b8285bfaea25/sphinx_rtd_theme-3.1.0.tar.gz", hash = "sha256:b44276f2c276e909239a4f6c955aa667aaafeb78597923b1c60babc76db78e4c", size = 7620915, upload-time = "2026-01-12T16:03:31.17Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/87/c7/b5c8015d823bfda1a346adb2c634a2101d50bb75d421eb6dcb31acd25ebc/sphinx_rtd_theme-3.1.0-py2.py3-none-any.whl", hash = "sha256:1785824ae8e6632060490f67cf3a72d404a85d2d9fc26bce3619944de5682b89", size = 7655617, upload-time = "2026-01-12T16:03:28.101Z" }, ] [[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-jquery" version = "4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/de/f3/aa67467e051df70a6330fe7770894b3e4f09436dea6881ae0b4f3d87cad8/sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a", size = 122331, upload-time = "2023-03-14T15:01:01.944Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/76/85/749bd22d1a68db7291c89e2ebca53f4306c3f205853cf31e9de279034c3c/sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae", size = 121104, upload-time = "2023-03-14T15:01:00.356Z" }, ] [[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.4.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, ] [[package]] name = "typing-extensions" version = "4.15.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] [[package]] name = "urllib3" version = "2.6.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, ]