diff --git a/.flake8 b/.flake8 deleted file mode 100644 index beca790c..00000000 --- a/.flake8 +++ /dev/null @@ -1,23 +0,0 @@ -[flake8] -exclude = .git,__pycache__,build,dist,typhos/_version.py -max-line-length = 88 -select = C,E,F,W,B,B950 -extend-ignore = E203, E501, E226, W503, W504 - -# Explanation section: -# B950 -# This takes into account max-line-length but only triggers when the value -# has been exceeded by more than 10% (96 characters). -# E203: Whitespace before ':' -# This is recommended by black in relation to slice formatting. -# E501: Line too long (82 > 79 characters) -# Our line length limit is 88 (above 79 defined in E501). Ignore it. -# E226: Missing whitespace around arithmetic operator -# This is a stylistic choice which you'll find everywhere in pcdsdevices, for -# example. Formulas can be easier to read when operators and operands -# have no whitespace between them. -# -# W503: Line break occurred before a binary operator -# W504: Line break occurred after a binary operator -# flake8 wants us to choose one of the above two. Our choice -# is to make no decision. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 4d173ec0..d76cc596 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -26,5 +26,3 @@ - [ ] Code has been checked for threading issues (no blocking tasks in GUI thread) - [ ] Test suite passes locally - [ ] Test suite passes on GitHub Actions -- [ ] Ran ``docs/pre-release-notes.sh`` and created a pre-release documentation page -- [ ] Pre-release docs include context, functional descriptions, and contributors as appropriate diff --git a/.gitignore b/.gitignore index f3620fdd..a1cbc398 100644 --- a/.gitignore +++ b/.gitignore @@ -116,3 +116,6 @@ ENV/ *.swp *~ .vscode + +# Versioning +_version.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ffe22065..7b2777c5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,28 +6,25 @@ exclude: | )$ repos: -- repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 - hooks: - - id: no-commit-to-branch - - id: trailing-whitespace - - id: end-of-file-fixer - - id: check-ast - - id: check-case-conflict - - id: check-json - - id: check-merge-conflict - - id: check-symlinks - - id: check-xml - - id: check-yaml - exclude: '^(conda-recipe/meta.yaml)$' - - id: debug-statements +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: no-commit-to-branch + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-ast + - id: check-case-conflict + - id: check-json + - id: check-merge-conflict + - id: check-symlinks + - id: check-xml + - id: check-yaml + exclude: '^(conda-recipe/meta.yaml)$' + - id: debug-statements -- repo: https://github.com/pycqa/flake8.git - rev: 7.2.0 - hooks: - - id: flake8 - -- repo: https://github.com/timothycrosley/isort - rev: 6.0.1 - hooks: - - id: isort +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.6 + hooks: + - id: ruff-check # run the linter + args: [ --fix ] # and the safe fixes + - id: ruff-format # run the formatter diff --git a/conda-recipe/meta.yaml b/conda-recipe/meta.yaml index 2897f2ae..9c9119e6 100644 --- a/conda-recipe/meta.yaml +++ b/conda-recipe/meta.yaml @@ -14,8 +14,6 @@ build: noarch: python script: {{ PYTHON }} -m pip install . -vv - - requirements: build: - python >=3.9 @@ -34,7 +32,6 @@ requirements: - pydm >=1.19.1 - pyqt =5 - pyqtgraph - - pyyaml - qdarkstyle - qtawesome - qtpy @@ -42,8 +39,6 @@ requirements: run_constrained: - happi >=1.14.0 - - test: commands: - typhos --help @@ -54,18 +49,12 @@ test: - caproto - doctr - doctr-versions-menu - - flake8 - - ipython>=7.16 - - jinja2<3.1 - line_profiler - pcdsdevices>=8.4.0 - pytest - pytest-benchmark - pytest-qt - pytest-timeout - - simplejson - - about: dev_url: https://github.com/pcdshub/typhos diff --git a/dev-requirements.txt b/dev-requirements.txt deleted file mode 100644 index d6d1919f..00000000 --- a/dev-requirements.txt +++ /dev/null @@ -1,12 +0,0 @@ -caproto -flake8 -ipython>=7.16 -jinja2 -line_profiler -pcdsdevices>=8.4.0 -pytest -pytest-benchmark -pytest-cov -pytest-qt -pytest-timeout -simplejson diff --git a/docs-requirements.txt b/docs-requirements.txt deleted file mode 100644 index cdde92b8..00000000 --- a/docs-requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -docs-versions-menu -happi -sphinx -sphinx_rtd_theme -sphinxcontrib-jquery diff --git a/docs/pre-release-notes.sh b/docs/pre-release-notes.sh deleted file mode 100755 index 6e1e12b6..00000000 --- a/docs/pre-release-notes.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/bash - -ISSUE=$1 -shift -DESCRIPTION=$* - -if [[ -z "$ISSUE" || -z "$DESCRIPTION" ]]; then - echo "Usage: $0 (ISSUE NUMBER) (DESCRIPTION)" - exit 1 -fi - -re_issue_number='^[1-9][0-9]*$' - -if ! [[ "$ISSUE" =~ $re_issue_number ]]; then - echo "Error: Issue number is not a number: $ISSUE" - echo - echo "This should preferably be the issue number that this pull request solves." - echo "We may also accept the Pull Request number in place of the issue." - exit 1 -fi - -echo "Issue: $ISSUE" -echo "Description: $DESCRIPTION" - -FILENAME=source/upcoming_release_notes/${ISSUE}-${DESCRIPTION// /_}.rst - -pushd "$(dirname "$0")" || exit 1 - -sed -e "s/IssueNumber Title/${ISSUE} ${DESCRIPTION}/" \ - "source/upcoming_release_notes/template-short.rst" > "${FILENAME}" - -if ${EDITOR} "${FILENAME}"; then - echo "Adding ${FILENAME} to the git repository..." - git add "${FILENAME}" -fi - -popd || exit 0 diff --git a/docs/release_notes.py b/docs/release_notes.py deleted file mode 100644 index 16baef98..00000000 --- a/docs/release_notes.py +++ /dev/null @@ -1,114 +0,0 @@ -import sys -import time -from collections import defaultdict -from pathlib import Path - -# find the pre-release directory and release notes file -THIS_DIR = Path(__file__).resolve().parent -PRE_RELEASE = THIS_DIR / 'source' / 'upcoming_release_notes' -TEMPLATE = PRE_RELEASE / 'template-short.rst' -RELEASE_NOTES = THIS_DIR / 'source' / 'release_notes.rst' - -# Set up underline constants -TITLE_UNDER = '#' -RELEASE_UNDER = '=' -SECTION_UNDER = '-' - - -def parse_pre_release_file(path): - """ - Return dict mapping of release notes section to lines. - - Uses empty list when no info was entered for the section. - """ - print(f'Checking {path} for release notes.') - with path.open('r', encoding='utf-8') as fd: - lines = fd.readlines() - - section_dict = defaultdict(list) - prev = None - section = None - - for line in lines: - if prev is not None: - if line.startswith(SECTION_UNDER * 2): - section = prev.strip() - continue - if section is not None and line[0] in ' -': - notes = section_dict[section] - if len(line) > 6: - notes.append(line) - section_dict[section] = notes - prev = line - - return section_dict - - -def extend_release_notes(path, version, release_notes): - """ - Given dict mapping of section to lines, extend the release notes file. - """ - with path.open('r', encoding='utf-8') as fd: - lines = fd.readlines() - - new_lines = ['\n', '\n'] - date = time.strftime('%Y-%m-%d') - release_title = f'{version} ({date})' - new_lines.append(release_title + '\n') - new_lines.append(len(release_title) * RELEASE_UNDER + '\n') - new_lines.append('\n') - for section, section_lines in release_notes.items(): - if section == 'Contributors': - section_lines = sorted(list(set(section_lines))) - if len(section_lines) > 0: - new_lines.append(section + '\n') - new_lines.append(SECTION_UNDER * len(section) + '\n') - new_lines.extend(section_lines) - new_lines.append('\n') - - output_lines = lines[:2] + new_lines + lines[2:] - - print('Writing out release notes file') - for line in new_lines: - print(line.strip('\n')) - with path.open('w', encoding='utf-8') as fd: - fd.writelines(output_lines) - - -def main(version_number: str): - section_notes = parse_pre_release_file(TEMPLATE) - to_delete = [] - for path in PRE_RELEASE.iterdir(): - if path.name[0] in '1234567890': - to_delete.append(path) - extra_notes = parse_pre_release_file(path) - for section, notes in section_notes.items(): - notes.extend(extra_notes[section]) - section_notes[section] = notes - - extend_release_notes(RELEASE_NOTES, version_number, section_notes) - - print( - "* Wrote release notes. Please perform the following manually:", - file=sys.stderr, - ) - for path in to_delete: - print(f" git rm {path}", file=sys.stderr) - print(f" git add {RELEASE_NOTES}", file=sys.stderr) - - -if __name__ == '__main__': - if len(sys.argv) != 2: - print(f"Usage: {sys.argv[0]} VERSION_NUMBER", file=sys.stderr) - sys.exit(1) - - version_number = sys.argv[1] - - if not version_number.startswith("v"): - print( - f"Version number should start with 'v': {version_number}", - file=sys.stderr - ) - sys.exit(1) - - main(version_number) diff --git a/docs/source/index.rst b/docs/source/index.rst index 6f52dd6b..c0bd3b21 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -44,7 +44,6 @@ Related Projects python_methods.rst templates.rst release_notes.rst - upcoming_changes.rst .. toctree:: :maxdepth: 3 diff --git a/docs/source/release_notes.rst b/docs/source/release_notes.rst index c2863041..1d9b501d 100644 --- a/docs/source/release_notes.rst +++ b/docs/source/release_notes.rst @@ -1,992 +1,4 @@ Release History ############### - -v4.2.0 (2025-04-07) -=================== - -Features --------- -- Add `long_name` support for `device.signal` or `device.component.signal` that replace `label_text` for rows in `SignalPanel` -- Allows custom displays with "add_device" methods to hook typhos correctly - -Bugfixes --------- -- Clean up signal plugin with channel.disconnect(destroying=True), since channels are being deleted on test cleanup - -Contributors ------------- -- ZLLentz -- aberges-SLAC -- tangkong - - - -v4.1.0 (2024-12-19) -=================== - -Bugfixes --------- -- Fix an issue where detailed tree screens would automatically load without - scrollbars. Now, the "auto" scrollbar setting is based primarily on the - apparent display type of the screen, not of the originally requested - display type, which may not be used if no such template exists. -- Fix an issue where dynamic font scaling applied to a combobox - with no entries would raise an exception and close typhos. -- Fix an issue where long status messages from positioner widget moves - would fail to display in certain circumstances. - -Contributors ------------- -- zllentz - - - -v4.0.0 (2024-08-20) -=================== - -API Breaks ----------- -- ``TyphosStatusThread`` now has a dramatically different signal API. - This is an improved version but if you were using this class take note - of the changes. Key member signals are: - - ``TyphosStatusThread.status_started`` - - ``TyphosStatusThread.status_timeout`` - - ``TyphosStatusThread.status_finished`` - - ``TyphosStatusThread.error_message`` - - ``TyphosStatusThread.status_exc`` - -Features --------- -- Rework the design, sizing, and font scaling of the positioner row widget to address - concerns about readability and poor use of space for positioners that don't need - all of the widget components. -- Implement dynamic resizing in all directions for positioner row widgets. -- Make the timeout messages friendlier and more accurate when the - timeouts come from the ``TyphosPositionerWidget``. -- Make error messages in general (including status timeouts) clearer - when they come from the positioner device class controlled by the - ``TyphosPositionerWidget``. - -Bugfixes --------- -- Fix an issue where the row positioner widget's resizing would peg the cpu to 100% -- Fix various issues that cause font clipping for specific motors using the positioner row widget. -- Fix various issues with enum handling in the SignalPlugin. - -Maintenance ------------ -- Fix issues with cloud-only CI failures and segfaults. -- unpin jinja, sphinx no longer incompatible -- Refactor ``TyphosStatusThread`` to facilitate timeout message changes. -- In dev/test requirements, pin pcdsdevices to current latest to fix the CI builds. - -Contributors ------------- -- canismarko -- tangkong -- zllentz - - - -v3.1.1 (2024-04-12) -=================== - -Fixes ------ -- Change position widget limit colors from yellow off/yellow on to - dark gray off/orange on for better contrast. - -Contributors ------------- -- zllentz - - -v3.1.0 (2023-12-05) -=================== - -Features --------- -- Add overridable ``find_signal`` method to the ``SignalConnection`` class to allow - the use of alternate signal registries in subclasses. -- Updated look and feel: change the typhos suite cli defaults to - "embedded" displays arranged "vertically" instead of - "detailed" displays arranged "horizontally" - to more naturally match how typhos is used at present. - -Contributors ------------- -- canismarko -- zllentz - - -v3.0.0 (2023-09-27) -=================== - -API Breaks ----------- -- The deprecated ``TyphosConsole`` has been removed as discussed in issue #538. -- ``TyphosDeviceDisplay`` composite heuristics have been removed in favor of - simpler methods, described in the features section. -- The packaged IOC for benchmark testing is now in ``typhos.benchmark.ioc``. - -Features --------- -- Added ``typhos --screenshot filename_pattern`` to take screenshots of typhos - displays prior to exiting early (in combination with ``--exit-after``). -- Added ``TyphosSuite.save_screenshot`` which takes a screenshot of the entire - suite as-displayed. -- Added ``TyphosSuite.save_device_screenshots`` which takes individual - screenshots of each device display in the suite and saves them to the - provided formatted filename. -- ``LazySubdisplay.get_subdisplay`` now provides the option to only get - existing widgets (by using the argument ``instantiate=False``). -- ``TyphosNoteEdit`` now supports ``.add_device()`` like other typhos widgets. - This is alongside its original ``setup_data`` API. -- ``TyphosNoteEdit`` is now a ``TyphosBase`` object and is accessible in the Qt - designer. -- Added new designable widget ``TyphosPositionerRowWidget``. This compact - positioner widget makes dense motor-heavy screens much more space efficient. -- The layout method for ``TyphosDeviceDisplay`` has changed. For large device trees, - it now favors showing the compact "embedded" screens over detailed screens. The order - of priority is now as follows: -- For top-level devices (e.g., ``at2l0``), the template load priority is as follows: - - * Happi-defined values (``"detailed_screen"``, ``embedded_screen"``, ``"engineering_screen"``) - * Device-specific screens, if available (named as ``ClassNameHere.detailed.ui``) - * The detailed tree, if the device has sub-devices - * The default templates - -- For nested displays in a device tree, sub-device (e.g., ``at2l0.blade_01``) - template load priority is as follows: - - * Device-specific screens, if available (named as ``ClassNameHere.embedded.ui``) - * The detailed tree, if the device has sub-devices - * The default templates (``embedded_screen.ui``) - -- Increase motor timeouts proportionally for longer moves. -- Added dynamic font sizer utility which can work with some Qt-provided widgets - as well as PyDM widgets. -- Qt object names for displays will now be set automatically to aid in - debugging. - -Bugfixes --------- -- Fix an issue where setpoint widgets in the full positioner - widget had become zero-width. -- Creates new notes file if requested note file does not exist -- Typhos suites will now resize in width to fit device displays. -- For devices which do not require keyword arguments to instantiate, the typhos - CLI will no longer require an empty dictionary. That is, ``$ typhos - ophyd.sim.SynAxis[]`` is equivalent to ``$ typhos ophyd.sim.SynAxis[{}]``. - As before, ophyd's required "name" keyword argument is filled in by typhos by - default. -- Fix an issue where ophyd signals with floats would always display with a - precision of 0 without special manual configuration. Floating-point signals - now default to a precision of 3. -- Fix issues with running the CLI benchmarks in certain - conda installs, particularly python>=3.10. -- ``ophyd.Kind`` usage has been fixed for Python 3.11. Python 3.11 differs in - enumeration of ``IntFlag`` items, resulting in typhos only picking up - component kinds that were a power of 2. -- ``multiprocessing`` is no longer used to spawn the test suite benchmarking - IOC, as it was problematic for Python 3.11. The provided IOC is now spawned - using the same utilities provided by the caproto test suite. -- Vendored pydm ``load_ui_file`` and modified it so we can always get our - ``Display`` instance back in ``TyphosDeviceDisplay``. -- Ignore deleted qt objects on ``SignalConnection.remove_connection``, avoiding - teardown error tracebacks. -- Avoid creating subdisplays during a call to ``TyphosSuite.hide_subdisplays`` -- Added a pytest hook helper to aid in finding widgets that were not cleaned -- Avoid failing screenshot taking when widgets are garbage collected at the - same time. -- Avoid race condition in description cache if the cache is externally cleared - when a new description callback is received. -- Avoid uncaught ``TypeError`` when ``None`` is present in a positioner - ``.limits``. - -Maintenance ------------ -- adds TyphosDisplaySwitcher to TyphosPositionerRowWidget -- adds checklist to Pull Request Template -- Add pre-release notes scripts -- Update build requirements to use pip-provided extras for documentation and test builds -- Update PyDM pin to >=1.19.1 due to Display method being used. -- Avoid hundreds of warnings during line profiling profiling by intercepting - messages about profiling the wrapped function instead of the wrapper. -- The setpoint history menu on ``TyphosLineEdit`` is now only created on-demand. - -Contributors ------------- -- klauer -- tangkong -- zllentz - - -v2.4.1 (2023-4-4) -================= - -Description ------------ -This is a bugfix and maintenance/CI release. - -Bugfixes --------- -- Include the normal PyDM stylesheets in the loading process. - Previously, this was leading to unexpected behavior. - -Maintenance ------------ -- Fix an issue related to a deleted flake8 mirror. -- Migrates from Travis CI to GitHub Actions for continuous integration testing, and documentation deployment. -- Updates typhos to use setuptools-scm, replacing versioneer, as its version-string management tool of choice. -- Syntax has been updated to Python 3.9+ via ``pyupgrade``. -- typhos has migrated to modern ``pyproject.toml``, replacing ``setup.py``. -- Sphinx 6.0 now supported for documentation building. - -Contributors ------------- -- tangkong -- zllentz - - -v2.4.0 (2022-11-4) -================== - -Description ------------ -This is a small release with features for improving the usage -and configurability of the ``PositionerWidget``. - -Features --------- -- Report errors raised during the execution of positioner - ``set`` commands in the positioner widget instead of in a pop-up. - This makes it easier to keep track of which positioner widget - is associated with which error and makes it less likely that the - message will be missed or lost on large monitors. -- Add a designer property to ``PositionerWidget``, ``alarmKindLevel``, - to configure the enclosed alarm widget's ``kindLevel`` property in - designer. This was previously only configurable in code. - -Contributors ------------- -- zllentz - - -v2.3.3 (2022-10-20) -=================== - -Description ------------ -This is a small release with bugfixes and maintenance. - -Bugfixes --------- -- Do not wait for lazy signals when creating a SignalPanel. - This was causing long setup times in some applications. -- Call stop with success=True in the positioner widget to avoid causing - our own UnknownStatusError, which was then displayed to the user. - -Maintenance ------------ -- Add cleanup for background threads. -- Add replacement for functools.partial usage in methods as - this was preventing TyphosSuite from getting garbage collected. -- Removes custom designer widget plugin, - instead relying on PyDM's own mechanism -- Use pydm's data plugin entrypoint to include the sig and happi channels. -- Prevent TyphosStatusThread objects from being orphaned. - -Contributors ------------- -- klauer -- tangkong -- zllentz - - -v2.3.2 (2022-07-28) -=================== - -Description ------------ -This is a bugfix and maintenance release. - -Fixes ------ -- Fix various instances of clipping in the positioner widget. -- Show Python documentation when no web help is available. -- Fix issues with suite sidebar width. -- Lazy load all tools to improve performance. -- Fix the profiler to also profile class methods. -- Use cached paths for finding class templates. -- Properly handle various deprecations and deprecation warnings. -- Fix usage of deprecated methods in happi (optional dependency). - -Maintenance ------------ -- Log "unable to add device" without the traceback, which was previously unhelpful. -- Pin pyqt at 5.12 for test suite incompatibility in newer versions. -- Ensure that test.qss test suite artifact is cleaned up properly. -- Fix the broken test suite. -- Pin jinja2 at <3.1 in CI builds for sphinx <4.0.0 compatibility - -Contributors ------------- -- anleslac -- klauer -- zllentz - - -v2.3.1 (2022-05-02) -=================== - -Description ------------ -This is a small bugfix release. - -Fixes ------ -- Fix an issue where the configuration menu would be defunct for - custom template screens. - -Maintenance ------------ -- Add some additional documentation about sig:// and cli usage. -- Configure and satisfy the repository's own pre-commit checks. -- Update versioneer install to current latest. - -Contributors ------------- -- klauer -- zllentz - - -v2.3.0 (2022-03-31) -=================== - -Description ------------ -This is a small release with fixes and features that were implemented -last month. - -Features --------- -- Add the option to hide displays in the suite at launch, - rather than automatically showing all of them. -- Allow the sig:// protocol to be used in typhos templates by - automatically registering all of a device's signals at launch. - -Fixes ------ -- Fix an issue where an assumption about the nature of EpicsSignal - object was breaking when using PytmcSignal objects from pcdsdevices. -- Make a workaround for a C++ wrapped exception that could happen - in specific orders of loading and unloading typhos alarm widgets. - - -v2.2.1 (2022-02-07) -=================== - -Description ------------ -This is a small bugfix release that was deployed as a hotfix -to avoid accidental moves. - -Fixes ------ -- Disable scroll wheel interaction with positioner combo boxes. - This created a situation where operators were accidentally - requesting moves while trying to scroll past the control box. - This was previously fixed for the typhos combo boxes found on - the various automatically generated panels in v1.1.0, but not - for the positioner combo boxes. - - -v2.2.0 (2021-11-30) -=================== - -Description ------------ -This is a feature and bugfix release to extend the customizability of -typhos suites and launcher scrips, to fix various issues in control -layer and enum handling, and to do some necessary CI maintenance. - -Enhancements / What's new -------------------------- -* Add suite options for layouts, display types, scrollbars, and - starting window size. These are all also available as CLI arguments, - with the intention of augmenting typhos suite launcher scripts. - Here are some examples: - - * ``--layout grid --cols 3``: lays out the device displays in a 3-column - grid - * ``--layout flow``: lays out the device displays in a grid that adjusts - dynamically as the window is resized. - * ``--display-type embed``: starts all device displays in their embedded - state - * ``--size 1000,1000``: sets a starting size of 1000 width, 1000 height for - the suite window. - - See `#450 `_ - -Fixes ------ -* Respect ophyd signal enum_strs and metadata updates. Previously, these were - ignored, but these can update during the lifetime of a screen and should be - used. (`#459 `_) -* Identify signals that use non-EPICS control layers and handle them - appropriately. Previously, these would be misidentified as EPICS signals - and handled using the ca:// PyDM plugin, which was not correct. - (`#463 `_) -* Fix an issue where get_native_methods could fail. This was not observed - in the field, but it broke the test suite. - (`#464 `_) - -Maintenance ------------ -* Fix various issues related to the test suite stability. - - -v2.1.0 (2021-10-18) -=================== - -Description ------------ -This is a minor feature release of typhos. - -Enhancements / What's new -------------------------- -* Added option to pop out documentation frame - (`#458 `_) - -Fixes ------ -* Fixed authorization headers on Typhos help widget redirect - (`#457 `_) - - * This allows for the latest Confluence to work with Personal - Access Tokens while navigating through the page - -Maintenance ------------ -* Reduced javascript log message spam from the web view widget - (part of `#457 `_) -* Reduced log message spam from variety metadata handling - (part of `#457 `_) -* Fixed compatibility with pyqtgraph v0.12.3 -* Web-related widgets are now in a new submodule `typhos.web`. - - -v2.0.0 (2021-08-05) -=================== - -Description ------------ -This is a feature update with backwards-incompatible changes, namely the -removal and relocation of the LCLS typhos templates. - -API Breaks ----------- -All device templates except for the ``PositionerBase`` template have been -moved from typhos to pcdsdevices, which is where their device classes -are defined. This will break LCLS environments that update typhos without -also updating pcdsdevices, but will not affect environments outside of LCLS. - -Enhancements / What's New -------------------------- -- Add the ``TyphosRelatedSuiteButton``, a ``QPushButton`` that will open a device's - typhos screen. This can be included in embedded widgets or placed on - traditional hand-crafted pydm screens as a quick way to open the typhos - expert screen. -- Add the typhos help widget, which is a new addition to the display switcher - that is found in all built-in typhos templates. Check out the ``?`` button! - See the docs for information on how to configure this. - The main features implemented here are: - - - View the class docstring from inside the typhos window - - Open site-specific web documentation in a browser - - Report bugs directly from the typhos screen - -- Expand the ``PositionerWidget`` with aesthetic updates and more features: - - - Show driver-specific error messages from the IOC - - Add a "clear error" button that can be linked to IOC-specific error - reset routines by adding a ``clear_error`` method to your positioner - class. This will also clear status errors returned from the positioner's - set routine from the display. - - Add a moving/done_moving indicator (for ``EpicsMotor``, uses the ``.MOVN`` field) - - Add an optional ``TyphosRelatedSuite`` button - - Allow the ``stop`` button to be removed if the ``stop`` method is missing or - otherwise raises an ``AttributeError`` on access - - Add an alarm indicator - -- Add the ``typhos.ui`` entry point. This allows a module to notify typhos that - it should check specified directories for custom typhos templates. To be - used by typhos, the entry point should load a ``str``, ``pathlib.Path``, or ``list`` - of such objects. -- Move the examples submodule into the ``typhos.examples`` submodule, so we can - launch the examples by way of e.g. ``typhos -m typhos.examples.positioner``. -- For the alarm indicator widgets, allow the pen width, pen color, and - pen style to be customized. - -Compatibility / Fixes ---------------------- -- Find a better fix for the issue where the positioner combobox widget would - put to the PV on startup and on IOC reboot - (see ``v1.1.0`` note about a hacky workaround). -- Fix the issue where the positioner combobox widget could not be used to - move to the last position selected. -- Fix an issue where a positioner status that was marked as failed immediately - would show as an unknown error, even if it had an associated exception - with useful error text. - -Docs / Testing --------------- -- Add documentation for all features included in this update -- Add documentation for how to create custom ``typhos`` templates - - -v1.2.0 (2021-07-09) -=================== - -Description ------------ -This is a feature update intended for use in lucid, but it may also be useful -elsewhere. - -Enhancements / What's New -------------------------- -Add a handful of new widgets for indicating device alarm state. These will -change color based on the most severe alarm found among the device's signals. -Their shapes correlate with the available shapes of PyDMDrawingWidget: - -- TyphosAlarmCircle -- TyphosAlarmRectangle -- TyphosAlarmTriangle -- TyphosAlarmEllipse -- TyphosAlarmPolygon - -Compatibility / Fixes ---------------------- -- Add a sigint handler to avoid annoying behavior when closing with Ctrl-C on - macOS. -- Increase some timeouts to improve unit test consistency. - - -v1.1.6 (2021-04-05) -=================== - -Description ------------ -This is maintenance/compatibility release for pydm v1.11.0. - -Compatibility / Fixes ---------------------- -- Internal fixes regarding error handling and input sanitization. - Some subtle issues cropped up here in the update to pydm v1.11.0. -- Fix issue where the test suite would freeze when pydm displays - an exception to the user. - - -v1.1.5 (2020-04-02) -=================== - -Description ------------ -This is a maintenance release - -Compatibility / Fixes ---------------------- -- Fix an issue where certain data files were not included in the package - build. - - -v1.1.4 (2020-02-26) -=================== - -Description ------------ -This is a bugfix release - -Compatibility / Fixes ---------------------- -- Fix returning issue where certain devices could fail to load with a - "dictionary changed during iteration" error. -- Fix issue where the documentation was not building properly. - - -v1.1.3 (2020-02-10) -=================== - -Description ------------ -This is a minor screen inclusion release. - -Enhancements / What's New -------------------------- -- Add a screen for AT1K4. This, and similar screens, should be moved out of - typhos and into an LCLS-specific landing-zone, but this is not ready yet. - - -v1.1.2 (2020-12-22) -=================== - -Description ------------ -This is a minor bugfix release. - -Compatibility / Fixes ---------------------- -- Fix issue where ``SignalRO`` from ``ophyd`` was not showing as read-only. -- Update the AT2L0 screen to not have a redundant calculation dialog as per - request. - - -v1.1.1 (2020-08-19) -=================== - -Description ------------ -This is a bugfix release. Please use this instead of v1.1.0. - -Compatibility / Fixes ---------------------- -- Fix issue with ui files not being included in the manifest -- Fix issue with profiler failing on tests submodule - - -v1.1.0 (2020-08-18) -=================== - -Description ------------ -This is a big release with many fixes and features. - -Enhancements / What's New -------------------------- -- Make Typhos aware of variety metadata and assign appropriate widgets based - on the variety metadata assigned in pcdsdevices. -- Split templates into three categories: core, devices, and widgets. - Core templates are the main typhos display templates, e.g. detailed_tree. - Devices templates are templates tailored for specific device classes. - Widgets templates define special typhos widgets like tweakable, positioner, - etc. -- Add attenuator calculator screens. These may be moved to another repo in a - future release. -- Add information to loading widgets indicating timeout details. - -Compatibility / fixes ---------------------- -- Fix issue with comboboxes being set on mouse scroll. -- Allow loading classes from cli with numbers in the name. -- Fix issue with legacy codepath used in lightpath. -- Fix issue with widget UnboundLocalError. -- Hacky workaround for issue with newer versions of Python. -- Hacky workaround for issue where positioner widget puts on startup. -- Fix issue with unset _channel member. -- Fix issue with typhos creating and installing a tests package separate - from the main typhos package. - -Docs / Testing --------------- -- Add variety testing IOC. -- Add doctr_versions_menu extension to properly render version menu. -- Fix issues with failing benchmark tests - - -v1.0.2 (2020-07-01) -=================== - -Description ------------ - -A bug fix and package maintenance release. - -Enhancements / What's New -------------------------- -- PositionerWidget moves set their timeouts based on expected - velocity and acceleration, rather than a flat 10 seconds. - -Compatibility / fixes ---------------------- -- Ensure that widgets with no layout or minimum size are still displayed. -- Update local conda recipe to match conda-forge. -- Update CI to used shared configurations. - - -v1.0.1 (2020-05-20) -=================== - -Description ------------ - -A bug fix release with a minor addition. - -Enhancements / What's New -------------------------- -- TyphosLoading now takes in a timeout value to switch the animation - with a text message stating that the operation timed-out after X - seconds. - - -Compatibility / fixes ---------------------- - -- Combobox widgets were appearing when switching or refreshing templates. - - -v1.0.0 (2020-05-18) -=================== - -Description ------------ - -A major new feature release with added views for complex devices and -simplified configurability. - -As planned, the deprecated import name ``typhon`` and the ``typhon`` -command-line tool have been removed. - -Enhancements / What's New -------------------------- - -- Panels: New ``TyphosCompositeSignalPanel``, which composes multiple - ``TyphosDisplay``\ s in a tree-like view. -- Benchmarking: new profiling tools accessible in the command-line - ``typhos`` tool, allowing for per-line profiling of standardized - devices. (``--benchmark``) -- Template discovery: templates are discovered based on screen macros - and class inheritance structure, with the fallback of built-in - templates. -- New command-line options for testing with mock devices - (``--fake-device``). -- Performance: Major performance improvements by way of background - threading of signal description determination, display path caching, - and connection status monitoring to reduce GUI thread blocking. -- Display: Adds a "display switcher" tool for easy access to different - screen types. -- Display: Adds a "configuration" button to displays. -- Filtering: Filter panel contents by kinds. -- Filtering: Filter panel contents by signal names. -- Setpoint history: a history of previous setpoints has been added to - the context menu in ``TyphosLineEdit``. -- Positioner widgets have been redesigned to be less magical and more fault- - tolerant. Adds designable properties that allow for specification of - attribute names. -- Anything that inherits from ``PositionerBase`` will have the template as an - option (``EpicsMotor``, ``PCDSMotorBase``, etc.) -- Reworked default templates to remove the ``miscellaneous`` panel. Omitted - signals may still be shown by way of panel context menus or configuration - menus. - -Compatibility / fixes ---------------------- - -- Python 3.8 is now being included in the test suite. -- Happi is now completely optional. -- Popped-out widgets such as plots will persist even when the parent - display is closed. -- Font sizes should be more consistent on various DPI displays. -- Module ``typhos.signal`` has been renamed to ``typhos.panel``. -- ``TyphosTimePlot`` no longer automatically adds signals to the plot. -- Removed internally-used ``typhos.utils.grab_kind``. -- OSX layout of ``TyphosSuite`` should be improved using the unified title and - toolbar. - -v0.7.0 (2020-03-09) -=================== - -- Fix docs deployment -- Add “loading in progress” gif -- Fix sorting of signals -- Automatically choose exponential format based on engineering units -- Fix lazy loading in ophyd 1.4 -- Save images of widgets when running tests -- Add a new “PopBar” which pops in the device tree in the suite -- Clean up the codebase - sort all imports + fix style -- Relocate SignalRO to a single spot - - -v0.6.0 (2020-01-09) -=================== - -Description ------------ - -This release is dedicated to the renaming of the package from ``Typhon`` -to ``Typhos``. The main reason for the renaming is a naming conflict at -PyPI that is now addressed. - -Compatibility -------------- - -This release is still compatible and will throw some DeprecationWarnings -when ``typhon`` is used. The only incompatible piece is for Qt -Stylesheets. You will need to add the ``typhos`` equivalents to your -custom stylesheets if you ever created one. - -**This is the first release with the backwards compatibility for typhon. -In two releases time it will be removed.** - - -v0.5.0 (2019-09-18) -=================== - -Description ------------ - -It was a long time since the latest release of ``Typhon``. It is time -for a new one. Next releases will have again the beautiful and -descriptive messages for enhancements, bug fixes and etc. - -What’s New ----------- - -A lot. - - -v0.2.1 (2018-09-28) -=================== - -Description ------------ - -This is a minor release of the ``Typhon`` library. No major features -were added, but instead the library was made more stable and utilitarian -for use in other programs. This includes making sure that any calls to a -signal’s values or metadata are capable of handling disconnections. It -also moves some of the methods that were hidden in larger classes or -functions into smaller, more useful methods. - -Enhancements -~~~~~~~~~~~~ - -- ``SignalPlugin`` now transmits all the metadata that is guaranteed to - be present from the base ``Signal`` object. This includes - ``enum_strs``, ``precision``, and ``units`` - (`#92 `__) -- ``DeviceDisplay`` now has an optional argument ``children``. This - makes it possible to ignore a ``Device`` components when creating the - display (`#96 `__) -- The following utility functions have been created to ensure that a - uniform approach is taken for\ ``Device`` introspection: - ``is_signal_ro``, ``grab_hints`` - (`#98 `__) - -Maintenance -~~~~~~~~~~~ - -- Catch exceptions when requesting information from a ``Signal`` in - case of disconnection, e.t.c - (`#91 `__, - `#92 `__) -- The library now imports entirely from the ``qtpy`` compatibility - layer (`#94 `__) - -Deprecations -~~~~~~~~~~~~ - -- The ``title`` command in ``SignalPanel`` was no longer used. It is - still accepted in this release, but will dropped in the next major - release (`#90 `__) - - -v0.2.0 (2018-06-27) -=================== - -Description ------------ - -This ``Typhon`` release marks the transition from prototype to a stable -library. There was a variety of API breaks and deprecations after -``v0.1.0`` as many of the names and functions were not future-proof. - -Enhancements -~~~~~~~~~~~~ - -- ``Typhon`` is now available on the ``pcds-tag`` Anaconda channel - (`#45 `__) -- ``Typhon`` now installs a special data plugin for ``PyDM`` called - ``SignalPlugin``. This uses the generic ``ophyd.Signal`` methods to - communicate information to PyDM widgets. - (`#63 `__) -- ``Typhon`` now supports two different stylesheets a “light” and - “dark” mode. These are not activated by default, but instead can be - accessed via ``use_stylesheet`` function - (`#61 `__, - `#89 `__) -- There is now a sidebar to the ``DeviceDisplay`` that makes adding - devices and tools easier. The ``add_subdisplay`` function still works - but it is preferable to use the more specific ``add_tool`` and - ``add_subdevice``. - (`#61 `__) -- ``Typhon`` will automaticaly create a ``PyDMLogDisplay`` to show the - output of the ``logging.Logger`` object attached to each - ``ophyd.Device`` - (`#70 `__) -- ``Typhon`` now creates a ``PyDMTimePlot`` with the “hinted” - attributes of the Device. This can be configured at runtime to have - fewer or more signals - (`#73 `__) - -API Changes -~~~~~~~~~~~ - -- All of the ``Panel`` objects have been moved to different files. - ``SignalPanel`` now resides in ``typhon.signal`` while the base - ``Panel`` that is no longer used to display signals is in the generic - ``typhon.widgets`` renamed as ``TogglePanel`` - (`#50 `__) - -Deprecations -~~~~~~~~~~~~ - -- ``RotatingImage`` has been removed as it is no longer used by the - library (`#58 `__) -- ``ComponentButton`` has been removed as it is no longer used by the - library(`#58 `__) -- The base ``DeviceDisplay`` no longer has a plot. The - ``add_pv_to_plot`` function has been completely removed. - (`#58 `__) - -Dependencies -~~~~~~~~~~~~ - -- ``TyphonDisplay`` requires ``ophyd >= 1.2.0``. The ``PyDMLogDisplay`` - tool is attached to the ``Device.log`` that is now present on all - ``ophyd`` devices. - (`#53 `__) -- ``pydm >= 1.2.0`` due to various bug fixes and widget additions - (`#63 `__) -- ``QDarkStyleSheet`` is now included in the recipe to provide dark - stylesheet support. - (`#89 `__) - -Bug Fixes -~~~~~~~~~ - -- ``SignalPanel`` previously did not account for the fact that ``read`` - and ``configuration`` attributes could be devices themselves - (`#42 `__) -- ``SignalPanel`` no longer assumes that all signals are - ``EpicsSignal`` objects - (`#71 `__) - - -v0.1.0 (2017-12-15) -=================== - -The initial release of Typhon. This serves as a proof of concept for the -automation of PyDM screen building as informed by the structure of an -Ophyd Device. - -Features --------- - -- Generate a full ``DeviceDisplay`` with all of the device signals and - sub-devices available -- Include methods from the ophyd Device in the User Interface, - automatically parse the arguments to make a widget representation of - the function -- Include ``png`` images associated with devices and sub-devices +See the `releases page on GitHub `_ diff --git a/docs/source/upcoming_changes.rst b/docs/source/upcoming_changes.rst deleted file mode 100644 index 1323597c..00000000 --- a/docs/source/upcoming_changes.rst +++ /dev/null @@ -1,8 +0,0 @@ -Upcoming Changes -################ - -.. toctree:: - :maxdepth: 1 - :glob: - - upcoming_release_notes/[0-9]* diff --git a/docs/source/upcoming_release_notes/635-bug_parametertree_value.rst b/docs/source/upcoming_release_notes/635-bug_parametertree_value.rst deleted file mode 100644 index 49352aa6..00000000 --- a/docs/source/upcoming_release_notes/635-bug_parametertree_value.rst +++ /dev/null @@ -1,22 +0,0 @@ -635 bug_parametertree_value -########################### - -API Breaks ----------- -- N/A - -Features --------- -- N/A - -Bugfixes --------- -- Ensures value is provided at __init__ for all subclasses of pyqtgraph.parametertree.Parameter - -Maintenance ------------ -- N/A - -Contributors ------------- -- tangkong diff --git a/docs/source/upcoming_release_notes/template-full.rst b/docs/source/upcoming_release_notes/template-full.rst deleted file mode 100644 index 31657f76..00000000 --- a/docs/source/upcoming_release_notes/template-full.rst +++ /dev/null @@ -1,36 +0,0 @@ -IssueNumber Title -################# - -Update the title above with your issue number and a 1-2 word title. -Your filename should be issuenumber-title.rst, substituting appropriately. - -Make sure to fill out any section that represents changes you have made, -or replace the default bullet point with N/A. - -API Breaks ----------- -- List backwards-incompatible changes here. - Changes to PVs don't count as API changes for this library, - but changing method and component names or changing default behavior does. - -Features --------- -- List new updates that add utility to many classes, - provide a new base classes, add options to helper methods, etc. - -Bugfixes --------- -- List bug fixes that are not covered in the above sections. - -Maintenance ------------ -- List anything else. The intent is to accumulate changes - that the average user does not need to worry about. - -Contributors ------------- -- List your github username and anyone else who made significant - code or conceptual contributions to the PR. You don't need to - add reviewers unless their suggestions lead to large rewrites. - These will be used in the release notes to give credit and to - notify you when your code is being tagged. diff --git a/docs/source/upcoming_release_notes/template-short.rst b/docs/source/upcoming_release_notes/template-short.rst deleted file mode 100644 index 8349cd8a..00000000 --- a/docs/source/upcoming_release_notes/template-short.rst +++ /dev/null @@ -1,22 +0,0 @@ -IssueNumber Title -################# - -API Breaks ----------- -- N/A - -Features --------- -- N/A - -Bugfixes --------- -- N/A - -Maintenance ------------ -- N/A - -Contributors ------------- -- N/A diff --git a/pyproject.toml b/pyproject.toml index e4b0bfbd..0a978ef9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,10 +5,28 @@ requires = [ "setuptools>=45", "setuptools_scm[toml]>=6.2",] [project] classifiers = [ "Development Status :: 5 - Production/Stable", "Natural Language :: English", "Programming Language :: Python :: 3",] description = "Interface generation for ophyd devices" -dynamic = [ "version", "readme", "dependencies", "optional-dependencies", "optional-dependencies",] +dynamic = [ "version", "readme" ] keywords = [] name = "typhos" -requires-python = ">=3.9" +requires-python = ">=3.12" +dependencies = [ + "coloredlogs", + "entrypoints", + "lxml", + "numpy", + "numpydoc", + "ophyd", + "pcdsutils", + "platformdirs", + "PyQt5", + "pydm>=1.19.1", + "pyqtgraph", + "qdarkstyle", + "qtawesome", + "qtpy", + "timechart", +] + [[project.authors]] name = "SLAC National Accelerator Laboratory" @@ -47,8 +65,34 @@ TyphosSignalPanelPlugin = "typhos.panel:TyphosSignalPanel" HappiPlugin = "typhos.plugins:HappiPlugin" SignalPlugin = "typhos.plugins:SignalPlugin" -[tool.pytest.ini_options] -addopts = "--cov=." +[project.optional-dependencies] +dev = [ + "caproto", + "happi", + "line_profiler", + "lxml-stubs", + "pcdsdevices>=8.4.0", + "ruff>=0.15.11", +] +doc = [ + "docs-versions-menu", + "happi", + "ipython>=7.16", + "sphinx", + "sphinx_rtd_theme", + "sphinxcontrib-jquery", +] +test = [ + "caproto", + "happi", + "line_profiler", + "pcdsdevices>=8.4.0", + "pytest", + "pytest-benchmark", + "pytest-cov", + "pytest-qt", + "pytest-timeout", +] [tool.setuptools.packages.find] where = [ ".",] @@ -59,11 +103,21 @@ namespaces = false file = "README.md" content-type = "text/markdown" -[tool.setuptools.dynamic.dependencies] -file = [ "requirements.txt",] +[tool.ruff] +line-length = 120 +exclude = [".git", "__pycache__", "build", "dist", "*/_version.py"] + +[tool.ruff.lint] +select = ["E", "F", "W", "B", "I"] + +[tool.ruff.lint.pydocstyle] +convention = "numpy" -[tool.setuptools.dynamic.optional-dependencies.test] -file = "dev-requirements.txt" +[tool.pyright.defineConstant] +PYQT5 = true +PYSIDE2 = false +PYQT6 = false +PYSIDE6 = false -[tool.setuptools.dynamic.optional-dependencies.doc] -file = "docs-requirements.txt" +[tool.uv] +exclude-newer = "7 days" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 3fb04fd0..00000000 --- a/requirements.txt +++ /dev/null @@ -1,15 +0,0 @@ -coloredlogs -entrypoints -numpy -numpydoc -ophyd -pcdsutils -platformdirs -PyQt5 -pydm>=1.19.1 -pyqtgraph -pyyaml -qdarkstyle -qtawesome -qtpy -timechart diff --git a/typhos/__init__.py b/typhos/__init__.py index cfaab6d4..687ab0d7 100644 --- a/typhos/__init__.py +++ b/typhos/__init__.py @@ -8,15 +8,15 @@ from .version import __version__ # noqa: F401 __all__ = [ - 'use_stylesheet', - 'register_signal', - 'load_suite', - 'TyphosCompositeSignalPanel', - 'TyphosDeviceDisplay', - 'TyphosSuite', - 'TyphosSignalPanel', - 'TyphosPositionerWidget', - 'TyphosMethodButton', + "use_stylesheet", + "register_signal", + "load_suite", + "TyphosCompositeSignalPanel", + "TyphosDeviceDisplay", + "TyphosSuite", + "TyphosSignalPanel", + "TyphosPositionerWidget", + "TyphosMethodButton", ] diff --git a/typhos/alarm.py b/typhos/alarm.py index b48cd93a..12f10b88 100644 --- a/typhos/alarm.py +++ b/typhos/alarm.py @@ -1,6 +1,7 @@ """ Module to define alarm summary frameworks and widgets. """ + import enum import logging import os @@ -12,15 +13,19 @@ from ophyd.signal import EpicsSignalBase from pydm.widgets.base import PyDMPrimitiveWidget from pydm.widgets.channel import PyDMChannel -from pydm.widgets.drawing import (PyDMDrawing, PyDMDrawingCircle, - PyDMDrawingEllipse, PyDMDrawingPolygon, - PyDMDrawingRectangle, PyDMDrawingTriangle) +from pydm.widgets.drawing import ( + PyDMDrawing, + PyDMDrawingCircle, + PyDMDrawingEllipse, + PyDMDrawingPolygon, + PyDMDrawingRectangle, + PyDMDrawingTriangle, +) from qtpy import QtCore, QtGui, QtWidgets from qtpy.QtCore import Qt from .plugins import register_signal -from .utils import (TyphosObject, channel_from_signal, - get_all_signals_from_device, pyqt_class_from_enum) +from .utils import TyphosObject, channel_from_signal, get_all_signals_from_device, pyqt_class_from_enum from .widgets import HappiChannel logger = logging.getLogger(__name__) @@ -28,6 +33,7 @@ class KindLevel(enum.IntEnum): """Options for TyphosAlarm.kindLevel.""" + HINTED = 0 NORMAL = 1 CONFIG = 2 @@ -40,6 +46,7 @@ class AlarmLevel(enum.IntEnum): These are also the valuess emitted from TyphosAlarm.alarm_changed. """ + NO_ALARM = 0 MINOR = 1 MAJOR = 2 @@ -53,14 +60,10 @@ class AlarmLevel(enum.IntEnum): # Define behavior for the user's Kind selection. KIND_FILTERS = { - KindLevel.HINTED: - (lambda walk: walk.item.kind == Kind.hinted), - KindLevel.NORMAL: - (lambda walk: walk.item.kind in (Kind.hinted, Kind.normal)), - KindLevel.CONFIG: - (lambda walk: walk.item.kind != Kind.omitted), - KindLevel.OMITTED: - (lambda walk: True), + KindLevel.HINTED: (lambda walk: walk.item.kind == Kind.hinted), + KindLevel.NORMAL: (lambda walk: walk.item.kind in (Kind.hinted, Kind.normal)), + KindLevel.CONFIG: (lambda walk: walk.item.kind != Kind.omitted), + KindLevel.OMITTED: (lambda walk: True), } @@ -82,13 +85,13 @@ def alarm(self) -> AlarmLevel: def describe(self) -> str: alarm = self.alarm if alarm == AlarmLevel.NO_ALARM: - desc = f'{self.address} has no alarm' + desc = f"{self.address} has no alarm" elif alarm in (AlarmLevel.DISCONNECTED, AlarmLevel.INVALID): - desc = f'{self.address} is {alarm.name}' + desc = f"{self.address} is {alarm.name}" else: - desc = f'{self.address} has a {alarm.name} alarm' + desc = f"{self.address} has a {alarm.name} alarm" if self.signal_name: - return f'{desc} ({self.signal_name})' + return f"{desc} ({self.signal_name})" else: return desc @@ -103,6 +106,7 @@ class TyphosAlarm(TyphosObject, PyDMDrawing, _KindLevel, _AlarmLevel): We will consider a subset of the signals that is of KindLevel and above and summarize state based on the "worst" alarm we see as defined by AlarmLevel. """ + QtCore.Q_ENUMS(_KindLevel) QtCore.Q_ENUMS(_AlarmLevel) @@ -121,7 +125,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Default drawing properties, can override if needed self.penWidth = 2 - self.penColor = QtGui.QColor('black') + self.penColor = QtGui.QColor("black") self.penStyle = Qt.SolidLine self.reset_alarm_state() self.alarm_changed.connect(self.set_alarm_color) @@ -170,14 +174,14 @@ def channel(self, value): # Remove old connection if self._channels: for channel in self._channels: - if hasattr(channel, 'disconnect'): + if hasattr(channel, "disconnect"): channel.disconnect() if channel in self.signal_info: del self.signal_info[channel] self._channels.clear() # Load new channel self._channel = str(value) - if 'happi://' in self._channel: + if "happi://" in self._channel: channel = HappiChannel( address=self._channel, tx_slot=self._tx, @@ -185,26 +189,24 @@ def channel(self, value): else: channel = PyDMChannel( address=self._channel, - connection_slot=partial(self.update_connection, - addr=self._channel), - severity_slot=partial(self.update_severity, - addr=self._channel), + connection_slot=partial(self.update_connection, addr=self._channel), + severity_slot=partial(self.update_severity, addr=self._channel), ) self.signal_info[self._channel] = SignalInfo( address=self._channel, channel=channel, - signal_name='', + signal_name="", connected=False, severity=AlarmLevel.INVALID, ) self._channels = [channel] # Connect the channel to the HappiPlugin - if hasattr(channel, 'connect'): + if hasattr(channel, "connect"): channel.connect() def _tx(self, value): """Receive information from happi channel""" - self.add_device(value['obj']) + self.add_device(value["obj"]) def reset_alarm_state(self): self.signal_info = {} @@ -244,10 +246,7 @@ def setup_alarm_config(self, device): level, configuring the PyDMChannels to update our alarm state and color when we get updates from our PVs. """ - sigs = get_all_signals_from_device( - device, - filter_by=KIND_FILTERS[self._kind_level] - ) + sigs = get_all_signals_from_device(device, filter_by=KIND_FILTERS[self._kind_level]) channel_addrs = [channel_from_signal(sig) for sig in sigs] for sig in sigs: if not isinstance(sig, EpicsSignalBase): @@ -258,9 +257,10 @@ def setup_alarm_config(self, device): connection_slot=partial(self.update_connection, addr=addr), severity_slot=partial(self.update_severity, addr=addr), ) - for addr in channel_addrs] + for addr in channel_addrs + ] - for ch, sig in zip(channels, sigs): + for ch, sig in zip(channels, sigs, strict=True): info = SignalInfo( address=ch.address, channel=ch, @@ -275,13 +275,13 @@ def setup_alarm_config(self, device): all_channels = self.channels() if all_channels: logger.debug( - f'Finished setup of alarm config for device {device.name} on ' - f'alarm widget with channel {all_channels[0]}.' + f"Finished setup of alarm config for device {device.name} on " + f"alarm widget with channel {all_channels[0]}." ) else: logger.warning( - f'Tried to set up alarm config for device {device.name}, but ' - 'did not configure any channels! Check your kindLevel!' + f"Tried to set up alarm config for device {device.name}, but " + "did not configure any channels! Check your kindLevel!" ) def update_alarm_config(self): @@ -322,12 +322,12 @@ def update_current_alarm(self): self.alarm_changed.emit(new_alarm) except RuntimeError: # Widget was destroyed and not properly cleaned up - logger.debug('Dangling reference to alarm widget!') + logger.debug("Dangling reference to alarm widget!") return else: logger.debug( - f'Updated alarm from {self.alarm_summary} to {new_alarm} ' - f'on alarm widget with channel {self.channels()[0]}' + f"Updated alarm from {self.alarm_summary} to {new_alarm} " + f"on alarm widget with channel {self.channels()[0]}" ) self.alarm_summary = new_alarm @@ -364,25 +364,24 @@ def show_alarm_tooltip(self, event): # Start with the channel field, just show the status. if self.channel in self.signal_info: info = self.signal_info[self.channel] - tooltip_lines.append(f'Channel {info.describe()}') + tooltip_lines.append(f"Channel {info.describe()}") # Handle each device for name, device_info_list in self.device_info.items(): # At least show the device name - tooltip_lines.append(f'Device {name}') + tooltip_lines.append(f"Device {name}") has_alarm = False for info in device_info_list: if info.alarm != AlarmLevel.NO_ALARM: if not has_alarm: has_alarm = True - tooltip_lines.append('-' * 2 * len(tooltip_lines[-1])) + tooltip_lines.append("-" * 2 * len(tooltip_lines[-1])) tooltip_lines.append(info.describe()) if tooltip_lines: tooltip = os.linesep.join(tooltip_lines) QtWidgets.QToolTip.showText( - self.mapToGlobal( - QtCore.QPoint(event.x() + 10, event.y())), + self.mapToGlobal(QtCore.QPoint(event.x() + 10, event.y())), tooltip, self, ) @@ -394,6 +393,7 @@ def show_alarm_tooltip(self, event): # Subclass an re-introduce properties as needed # Each of these must be included for these to work in designer + class TyphosAlarmCircle(TyphosAlarm, PyDMDrawingCircle): QtCore.Q_ENUMS(_KindLevel) kindLevel = TyphosAlarm.kindLevel @@ -437,22 +437,17 @@ def indicator_stylesheet(shape_cls, alarm): indicator_stylesheet : str The correctly colored stylesheet to apply to the widget. """ - base = ( - f'{shape_cls.__name__} ' - '{border: none; ' - ' background: transparent;' - ' qproperty-brush: rgba' - ) + base = f"{shape_cls.__name__} {{border: none; background: transparent; qproperty-brush: rgba" if alarm == AlarmLevel.DISCONNECTED: - return base + '(255,255,255,255);}' + return base + "(255,255,255,255);}" elif alarm == AlarmLevel.NO_ALARM: - return base + '(0,255,0,255);}' + return base + "(0,255,0,255);}" elif alarm == AlarmLevel.MINOR: - return base + '(255,255,0,255);}' + return base + "(255,255,0,255);}" elif alarm == AlarmLevel.MAJOR: - return base + '(255,0,0,255);}' + return base + "(255,0,0,255);}" elif alarm == AlarmLevel.INVALID: - return base + '(255,0,255,255);}' + return base + "(255,0,255,255);}" else: - raise ValueError(f'Recieved invalid alarm level {alarm}') + raise ValueError(f"Recieved invalid alarm level {alarm}") diff --git a/typhos/app.py b/typhos/app.py index bdd6344f..33a42c07 100644 --- a/typhos/app.py +++ b/typhos/app.py @@ -1,4 +1,5 @@ """This module defines methods for launching full typhos applications.""" + import logging from typing import Optional @@ -24,10 +25,7 @@ def get_qapp(): return qapp -def launch_suite( - suite: TyphosSuite, - initial_size: Optional[QSize] = None -) -> QMainWindow: +def launch_suite(suite: TyphosSuite, initial_size: Optional[QSize] = None) -> QMainWindow: """ Creates a main window and execs the application. diff --git a/typhos/benchmark/cases.py b/typhos/benchmark/cases.py index c94f7ac5..7f220d53 100644 --- a/typhos/benchmark/cases.py +++ b/typhos/benchmark/cases.py @@ -4,6 +4,7 @@ These are included as standalone functions to make it easy to pass them into arbitrary profiling modules. """ + from __future__ import annotations import typing @@ -25,18 +26,14 @@ # Define matrix of testing parameters -Shape = namedtuple('Shape', ['num_signals', 'subdevice_layers', - 'subdevice_spread']) +Shape = namedtuple("Shape", ["num_signals", "subdevice_layers", "subdevice_spread"]) # total_signals == num_signals * (subdevice_spread ** subdevice_layers) -SHAPES = dict(flat=Shape(100, 1, 1), - deep=Shape(100, 25, 1), - wide=Shape(1, 1, 100), - cube=Shape(4, 2, 5)) +SHAPES = dict(flat=Shape(100, 1, 1), deep=Shape(100, 25, 1), wide=Shape(1, 1, 100), cube=Shape(4, 2, 5)) -Test = namedtuple('Test', ['signal_class', 'include_prefix', 'start_ioc']) -TESTS = dict(soft=Test(Signal, False, False), - connect=Test(EpicsSignal, True, True), - noconnect=Test(EpicsSignal, True, False)) +Test = namedtuple("Test", ["signal_class", "include_prefix", "start_ioc"]) +TESTS = dict( + soft=Test(Signal, False, False), connect=Test(EpicsSignal, True, True), noconnect=Test(EpicsSignal, True, False) +) def profiler_benchmark( @@ -54,8 +51,7 @@ def profiler_benchmark( """ prefix = random_prefix() with benchmark_context(start_ioc, cls, prefix, full_test_name, request=request): - return launch_from_devices([cls(prefix, name='test')], - auto_exit=auto_exit) + return launch_from_devices([cls(prefix, name="test")], auto_exit=auto_exit) def unittest_benchmark( @@ -73,7 +69,7 @@ def unittest_benchmark( """ prefix = random_prefix() context = benchmark_context(start_ioc, cls, prefix, full_test_name, request=request) - suite = TyphosSuite.from_device(cls(prefix, name='test')) + suite = TyphosSuite.from_device(cls(prefix, name="test")) return suite, context @@ -99,14 +95,16 @@ def make_tests(): unit_tests = {} for shape_name, shape in SHAPES.items(): for test_name, test in TESTS.items(): - full_test_name = shape_name + '_' + test_name + full_test_name = shape_name + "_" + test_name cls_name = full_test_name - cls = make_cls(name=cls_name, - signal_class=test.signal_class, - include_prefix=test.include_prefix, - num_signals=shape.num_signals, - subdevice_layers=shape.subdevice_layers, - subdevice_spread=shape.subdevice_spread) + cls = make_cls( + name=cls_name, + signal_class=test.signal_class, + include_prefix=test.include_prefix, + num_signals=shape.num_signals, + subdevice_layers=shape.subdevice_layers, + subdevice_spread=shape.subdevice_spread, + ) classes[cls_name] = cls profiler_test = partial(profiler_benchmark, cls, test.start_ioc, full_test_name) @@ -140,7 +138,7 @@ def interactive_benchmark(benchmark): def get_profiler_test(benchmark): try: return profiler_tests[benchmark] - except KeyError: - raise RuntimeError(f'{benchmark} is not a valid benchmark. ' - 'The full list of valid benchmarks is ' - f'{list(profiler_tests.keys())}') + except KeyError as exc: + raise RuntimeError( + f"{benchmark} is not a valid benchmark. The full list of valid benchmarks is {list(profiler_tests.keys())}" + ) from exc diff --git a/typhos/benchmark/device.py b/typhos/benchmark/device.py index 0d5c173b..d8693709 100644 --- a/typhos/benchmark/device.py +++ b/typhos/benchmark/device.py @@ -9,14 +9,15 @@ In the future it can be expanded to have Kind information, different data types to test different widget types, etc. """ + from ophyd.device import Component as Cpt from ophyd.device import create_device_from_components as create_device from ophyd.signal import Signal -def make_test_device_class(name='TestClass', signal_class=Signal, - include_prefix=False, num_signals=10, - subdevice_layers=0, subdevice_spread=0): +def make_test_device_class( + name="TestClass", signal_class=Signal, include_prefix=False, num_signals=10, subdevice_layers=0, subdevice_spread=0 +): """ Creates a test :class:`ophyd.Device` subclass. @@ -60,20 +61,20 @@ def make_test_device_class(name='TestClass', signal_class=Signal, signals = {} for nsig in range(num_signals): if include_prefix: - sig_cpt = Cpt(signal_class, f'SIGPV{nsig}') + sig_cpt = Cpt(signal_class, f"SIGPV{nsig}") else: sig_cpt = Cpt(signal_class) - signals[f'signum{nsig}'] = sig_cpt + signals[f"signum{nsig}"] = sig_cpt - SignalHolder = create_device('SignalHolder', **signals) + SignalHolder = create_device("SignalHolder", **signals) if all((subdevice_layers > 0, subdevice_spread > 0)): PrevClass = SignalHolder while subdevice_layers > 0: subdevices = {} for ndev in range(subdevice_spread): - subdevices[f'devnum{ndev}'] = Cpt(PrevClass, f'PREFIX{ndev}:') - ThisClass = create_device(f'Layer{subdevice_layers}', **subdevices) + subdevices[f"devnum{ndev}"] = Cpt(PrevClass, f"PREFIX{ndev}:") + ThisClass = create_device(f"Layer{subdevice_layers}", **subdevices) PrevClass = ThisClass subdevice_layers -= 1 else: diff --git a/typhos/benchmark/ioc.py b/typhos/benchmark/ioc.py index ecec3cb9..e1227ff5 100644 --- a/typhos/benchmark/ioc.py +++ b/typhos/benchmark/ioc.py @@ -22,6 +22,7 @@ * wide_noconnect * wide_soft """ + from __future__ import annotations import logging @@ -58,8 +59,8 @@ def get_suffix(walk: ophyd.device.ComponentWalk) -> str: subclass and that it was defined using only :class:`ophyd.Component` in the device ancestors tree. """ - suffix = '' - for cls, attr in zip(walk.ancestors, walk.dotted_name.split('.')): + suffix = "" + for cls, attr in zip(walk.ancestors, walk.dotted_name.split("."), strict=True): suffix += getattr(cls, attr).suffix return suffix @@ -96,9 +97,7 @@ def run_caproto_ioc(prefix: str, test_name: str) -> None: pvprops[suffix] = pvproperty() print( - f"Running caproto IOC for test: {test_name} " - f"with prefix {prefix!r} " - f"Total PVs: {len(pvprops)}", + f"Running caproto IOC for test: {test_name} with prefix {prefix!r} Total PVs: {len(pvprops)}", ) try: diff --git a/typhos/benchmark/profile.py b/typhos/benchmark/profile.py index 843a7c2d..a58c227f 100644 --- a/typhos/benchmark/profile.py +++ b/typhos/benchmark/profile.py @@ -1,6 +1,7 @@ """ Module using line_profiler to measure code performance and diagnose slowdowns. """ + import logging import warnings from contextlib import contextmanager @@ -9,10 +10,10 @@ logger = logging.getLogger(__name__) -_optional_err = ('Optional dependency line_profiler missing from python ' - 'environment. Cannot run profiler.') +_optional_err = "Optional dependency line_profiler missing from python environment. Cannot run profiler." try: from line_profiler import LineProfiler + has_line_profiler = True except ImportError: has_line_profiler = False @@ -58,7 +59,7 @@ def setup_profiler(module_names=None): e.g. module_names=['typhos.display']. """ if module_names is None: - module_names = ['typhos'] + module_names = ["typhos"] profiler = get_profiler() @@ -87,7 +88,7 @@ def toggle_profiler(turn_on): def save_results(filename): """Saves the formatted profiling results to filename.""" profiler = get_profiler() - with open(filename, 'w') as fd: + with open(filename, "w") as fd: profiler.print_stats(fd, stripzeros=True, output_unit=1e-3) diff --git a/typhos/benchmark/utils.py b/typhos/benchmark/utils.py index c0330337..ec9ba50a 100644 --- a/typhos/benchmark/utils.py +++ b/typhos/benchmark/utils.py @@ -1,6 +1,7 @@ """ Helpful functions that don't belong in a more specific submodule. """ + from __future__ import annotations import importlib @@ -22,11 +23,11 @@ logger = logging.getLogger(__name__) -_optional_err = ('Optional dependency caproto missing from python ' - 'environment. Cannot test server.') +_optional_err = "Optional dependency caproto missing from python environment. Cannot test server." try: from caproto.tests.conftest import run_example_ioc + has_caproto = True except ImportError: has_caproto = False @@ -60,7 +61,7 @@ def caproto_context( def random_prefix(): """Returns a random prefix to avoid test cross-talk.""" - return str(uuid.uuid4())[:8] + ':' + return str(uuid.uuid4())[:8] + ":" def is_native(obj, module): @@ -99,8 +100,7 @@ def get_native_methods(cls, module, *, native_methods=None, seen=None): if not is_native(obj, module): continue elif isclass(obj): - get_native_methods(obj, module, native_methods=native_methods, - seen=seen) + get_native_methods(obj, module, native_methods=native_methods, seen=seen) elif isfunction(obj): native_methods.add(obj) elif ismethod(obj): @@ -128,8 +128,8 @@ def get_submodule_names(module_name): return submodule_names for info, submodule_name, is_pkg in pkgutil.walk_packages(module_path): - if submodule_name != '__main__' and info.path in module_path: - full_submodule_name = module_name + '.' + submodule_name + if submodule_name != "__main__" and info.path in module_path: + full_submodule_name = module_name + "." + submodule_name submodule_names.append(full_submodule_name) if is_pkg: subsubmodule_names = get_submodule_names(full_submodule_name) diff --git a/typhos/cache.py b/typhos/cache.py index ed97f0ff..6fad9006 100644 --- a/typhos/cache.py +++ b/typhos/cache.py @@ -72,8 +72,7 @@ def __init__(self): self.cache = {} self.connect_thread = utils.ObjectConnectionMonitorThread(parent=self) - self.connect_thread.connection_update.connect( - self._connection_update, QtCore.Qt.QueuedConnection) + self.connect_thread.connection_update.connect(self._connection_update, QtCore.Qt.QueuedConnection) self.connect_thread.start() @@ -88,10 +87,8 @@ def _describe(self, obj): try: return obj.describe()[obj.name] except Exception: - logger.error("Unable to connect to %r during widget creation", - obj.name) - logger.debug("Unable to connect to %r during widget creation", - obj.name, exc_info=True) + logger.error("Unable to connect to %r during widget creation", obj.name) + logger.debug("Unable to connect to %r during widget creation", obj.name, exc_info=True) return {} def _worker_describe(self, obj): @@ -109,7 +106,7 @@ def _worker_describe(self, obj): if obj in self._in_process: self.new_description.emit(obj, desc) except Exception as ex: - logger.exception('Worker describe failed: %s', ex) + logger.exception("Worker describe failed: %s", ex) finally: try: self._in_process.remove(obj) @@ -130,9 +127,7 @@ def _connection_update(self, obj, connected, metadata): self._in_process.add(obj) func = functools.partial(self._worker_describe, obj) - QtCore.QThreadPool.globalInstance().start( - utils.ThreadPoolWorker(func) - ) + QtCore.QThreadPool.globalInstance().start(utils.ThreadPoolWorker(func)) def get(self, obj): """ @@ -186,8 +181,7 @@ def __init__(self): super().__init__() self.cache = {} self.describe_cache = get_global_describe_cache() - self.describe_cache.new_description.connect(self._new_description, - QtCore.Qt.QueuedConnection) + self.describe_cache.new_description.connect(self._new_description, QtCore.Qt.QueuedConnection) def clear(self): """Clear the cache.""" @@ -202,7 +196,7 @@ def _new_description(self, obj, desc): return item = SignalWidgetInfo.from_signal(obj, desc) - logger.debug('Determined widgets for %s: %s', obj.name, item) + logger.debug("Determined widgets for %s: %s", obj.name, item) self.cache[obj] = item self.widgets_determined.emit(obj, item) @@ -234,9 +228,7 @@ def get(self, obj): # The default stale cached_path threshold time, in seconds: -TYPHOS_DISPLAY_PATH_CACHE_TIME = int( - os.environ.get('TYPHOS_DISPLAY_PATH_CACHE_TIME', '600') -) +TYPHOS_DISPLAY_PATH_CACHE_TIME = int(os.environ.get("TYPHOS_DISPLAY_PATH_CACHE_TIME", "600")) class _CachedPath: @@ -261,8 +253,7 @@ class _CachedPath: happens on the next glob, and not on a timer-basis. """ - def __init__(self, path, *, - stale_threshold=TYPHOS_DISPLAY_PATH_CACHE_TIME): + def __init__(self, path, *, stale_threshold=TYPHOS_DISPLAY_PATH_CACHE_TIME): self.path = pathlib.Path(path) self.cache = None self._update_time = None @@ -306,7 +297,7 @@ def glob(self, pattern): elif self.time_since_last_update > self.stale_threshold: self.update() - if any(c in pattern for c in '*?['): + if any(c in pattern for c in "*?["): # Convert from glob syntax -> regular expression # And compile it for repeated usage. regex = re.compile(fnmatch.translate(pattern)) @@ -335,7 +326,7 @@ def __init__(self): def update(self): """Force a reload of all paths in the cache.""" - logger.debug('Clearing global path cache.') + logger.debug("Clearing global path cache.") for path in self.paths: path.cache = None @@ -348,9 +339,8 @@ def add_path(self, path): path : pathlib.Path or str The path to add. """ - logger.debug('Path added to _GlobalDisplayPathCache: %s', path) + logger.debug("Path added to _GlobalDisplayPathCache: %s", path) path = pathlib.Path(path).expanduser().resolve() - path = _CachedPath( - path, stale_threshold=TYPHOS_DISPLAY_PATH_CACHE_TIME) + path = _CachedPath(path, stale_threshold=TYPHOS_DISPLAY_PATH_CACHE_TIME) if path not in self.paths: self.paths.append(path) diff --git a/typhos/cli.py b/typhos/cli.py index 341731bf..96111f90 100644 --- a/typhos/cli.py +++ b/typhos/cli.py @@ -1,4 +1,5 @@ """This module defines the ``typhos`` command line utility""" + from __future__ import annotations import argparse @@ -22,7 +23,8 @@ from .app import get_qapp, launch_suite from .benchmark.cases import run_benchmarks from .benchmark.profile import profiler_context -from .display import DisplayTypes, ScrollOptions +from .display import DisplayTypes, ScrollOptions, TyphosDeviceDisplay +from .export import export_as_ui from .suite import TyphosSuite from .utils import apply_standard_stylesheets, compose_stylesheets, nullcontext @@ -55,111 +57,102 @@ class TyphosArguments(types.SimpleNamespace): # Argument Parser Setup parser = argparse.ArgumentParser( - description=( - 'Create a TyphosSuite for device/s stored in a Happi Database' - ), + description=("Create a TyphosSuite for device/s stored in a Happi Database"), ) parser.add_argument( - 'devices', - nargs='*', + "devices", + nargs="*", help=( - 'Device names to load in the TyphosSuite or class name with ' + "Device names to load in the TyphosSuite or class name with " 'parameters on the format: package.ClassName[{"param1":"val1",...}]' ), ) parser.add_argument( - '--layout', - default='vertical', + "--layout", + default="vertical", help=( - 'Select a alternate layout for suites of many ' + "Select a alternate layout for suites of many " 'devices. Valid options are "horizontal", ' '"vertical" (default), "grid", "flow", and any unique ' - 'shortenings of those options.' + "shortenings of those options." ), ) parser.add_argument( - '--cols', + "--cols", default=3, help=( - 'The number of columns to use for the grid layout ' - 'if selected in the layout argument. This will have ' - 'no effect for other layouts.' + "The number of columns to use for the grid layout " + "if selected in the layout argument. This will have " + "no effect for other layouts." ), ) parser.add_argument( - '--display-type', - default='embedded', + "--display-type", + default="embedded", help=( - 'The kind of display to open for each device at ' + "The kind of display to open for each device at " 'initial load. Valid options are "embedded" (default), ' '"detailed", "engineering", and any ' - 'unique shortenings of those options.' + "unique shortenings of those options." ), ) parser.add_argument( - '--scrollable', - default='auto', + "--scrollable", + default="auto", help=( - 'Whether or not to include the scrollbar. ' + "Whether or not to include the scrollbar. " 'Valid options are "auto", "true", "false", ' - 'and any unique shortenings of those options. ' + "and any unique shortenings of those options. " 'Selecting "auto" will include a scrollbar for ' - 'non-embedded layouts.' + "non-embedded layouts." ), ) parser.add_argument( - '--size', + "--size", help=( - 'A starting x,y size for the typhos suite. ' - 'Useful if the default size is not suitable for ' - 'your application. Example: --size 1000,1000' + "A starting x,y size for the typhos suite. " + "Useful if the default size is not suitable for " + "your application. Example: --size 1000,1000" ), ) parser.add_argument( - '--hide-displays', - action='store_true', - help=( - 'Option to start with subdisplays hidden instead ' - 'of shown.' - ) + "--hide-displays", action="store_true", help=("Option to start with subdisplays hidden instead of shown.") ) parser.add_argument( - '--happi-cfg', - help=( - 'Location of happi configuration file ' - 'if not specified by $HAPPI_CFG environment variable' - ), + "--happi-cfg", + help=("Location of happi configuration file if not specified by $HAPPI_CFG environment variable"), ) parser.add_argument( - '--fake-device', - action='store_true', + "--fake-device", + action="store_true", help=( - 'Create fake devices with no EPICS connections. ' - 'This does not yet work for happi devices. An ' - 'example invocation: ' - 'typhos --fake-device ophyd.EpicsMotor[]' + "Create fake devices with no EPICS connections. " + "This does not yet work for happi devices. An " + "example invocation: " + "typhos --fake-device ophyd.EpicsMotor[]" ), ) parser.add_argument( - '--version', - '-V', - action='store_true', - help='Current version and location ' 'of Typhos installation.', + "--version", + "-V", + action="store_true", + help="Current version and location of Typhos installation.", ) parser.add_argument( - '--verbose', - '-v', - action='store_true', - help='Show the debug logging stream', + "--verbose", + "-v", + action="store_true", + help="Show the debug logging stream", ) parser.add_argument( - '--dark', - action='store_true', - help='Use the QDarkStyleSheet shipped with Typhos', + "--dark", + action="store_true", + help="Use the QDarkStyleSheet shipped with Typhos", ) parser.add_argument( - "--stylesheet-override", "--stylesheet", + "--stylesheet-override", + "--stylesheet", action="append", help="Override all built-in stylesheets, using this stylesheet instead.", ) @@ -170,47 +163,40 @@ class TyphosArguments(types.SimpleNamespace): "Include an additional stylesheet in the loading process. " "This stylesheet will take priority over all built-in stylesheets, " "but not over a template or widget's styleSheet property." - ) + ), ) parser.add_argument( - '--profile-modules', - nargs='*', + "--profile-modules", + nargs="*", help=( - 'Submodules to profile during the execution. ' - 'If no specific modules are specified, ' - 'profiles all submodules of typhos. ' - 'Turns on line profiling.' + "Submodules to profile during the execution. " + "If no specific modules are specified, " + "profiles all submodules of typhos. " + "Turns on line profiling." ), ) parser.add_argument( - '--profile-output', - help=( - 'Filename to output the profile results to. ' - 'If omitted, prints results to stdout. ' - 'Turns on line profiling.' - ), + "--profile-output", + help=("Filename to output the profile results to. If omitted, prints results to stdout. Turns on line profiling."), ) parser.add_argument( - '--benchmark', - nargs='*', + "--benchmark", + nargs="*", help=( - 'Runs the specified benchmarking tests instead of ' - 'launching a screen. ' - 'If no specific tests are specified, ' - 'runs all of them. ' - 'Turns on line profiling.' + "Runs the specified benchmarking tests instead of " + "launching a screen. " + "If no specific tests are specified, " + "runs all of them. " + "Turns on line profiling." ), ) parser.add_argument( - '--exit-after', + "--exit-after", type=float, - help=( - "(For profiling purposes) Exit typhos after the provided number of " - "seconds" - ), + help=("(For profiling purposes) Exit typhos after the provided number of seconds"), ) parser.add_argument( - '--screenshot', + "--screenshot", dest="screenshot_filename", help=( "Save screenshot(s) of all contained TyphosDeviceDisplay instances to " @@ -219,26 +205,26 @@ class TyphosArguments(types.SimpleNamespace): "device, and name." ), ) +parser.add_argument( + "--export", default="", help="Instead of loading a suite, export the first device as a pure pydm ui file." +) # Append to module docs -__doc__ += '\n::\n\n ' + parser.format_help().replace('\n', '\n ') +__doc__ += "\n::\n\n " + parser.format_help().replace("\n", "\n ") # type: ignore def typhos_cli_setup(args): """Setup logging and style.""" # Logging Level handling logging.getLogger().addHandler(logging.NullHandler()) - shown_logger = logging.getLogger('typhos') + shown_logger = logging.getLogger("typhos") if args.verbose: level = "DEBUG" - log_fmt = ( - '[%(asctime)s] - %(levelname)s - Thread (%(thread)d - ' - '%(threadName)s ) - %(name)s -> %(message)s' - ) + log_fmt = "[%(asctime)s] - %(levelname)s - Thread (%(thread)d - %(threadName)s ) - %(name)s -> %(message)s" else: level = "INFO" - log_fmt = '[%(asctime)s] - %(levelname)s - %(message)s' + log_fmt = "[%(asctime)s] - %(levelname)s - %(message)s" coloredlogs.install(level=level, logger=shown_logger, fmt=log_fmt) logger.debug("Set logging level of %r to %r", shown_logger.name, level) @@ -279,10 +265,10 @@ def create_suite( device_names: list[str], cfg: Optional[str] = None, fake_devices: bool = False, - layout: str = 'horizontal', + layout: str = "horizontal", cols: int = 3, - display_type: str = 'detailed', - scroll_option: str = 'auto', + display_type: str = "detailed", + scroll_option: str = "auto", show_displays: bool = True, ) -> TyphosSuite: """ @@ -321,9 +307,7 @@ def create_suite( A suite that has been populated with devices. """ if device_names: - devices = create_devices( - device_names, cfg=cfg, fake_devices=fake_devices - ) + devices = create_devices(device_names, cfg=cfg, fake_devices=fake_devices) else: devices = [] if devices or not device_names: @@ -359,19 +343,17 @@ def get_layout_from_cli( qlayout : QLayout The qt layout object, instantiated. """ - if 'horizontal'.startswith(layout): + if "horizontal".startswith(layout): return QtWidgets.QHBoxLayout() - if 'vertical'.startswith(layout): + if "vertical".startswith(layout): return QtWidgets.QVBoxLayout() - if 'grid'.startswith(layout): + if "grid".startswith(layout): return FixedColGrid(cols=cols) - if 'flow'.startswith(layout): + if "flow".startswith(layout): return FlowLayout() else: raise ValueError( - f'{layout} is not a valid layout name. ' - 'The allowed values are "horizontal", ' - '"vertical", "grid", and "flow".' + f'{layout} is not a valid layout name. The allowed values are "horizontal", "vertical", "grid", and "flow".' ) @@ -410,33 +392,27 @@ def addWidget( def get_display_type_from_cli(display_type: str) -> DisplayTypes: """Convert the cli string to the appropriate DisplayTypes enum.""" display_type = display_type.lower() - if 'embedded'.startswith(display_type): + if "embedded".startswith(display_type): return DisplayTypes.embedded_screen - if 'detailed'.startswith(display_type): + if "detailed".startswith(display_type): return DisplayTypes.detailed_screen - if 'engineering'.startswith(display_type): + if "engineering".startswith(display_type): return DisplayTypes.engineering_screen raise ValueError( - f'{display_type} is not a valid display type. ' - 'The allowed values are "embedded", "detailed", ' - 'and "engineering".' + f'{display_type} is not a valid display type. The allowed values are "embedded", "detailed", and "engineering".' ) def get_scrollable_from_cli(scrollable: str) -> ScrollOptions: """Convert the cli string to the appropriate ScrollOptions enum.""" scrollable = scrollable.lower() - if 'auto'.startswith(scrollable): + if "auto".startswith(scrollable): return ScrollOptions.auto - if 'true'.startswith(scrollable): + if "true".startswith(scrollable): return ScrollOptions.scrollbar - if 'false'.startswith(scrollable): + if "false".startswith(scrollable): return ScrollOptions.no_scroll - raise ValueError( - f'{scrollable} is not a valid scroll option. ' - 'The allowed values are "auto", "true", ' - 'and "false".' - ) + raise ValueError(f'{scrollable} is not a valid scroll option. The allowed values are "auto", "true", and "false".') def create_devices(device_names, cfg=None, fake_devices=False): @@ -453,7 +429,7 @@ def create_devices(device_names, cfg=None, fake_devices=False): devices = list() klass_regex = re.compile( - r'([a-zA-Z][a-zA-Z0-9\.\_]*)\[(\{.*})*[\,]*\]' # noqa + r"([a-zA-Z][a-zA-Z0-9\.\_]*)\[(\{.*})*[\,]*\]" # noqa ) for device_name in device_names: @@ -474,19 +450,17 @@ def create_devices(device_names, cfg=None, fake_devices=False): # Give default value to missing positional args # This might fail, but is best effort for arg in inspect.getfullargspec(klass).args: - if arg not in default_kwargs and arg != 'self': - if arg == 'prefix': - default_kwargs[arg] = 'FAKE_PREFIX:' + if arg not in default_kwargs and arg != "self": + if arg == "prefix": + default_kwargs[arg] = "FAKE_PREFIX:" else: - default_kwargs[arg] = 'FAKE' + default_kwargs[arg] = "FAKE" device = klass(**default_kwargs) devices.append(device) except Exception: - logger.exception( - "Unable to load class entry: %s with args %s", klass, args - ) + logger.exception("Unable to load class entry: %s with args %s", klass, args) continue else: if not happi_client: @@ -496,16 +470,12 @@ def create_devices(device_names, cfg=None, fake_devices=False): ) continue if fake_devices: - raise NotImplementedError( - "Fake devices from happi not " "supported yet" - ) + raise NotImplementedError("Fake devices from happi not supported yet") try: device = happi_client.load_device(name=device_name) devices.append(device) except Exception: - logger.exception( - "Unable to load Happi entry: %r", device_name - ) + logger.exception("Unable to load Happi entry: %r", device_name) if fake_devices: clear_fake_device(device) return devices @@ -515,10 +485,10 @@ def typhos_run( device_names: list[str], cfg: Optional[str] = None, fake_devices: bool = False, - layout: str = 'horizontal', + layout: str = "horizontal", cols: int = 3, - display_type: str = 'detailed', - scroll_option: str = 'auto', + display_type: str = "detailed", + scroll_option: str = "auto", initial_size: Optional[str] = None, show_displays: bool = True, exit_after: Optional[float] = None, @@ -585,9 +555,7 @@ def typhos_run( if initial_size is not None: try: - initial_size = QtCore.QSize( - *(int(opt) for opt in initial_size.split(',')) - ) + initial_size = QtCore.QSize(*(int(opt) for opt in initial_size.split(","))) except TypeError as exc: raise ValueError( "Invalid --size argument. Expected a two-element pair " @@ -595,10 +563,7 @@ def typhos_run( ) from exc def exit_early(): - logger.warning( - "Exiting typhos early due to --exit-after=%s CLI argument.", - exit_after - ) + logger.warning("Exiting typhos early due to --exit-after=%s CLI argument.", exit_after) if screenshot_filename is not None: suite.save_device_screenshots(screenshot_filename) @@ -611,13 +576,53 @@ def exit_early(): return launch_suite(suite, initial_size=initial_size) +def typhos_export( + device_name: str, + export_filename: str, + cfg: Optional[str] = None, + fake_devices: bool = False, + display_type: str = "detailed", + scroll_option: str = "auto", +): + """ + Export a device display as a pydm-compatible ui file. + + Parameters + ---------- + device_name : str + The happi name associated with the device to instantiate, + or the full class specification from the cli. + export_filename : str, optional + Filepath to use as the target for ui file export. + cfg : str, optional + The happi configuration file to use. If omitted, uses + the environment variables specified by happi. + fake_devices : bool, optional + If True, use fake devices behind the screen instead of + making real connections. + display_type : str, optional + The type of display to use in the suite. See the + cli help for valid options. + scroll_option : str, optional + Options for the scrollbar. See the cli help for valid options. + """ + with utils.no_device_lazy_load(): + devices = create_devices([device_name], cfg=cfg, fake_devices=fake_devices) + display = TyphosDeviceDisplay.from_device( + device=devices[0], + scroll_option=get_scrollable_from_cli(scroll_option), + display_type=get_display_type_from_cli(display_type), + ) + return export_as_ui(display, export_filename=export_filename) + + def typhos_cli(args): """Command Line Application for Typhos.""" args = parser.parse_args(args, TyphosArguments()) if args.version: typhos_file = sys.modules["typhos"].__file__ - print(f'Typhos: Version {typhos_version} from {typhos_file}') + print(f"Typhos: Version {typhos_version} from {typhos_file}") return if any( @@ -642,8 +647,16 @@ def typhos_cli(args): if args.benchmark is not None: # Note: actually a list of suites suite = run_benchmarks(args.benchmark) + elif args.export: + suite = typhos_export( + device_name=args.devices[0], + export_filename=args.export, + cfg=args.happi_cfg, + fake_devices=args.fake_device, + display_type=args.display_type, + scroll_option=args.scrollable, + ) else: - suite = typhos_run( args.devices, cfg=args.happi_cfg, diff --git a/typhos/display.py b/typhos/display.py index 8150958a..dbb52eb0 100644 --- a/typhos/display.py +++ b/typhos/display.py @@ -1,4 +1,5 @@ """Contains the main display widget used for representing an entire device.""" + from __future__ import annotations import copy @@ -21,9 +22,8 @@ from qtpy import QtCore, QtGui, QtWidgets from qtpy.QtCore import Q_ENUMS, Property, Qt, Slot -from . import cache +from . import cache, utils, web, widgets from . import panel as typhos_panel -from . import utils, web, widgets from .jira import TyphosJiraIssueWidget from .notes import TyphosNotesEdit from .plugins.core import register_signal @@ -64,21 +64,15 @@ class ScrollOptions(enum.IntEnum): ScrollOptions.names = [view.name for view in ScrollOptions] -DEFAULT_TEMPLATES = { - name: [(utils.ui_dir / 'core' / f'{name}.ui').resolve()] - for name in DisplayTypes.names -} +DEFAULT_TEMPLATES = {name: [(utils.ui_dir / "core" / f"{name}.ui").resolve()] for name in DisplayTypes.names} -DETAILED_TREE_TEMPLATE = (utils.ui_dir / 'core' / 'detailed_tree.ui').resolve() -DEFAULT_TEMPLATES['detailed_screen'].append(DETAILED_TREE_TEMPLATE) +DETAILED_TREE_TEMPLATE = (utils.ui_dir / "core" / "detailed_tree.ui").resolve() +DEFAULT_TEMPLATES["detailed_screen"].append(DETAILED_TREE_TEMPLATE) -DEFAULT_TEMPLATES_FLATTEN = [f for _, files in DEFAULT_TEMPLATES.items() - for f in files] +DEFAULT_TEMPLATES_FLATTEN = [f for _, files in DEFAULT_TEMPLATES.items() for f in files] -def normalize_display_type( - display_type: Union[DisplayTypes, str, int] -) -> DisplayTypes: +def normalize_display_type(display_type: Union[DisplayTypes, str, int]) -> DisplayTypes: """ Normalize a given display type. @@ -101,16 +95,12 @@ def normalize_display_type( return DisplayTypes(display_type) except ValueError: try: - return DisplayTypes[display_type] - except KeyError: - raise ValueError( - f'Unrecognized display type: {display_type}' - ) + return DisplayTypes[display_type] # type: ignore + except KeyError as exc: + raise ValueError(f"Unrecognized display type: {display_type}") from exc -def normalize_scroll_option( - scroll_option: Union[ScrollOptions, str, int] -) -> ScrollOptions: +def normalize_scroll_option(scroll_option: Union[ScrollOptions, str, int]) -> ScrollOptions: """ Normalize a given scroll option. @@ -133,11 +123,9 @@ def normalize_scroll_option( return ScrollOptions(scroll_option) except ValueError: try: - return ScrollOptions[scroll_option] - except KeyError: - raise ValueError( - f'Unrecognized scroll option: {scroll_option}' - ) + return ScrollOptions[scroll_option] # type: ignore + except KeyError as exc: + raise ValueError(f"Unrecognized scroll option: {scroll_option}") from exc class TyphosToolButton(QtWidgets.QToolButton): @@ -158,7 +146,7 @@ class TyphosToolButton(QtWidgets.QToolButton): The default icon from fontawesome to use. """ - DEFAULT_ICON = 'circle' + DEFAULT_ICON = "circle" def __init__(self, icon=None, *, parent=None): super().__init__(parent=parent) @@ -215,7 +203,7 @@ class TyphosDisplayConfigButton(TyphosToolButton): This uses the common "vertical ellipse" icon by default. """ - DEFAULT_ICON = 'ellipsis-v' + DEFAULT_ICON = "ellipsis-v" _kind_to_property = typhos_panel.TyphosSignalPanel._kind_to_property @@ -247,11 +235,12 @@ def create_kind_filter_menu(self, panels, base_menu, *, only): True - create "Show only Kind" actions. """ for kind, prop in self._kind_to_property.items(): + def selected(new_value, *, prop=prop): if only: # Show *only* the specific kind for all panels - for kind, current_prop in self._kind_to_property.items(): - visible = (current_prop == prop) + for _, current_prop in self._kind_to_property.items(): + visible = current_prop == prop for panel in panels: setattr(panel, current_prop, visible) else: @@ -260,12 +249,11 @@ def selected(new_value, *, prop=prop): setattr(panel, prop, new_value) self.hide_empty() - title = f'Show only &{kind}' if only else f'Show &{kind}' + title = f"Show only &{kind}" if only else f"Show &{kind}" action = base_menu.addAction(title) if not only: action.setCheckable(True) - action.setChecked(all(getattr(panel, prop) - for panel in panels)) + action.setChecked(all(getattr(panel, prop) for panel in panels)) action.triggered.connect(selected) def create_name_filter_menu(self, panels, base_menu): @@ -280,6 +268,7 @@ def create_name_filter_menu(self, panels, base_menu): base_menu : QMenu The menu to add actions to. """ + def text_filter_updated(): text = line_edit.text().strip() for panel in panels: @@ -288,17 +277,16 @@ def text_filter_updated(): line_edit = QtWidgets.QLineEdit() - filters = list({panel.nameFilter for panel in panels - if panel.nameFilter}) + filters = list({panel.nameFilter for panel in panels if panel.nameFilter}) if len(filters) == 1: line_edit.setText(filters[0]) else: - line_edit.setPlaceholderText('/ '.join(filters)) + line_edit.setPlaceholderText("/ ".join(filters)) line_edit.editingFinished.connect(text_filter_updated) - line_edit.setObjectName('menu_action') + line_edit.setObjectName("menu_action") - action = base_menu.addAction('Filter by name:') + action = base_menu.addAction("Filter by name:") action.setEnabled(False) action = QtWidgets.QWidgetAction(self) @@ -332,6 +320,7 @@ def create_hide_empty_menu(self, panels, base_menu): base_menu : QMenu The menu to add actions to. """ + def handle_menu(checked): self.device_display.hideEmpty = checked @@ -345,7 +334,7 @@ def handle_menu(checked): else: self.hide_empty(search=False) - action = base_menu.addAction('Hide Empty Panels') + action = base_menu.addAction("Hide Empty Panels") action.setCheckable(True) action.setChecked(self.device_display.hideEmpty) action.triggered.connect(handle_menu) @@ -378,7 +367,7 @@ def generate_context_menu(self): panels = display.findChildren(typhos_panel.TyphosSignalPanel) or [] if panels: - base_menu.addSection('Filters') + base_menu.addSection("Filters") filter_menu = base_menu.addMenu("&Kind filter") self.create_kind_filter_menu(panels, filter_menu, only=False) filter_menu.addSeparator() @@ -387,8 +376,8 @@ def generate_context_menu(self): base_menu.addSeparator() self.create_hide_empty_menu(panels, base_menu) - base_menu.addSection('Tools') - action = base_menu.addAction('&Copy screenshot to clipboard') + base_menu.addSection("Tools") + action = base_menu.addAction("&Copy screenshot to clipboard") action.triggered.connect(display.copy_to_clipboard) return base_menu @@ -400,10 +389,7 @@ class TyphosDisplaySwitcherButton(TyphosToolButton): templates: Optional[List[pathlib.Path]] template_selected = QtCore.Signal(pathlib.Path) - icons = {'embedded_screen': 'compress', - 'detailed_screen': 'braille', - 'engineering_screen': 'cogs' - } + icons = {"embedded_screen": "compress", "detailed_screen": "braille", "engineering_screen": "cogs"} def __init__(self, display_type, *, parent=None): super().__init__(icon=self.icons[display_type], parent=parent) @@ -412,7 +398,7 @@ def __init__(self, display_type, *, parent=None): def _clicked(self) -> None: """Clicked callback - set the template.""" if self.templates is None: - logger.warning('set_device_display not called on %s', self) + logger.warning("set_device_display not called on %s", self) return # Show all our options in the context menu: @@ -431,10 +417,11 @@ def generate_context_menu(self) -> Optional[QtWidgets.QMenu]: prefix = "" for template in self.templates: + def selected(*, template: pathlib.Path = template): self.template_selected.emit(template) - action = menu.addAction(str(template)[len(prefix):]) + action = menu.addAction(str(template)[len(prefix) :]) action.triggered.connect(selected) return menu @@ -473,7 +460,7 @@ def __init__(self, parent=None, **kwargs): def new_jira_widget(self): """Open a new Jira issue reporting widget.""" if self.device_display is None: - logger.warning('set_device_display not called on %s', self) + logger.warning("set_device_display not called on %s", self) return devices = self.device_display.devices device = devices[0] if devices else None @@ -496,7 +483,7 @@ def _create_ui(self): self.config_button = TyphosDisplayConfigButton() layout.addWidget(self.config_button, 0, Qt.AlignRight) - self.config_button.setToolTip('Display settings...') + self.config_button.setToolTip("Display settings...") def _template_selected(self, template): """Template selected hook.""" @@ -504,8 +491,7 @@ def _template_selected(self, template): if self.device_display is not None: self.device_display.force_template = template - def _templates_loaded(self, templates: Dict[str, List[pathlib.Path]]) -> None: - ... + def _templates_loaded(self, templates: Dict[str, List[pathlib.Path]]) -> None: ... def set_device_display(self, display: TyphosDeviceDisplay) -> None: """Typhos hook for setting the associated device display.""" @@ -577,6 +563,7 @@ class TyphosHelpToggleButton(TyphosToolButton): A Qt signal indicating a request to toggle the related help display frame. """ + pop_out = QtCore.Signal() open_in_browser = QtCore.Signal() open_python_docs = QtCore.Signal() @@ -629,6 +616,7 @@ class TyphosHelpFrame(QtWidgets.QFrame, widgets.TyphosDesignerMixin): tooltip_updated : QtCore.Signal A signal indicating the help tooltip has changed. """ + tooltip_updated = QtCore.Signal(str) def __init__(self, parent=None): @@ -665,9 +653,7 @@ def open_in_browser(self, new=0, autoraise=True): autoraise : bool, optional If possible, autoraise raises the window (the default) or not. """ - return webbrowser.open( - self.help_url.toString(), new=new, autoraise=autoraise - ) + return webbrowser.open(self.help_url.toString(), new=new, autoraise=autoraise) def open_python_docs(self, show: bool = True): """Open the Python docstring information in a new window.""" @@ -702,22 +688,11 @@ def _get_tooltip(self): tooltip = [] # BUG: I'm seeing two devices in `self.devices` for # $ typhos --fake-device 'ophyd.EpicsMotor[{"prefix":"b"}]' - for device in sorted( - set(self.devices), - key=lambda dev: self.devices.index(dev) - ): + for device in sorted(set(self.devices), key=lambda dev: self.devices.index(dev)): heading = device.name or type(device).__name__ - tooltip.extend([ - heading, - "-" * len(heading), - "" - ]) - - tooltip.append( - inspect.getdoc(device) or - inspect.getdoc(type(device)) or - "No docstring" - ) + tooltip.extend([heading, "-" * len(heading), ""]) + + tooltip.append(inspect.getdoc(device) or inspect.getdoc(type(device)) or "No docstring") tooltip.append("") return "\n".join(tooltip) @@ -740,8 +715,7 @@ def help_url(self): try: device_url = utils.HELP_URL.format(device=device) except Exception: - logger.exception("Failed to format confluence URL for device %s", - device) + logger.exception("Failed to format confluence URL for device %s", device) return QtCore.QUrl("about:blank") return QtCore.QUrl(device_url) @@ -749,10 +723,7 @@ def help_url(self): def show_help(self): """Show the help information in a QWebEngineView.""" if web.TyphosWebEngineView is None: - logger.error( - "Failed to import QWebEngineView; " - "help view is unavailable." - ) + logger.error("Failed to import QWebEngineView; help view is unavailable.") return if self.help_web_view: @@ -828,8 +799,7 @@ class TyphosDisplayTitle(QtWidgets.QFrame, widgets.TyphosDesignerMixin): The parent widget. """ - def __init__(self, title='${name}', *, show_switcher=True, - show_underline=True, parent=None): + def __init__(self, title="${name}", *, show_switcher=True, show_underline=True, parent=None): self._show_underline = show_underline self._show_switcher = show_switcher super().__init__(parent=parent) @@ -853,27 +823,15 @@ def __init__(self, title='${name}', *, show_switcher=True, self.help = TyphosHelpFrame() if utils.HELP_WEB_ENABLED: # Toggle the help web view if we have documentation to show - self.switcher.help_toggle_button.toggle_help.connect( - self.toggle_help - ) + self.switcher.help_toggle_button.toggle_help.connect(self.toggle_help) else: # Otherwise, open the python docs as a fallback - self.switcher.help_toggle_button.toggle_help.connect( - self.help.open_python_docs - ) + self.switcher.help_toggle_button.toggle_help.connect(self.help.open_python_docs) self.switcher.help_toggle_button.pop_out.connect(self.pop_out_help) - self.switcher.help_toggle_button.open_in_browser.connect( - self.help.open_in_browser - ) - self.switcher.help_toggle_button.open_python_docs.connect( - self.help.open_python_docs - ) - self.switcher.help_toggle_button.report_jira_issue.connect( - self.help.new_jira_widget - ) - self.help.tooltip_updated.connect( - self.switcher.help_toggle_button.setToolTip - ) + self.switcher.help_toggle_button.open_in_browser.connect(self.help.open_in_browser) + self.switcher.help_toggle_button.open_python_docs.connect(self.help.open_python_docs) + self.switcher.help_toggle_button.report_jira_issue.connect(self.help.new_jira_widget) + self.help.tooltip_updated.connect(self.switcher.help_toggle_button.setToolTip) self.grid_layout.addWidget(self.help, 2, 0, 1, 2) @@ -945,39 +903,29 @@ def toggle(): self.label.toggle_requested.connect(toggle) # Make designable properties from the title label available here as well - label_alignment = forward_property('label', QtWidgets.QLabel, 'alignment') - label_font = forward_property('label', QtWidgets.QLabel, 'font') - label_indent = forward_property('label', QtWidgets.QLabel, 'indent') - label_margin = forward_property('label', QtWidgets.QLabel, 'margin') - label_openExternalLinks = forward_property('label', QtWidgets.QLabel, - 'openExternalLinks') - label_pixmap = forward_property('label', QtWidgets.QLabel, 'pixmap') - label_text = forward_property('label', QtWidgets.QLabel, 'text') - label_textFormat = forward_property('label', QtWidgets.QLabel, - 'textFormat') - label_textInteractionFlags = forward_property('label', QtWidgets.QLabel, - 'textInteractionFlags') - label_wordWrap = forward_property('label', QtWidgets.QLabel, 'wordWrap') + label_alignment = forward_property("label", QtWidgets.QLabel, "alignment") + label_font = forward_property("label", QtWidgets.QLabel, "font") + label_indent = forward_property("label", QtWidgets.QLabel, "indent") + label_margin = forward_property("label", QtWidgets.QLabel, "margin") + label_openExternalLinks = forward_property("label", QtWidgets.QLabel, "openExternalLinks") + label_pixmap = forward_property("label", QtWidgets.QLabel, "pixmap") + label_text = forward_property("label", QtWidgets.QLabel, "text") + label_textFormat = forward_property("label", QtWidgets.QLabel, "textFormat") + label_textInteractionFlags = forward_property("label", QtWidgets.QLabel, "textInteractionFlags") + label_wordWrap = forward_property("label", QtWidgets.QLabel, "wordWrap") # Make designable properties from the grid_layout - layout_margin = forward_property('grid_layout', QtWidgets.QHBoxLayout, - 'margin') - layout_spacing = forward_property('grid_layout', QtWidgets.QHBoxLayout, - 'spacing') + layout_margin = forward_property("grid_layout", QtWidgets.QHBoxLayout, "margin") + layout_spacing = forward_property("grid_layout", QtWidgets.QHBoxLayout, "spacing") # Make designable properties from the underline - underline_palette = forward_property('underline', QtWidgets.QFrame, - 'palette') - underline_styleSheet = forward_property('underline', QtWidgets.QFrame, - 'styleSheet') - underline_lineWidth = forward_property('underline', QtWidgets.QFrame, - 'lineWidth') - underline_midLineWidth = forward_property('underline', QtWidgets.QFrame, - 'midLineWidth') - - -class TyphosDeviceDisplay(utils.TyphosBase, widgets.TyphosDesignerMixin, - _DisplayTypes): + underline_palette = forward_property("underline", QtWidgets.QFrame, "palette") + underline_styleSheet = forward_property("underline", QtWidgets.QFrame, "styleSheet") + underline_lineWidth = forward_property("underline", QtWidgets.QFrame, "lineWidth") + underline_midLineWidth = forward_property("underline", QtWidgets.QFrame, "midLineWidth") + + +class TyphosDeviceDisplay(utils.TyphosBase, widgets.TyphosDesignerMixin, _DisplayTypes): """ Main display for a single ophyd Device. @@ -1033,12 +981,12 @@ def __init__( embedded_templates: Optional[list[str]] = None, detailed_templates: Optional[list[str]] = None, engineering_templates: Optional[list[str]] = None, - display_type: Union[DisplayTypes, str, int] = 'embedded_screen', + display_type: Union[DisplayTypes, str, int] = "embedded_screen", scroll_option: Union[ScrollOptions, str, int] = ScrollOptions.auto, nested: bool = False, ): self._current_template = None - self._forced_template = '' + self._forced_template = "" self._macros = {} self._display_widget = None self._scroll_option = scroll_option @@ -1056,17 +1004,16 @@ def __init__( self._display_type = DisplayTypes.embedded_screen instance_templates = { - 'embedded_screen': embedded_templates or [], - 'detailed_screen': detailed_templates or [], - 'engineering_screen': engineering_templates or [], + "embedded_screen": embedded_templates or [], + "detailed_screen": detailed_templates or [], + "engineering_screen": engineering_templates or [], } for view, path_list in instance_templates.items(): paths = [pathlib.Path(p).expanduser().resolve() for p in path_list] self.templates[view].extend(paths) - self._scroll_area = QtWidgets.QScrollArea() self._scroll_area.setAlignment(Qt.AlignTop) - self._scroll_area.setObjectName('scroll_area') + self._scroll_area.setObjectName("scroll_area") self._scroll_area.setFrameShape(QtWidgets.QFrame.StyledPanel) self._scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) self._scroll_area.setWidgetResizable(True) @@ -1146,9 +1093,7 @@ def _get_matching_templates_for_class( """Get matching templates for the given class.""" class_name_prefix = f"{cls.__name__}." return [ - filename - for filename in self.templates[display_type.name] - if filename.name.startswith(class_name_prefix) + filename for filename in self.templates[display_type.name] if filename.name.startswith(class_name_prefix) ] def _generate_template_menu(self, base_menu: QtWidgets.QMenu) -> None: @@ -1191,14 +1136,12 @@ def add_header(label: str, icon: Optional[QtGui.QIcon] = None) -> None: continue def by_match_order(template: pathlib.Path) -> int: - return matching.index(template) + return matching.index(template) # noqa: B023 if not added_header: add_header( f"{template_type.friendly_name} screens", - icon=TyphosToolButton.get_icon( - TyphosDisplaySwitcherButton.icons[template_type.name] - ), + icon=TyphosToolButton.get_icon(TyphosDisplaySwitcherButton.icons[template_type.name]), ) added_header = True @@ -1210,13 +1153,11 @@ def by_match_order(template: pathlib.Path) -> int: for template in DEFAULT_TEMPLATES_FLATTEN: add_template(template) - prefix = os.path.commonprefix( - [action.text() for action in actions] - ) + prefix = os.path.commonprefix([action.text() for action in actions]) # Arbitrary threshold: saving on a few characters is not worth it if len(prefix) > 9: for action in actions: - action.setText(action.text()[len(prefix):]) + action.setText(action.text()[len(prefix) :]) def _refresh_templates(self): """Force an update of the display cache and look for new ui files.""" @@ -1281,19 +1222,19 @@ def device_class(self): """Get the full class with module name of loaded device.""" device = self.device cls = self.device.__class__ - return f'{cls.__module__}.{cls.__name__}' if device else '' + return f"{cls.__module__}.{cls.__name__}" if device else "" @Property(str, designable=False) def device_name(self): """Get the name of the loaded device.""" device = self.device - return device.name if device else '' + return device.name if device else "" @property def device(self): """Get the device associated with this Device Display.""" try: - device, = self.devices + (device,) = self.devices return device except ValueError: ... @@ -1316,8 +1257,7 @@ def get_best_template(self, display_type, macros): if templates: return templates[0] - logger.warning("No templates available for display type: %s", - self._display_type) + logger.warning("No templates available for display type: %s", self._display_type) def _remove_display(self): """Remove the display widget, readying for a new template.""" @@ -1343,9 +1283,7 @@ def load_best_template(self): self.search_for_templates() self._remove_display() - - template = (self._forced_template or - self.get_best_template(self._display_type, self.macros)) + template = self._forced_template or self.get_best_template(self._display_type, self.macros) if not template: widget = QtWidgets.QWidget() @@ -1363,10 +1301,7 @@ def load_best_template(self): try: widget = self._load_template(self._current_template) except Exception: - logger.exception( - "Failed to fall back to previous template: %s", - self._current_template - ) + logger.exception("Failed to fall back to previous template: %s", self._current_template) template = None widget = None @@ -1378,9 +1313,9 @@ def load_best_template(self): if widget: if widget.objectName(): - widget.setObjectName(f'{widget.objectName()}_display_widget') + widget.setObjectName(f"{widget.objectName()}_display_widget") else: - widget.setObjectName('display_widget') + widget.setObjectName("display_widget") if widget.layout() is None and widget.minimumSize().width() == 0: # If the widget has no layout, use a fixed size for it. @@ -1435,21 +1370,18 @@ def _get_templates_from_macros(macros): try: value = pathlib.Path(value) except ValueError as ex: - logger.debug('Invalid path specified in macro: %s=%s', - display_type, value, exc_info=ex) + logger.debug("Invalid path specified in macro: %s=%s", display_type, value, exc_info=ex) else: - ret[display_type] = list(utils.find_file_in_paths( - value, paths=paths)) + ret[display_type] = list(utils.find_file_in_paths(value, paths=paths)) return ret def _load_template(self, filename): """Load template from file and return the widget.""" filename = pathlib.Path(filename) - loader = (pydm.display.load_py_file if filename.suffix == '.py' - else utils.load_ui_file) + loader = pydm.display.load_py_file if filename.suffix == ".py" else utils.load_ui_file - logger.debug('Load template using %s: %r', loader.__name__, filename) + logger.debug("Load template using %s: %r", loader.__name__, filename) try: return loader(str(filename), macros=self._macros) except Exception as ex: @@ -1468,10 +1400,10 @@ def _update_children(self): bases = display.findChildren(utils.TyphosBase) or [] for widget in set(bases + designer + [display]): - if device and hasattr(widget, 'add_device'): + if device and hasattr(widget, "add_device"): widget.add_device(device) - if hasattr(widget, 'set_device_display'): + if hasattr(widget, "set_device_display"): widget.set_device_display(self) @Property(str) @@ -1488,16 +1420,16 @@ def force_template(self, value): @staticmethod def _build_macros_from_device(device, macros=None): result = {} - if hasattr(device, 'md'): + if hasattr(device, "md"): if isinstance(device.md, dict): result = dict(device.md) else: result = dict(device.md.post()) - if 'name' not in result: - result['name'] = device.name - if 'prefix' not in result and hasattr(device, 'prefix'): - result['prefix'] = device.prefix + if "name" not in result: + result["name"] = device.name + if "prefix" not in result and hasattr(device, "prefix"): + result["prefix"] = device.prefix result.update(**(macros or {})) return result @@ -1546,20 +1478,20 @@ def search_for_templates(self): """Search the filesystem for device-specific templates.""" device = self.device if not device: - logger.debug('Cannot search for templates without device') + logger.debug("Cannot search for templates without device") return self._searched = True cls = device.__class__ - logger.debug('Searching for templates for %s', cls.__name__) + logger.debug("Searching for templates for %s", cls.__name__) macro_templates = self._get_templates_from_macros(self._macros) paths = cache.get_global_display_path_cache().paths for display_type in DisplayTypes.names: view = display_type - if view.endswith('_screen'): - view = view.split('_screen')[0] + if view.endswith("_screen"): + view = view.split("_screen")[0] template_list = self.templates[display_type] template_list.clear() @@ -1567,28 +1499,21 @@ def search_for_templates(self): # 1. Highest priority: macros for template in set(macro_templates[display_type] or []): template_list.append(template) - logger.debug('Adding macro template %s: %s (total=%d)', - display_type, template, len(template_list)) + logger.debug("Adding macro template %s: %s (total=%d)", display_type, template, len(template_list)) # 2. Templates based on class hierarchy names filenames = utils.find_templates_for_class(cls, view, paths) for filename in filenames: if filename not in template_list: template_list.append(filename) - logger.debug('Found new template %s: %s (total=%d)', - display_type, filename, len(template_list)) - + logger.debug("Found new template %s: %s (total=%d)", display_type, filename, len(template_list)) # 3. Ensure that the detailed tree template makes its way in for - # all top-level screens, if no class-specific screen exists - if DETAILED_TREE_TEMPLATE not in template_list: + # embedded and detailed screens, if no class-specific screen exists + if display_type != DisplayTypes.engineering_screen.name and DETAILED_TREE_TEMPLATE not in template_list: if not self._nested or self.suggest_composite_screen(cls): template_list.append(DETAILED_TREE_TEMPLATE) - # 4. Default templates - template_list.extend( - [templ for templ in DEFAULT_TEMPLATES[display_type] - if templ not in template_list] - ) + template_list.extend([templ for templ in DEFAULT_TEMPLATES[display_type] if templ not in template_list]) self.templates_loaded.emit(copy.deepcopy(self.templates)) @@ -1664,8 +1589,7 @@ def from_class(cls, klass, *, template=None, macros=None, **kwargs): try: obj = pcdsutils.utils.get_instance_by_name(klass, **kwargs) except Exception: - logger.exception('Failed to generate TyphosDeviceDisplay from ' - 'class %s', klass) + logger.exception("Failed to generate TyphosDeviceDisplay from class %s", klass) return None return cls.from_device(obj, template=template, macros=macros) @@ -1680,9 +1604,7 @@ def _get_specific_screens(cls, device_cls): paths = cache.get_global_display_path_cache().paths return [ template - for template in utils.find_templates_for_class( - device_cls, "detailed", paths - ) + for template in utils.find_templates_for_class(device_cls, "detailed", paths) if not utils.is_standard_template(template) ] @@ -1709,15 +1631,15 @@ def copy_to_clipboard(self): @Slot(object) def _tx(self, value): """Receive information from happi channel.""" - self.add_device(value['obj'], macros=value['md']) + self.add_device(value["obj"], macros=value["md"]) def __repr__(self): """Get a custom representation for TyphosDeviceDisplay.""" return ( - f'<{self.__class__.__name__} at {hex(id(self))} ' - f'device={self.device_class}[{self.device_name!r}] ' - f'nested={self._nested}' - f'>' + f"<{self.__class__.__name__} at {hex(id(self))} " + f"device={self.device_class}[{self.device_name!r}] " + f"nested={self._nested}" + f">" ) @@ -1775,6 +1697,7 @@ def hide_empty(widget, process_widget=True): This is useful since we don't want to hide the top-most widget otherwise users can't change the visibility back on. """ + def process(item, recursive=True): if isinstance(item, TyphosDeviceDisplay) and recursive: hide_empty(item) diff --git a/typhos/dynamic_font.py b/typhos/dynamic_font.py index 580ec91e..f486b9a8 100644 --- a/typhos/dynamic_font.py +++ b/typhos/dynamic_font.py @@ -3,6 +3,7 @@ Dynamically set widget font size based on its current size. """ + from __future__ import annotations import functools @@ -64,11 +65,7 @@ def get_widget_maximum_font_size( current_width = 0.0 # Only stop when step is small enough and new size is smaller than QWidget - while ( - step > precision - or (curent_height > target_height) - or (current_width > target_width) - ): + while step > precision or (curent_height > target_height) or (current_width > target_width): # Keep last tested value last_tested_size = current_size @@ -199,6 +196,7 @@ def patch_text_widget( The text is immediately resized for the first time during this function call. """ + def set_font_size() -> None: font_size = get_max_font_size_cached( widget.text(), @@ -264,10 +262,9 @@ def patch_combo_widget( The text is immediately resized for the first time during this function call. """ + def set_font_size() -> None: - combo_options = [ - widget.itemText(index) for index in range(widget.count()) - ] + combo_options = [widget.itemText(index) for index in range(widget.count())] font_sizes = [ get_max_font_size_cached( text, @@ -307,9 +304,7 @@ def resizeEvent(event: QtGui.QResizeEvent) -> None: orig_resize_event = widget.resizeEvent - resizeEvent._patched_methods_ = ( - widget.resizeEvent, - ) + resizeEvent._patched_methods_ = (widget.resizeEvent,) widget.resizeEvent = resizeEvent set_font_size() @@ -342,9 +337,7 @@ def unpatch_text_widget(widget: QtWidgets.QLabel | QtWidgets.QLineEdit): def unpatch_combo_widget(widget: QtWidgets.QComboBox): - ( - widget.resizeEvent, - ) = widget.resizeEvent._patched_methods_ + (widget.resizeEvent,) = widget.resizeEvent._patched_methods_ def is_patched(widget: QtWidgets.QWidget) -> bool: @@ -392,9 +385,7 @@ def patch_style_font_size(widget: QtWidgets.QWidget, font_size: int) -> None: if standard_comment in starting_stylesheet: unpatch_style_font_size(widget=widget) widget.setStyleSheet( - f"{widget.styleSheet()}\n" - f"{standard_comment}\n" - f"{widget.__class__.__name__} {{ font-size: {font_size} pt }}" + f"{widget.styleSheet()}\n{standard_comment}\n{widget.__class__.__name__} {{ font-size: {font_size} pt }}" ) @@ -406,6 +397,4 @@ def unpatch_style_font_size(widget: QtWidgets.QWidget) -> None: and the rule that we added. """ if standard_comment in widget.styleSheet(): - widget.setStyleSheet( - "\n".join(widget.styleSheet().split("\n")[:-2]) - ) + widget.setStyleSheet("\n".join(widget.styleSheet().split("\n")[:-2])) diff --git a/typhos/examples/device_classes.py b/typhos/examples/device_classes.py index c6647ea5..6cce9fe8 100644 --- a/typhos/examples/device_classes.py +++ b/typhos/examples/device_classes.py @@ -6,6 +6,7 @@ That's why this module exists. """ + import random import threading import time @@ -18,6 +19,7 @@ class PositionerBase: """ Trick Typhos into giving us the positioner template. """ + pass @@ -27,8 +29,8 @@ class ExamplePositioner(Device, PositionerBase): This behaves more or less like you'd expect a real motor to behave. """ - user_readback = Cpt(Signal, value=0.0, kind='hinted', - metadata={'precision': 3}) + + user_readback = Cpt(Signal, value=0.0, kind="hinted", metadata={"precision": 3}) user_setpoint = Cpt(Signal, value=0.0) low_limit_switch = Cpt(Signal, value=False) high_limit_switch = Cpt(Signal, value=False) @@ -37,8 +39,8 @@ class ExamplePositioner(Device, PositionerBase): velocity = Cpt(Signal, value=1.0) acceleration = Cpt(Signal, value=1.0) motor_is_moving = Cpt(Signal, value=False) - error_message = Cpt(Signal, value='') - cause_error = Cpt(Signal, value='') + error_message = Cpt(Signal, value="") + cause_error = Cpt(Signal, value="") def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -73,7 +75,7 @@ def _step_position(self): velo = self.velocity.get() * (1 + noise) step = velo * time_step if abs(dist) < step: - self.user_readback.put(self.user_setpoint.get() + noise/10) + self.user_readback.put(self.user_setpoint.get() + noise / 10) self._status.set_finished() elif dist > 0: self.user_readback.put(self.user_readback.get() + step) @@ -86,9 +88,7 @@ def stop(self, success=False): if success: self._status.set_finished() else: - self._status.set_exception( - RuntimeError('Move Interrupted') - ) + self._status.set_exception(RuntimeError("Move Interrupted")) @user_readback.sub_value def _update_position(self, value, **kwargs): @@ -105,7 +105,7 @@ def _limit_hit(self, value, **kwargs): self.stop(success=True) def clear_error(self): - self.error_message.put('') + self.error_message.put("") @cause_error.sub_value def _cause_error(self, value, **kwargs): @@ -120,8 +120,9 @@ class ExampleComboPositioner(Device, PositionerBase): instead of floating point positions, and this facilitates that behavior. """ - user_readback = Cpt(Signal, value='OUT', kind='hinted') - user_setpoint = Cpt(Signal, value='Unknown') + + user_readback = Cpt(Signal, value="OUT", kind="hinted") + user_setpoint = Cpt(Signal, value="Unknown") motor_is_moving = Cpt(Signal, value=False) stop = None @@ -129,7 +130,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._status = None self._status_ready_event = threading.Event() - enums = ('Unknown', 'OUT', 'TARGET1', 'TARGET2') + enums = ("Unknown", "OUT", "TARGET1", "TARGET2") self.user_readback.enum_strs = enums self.user_setpoint.enum_strs = enums @@ -143,17 +144,15 @@ def set(self, position): def _start_motion_thread(self, value, **kwargs): self._status = StatusBase() self._status_ready_event.set() - if value == 'Unknown': - self._status.set_exception( - RuntimeError('Unknown not a valid target state') - ) + if value == "Unknown": + self._status.set_exception(RuntimeError("Unknown not a valid target state")) else: td = threading.Thread(target=self._motion_thread) td.start() def _motion_thread(self): self.motor_is_moving.put(True) - self.user_readback.put('Unknown') + self.user_readback.put("Unknown") time.sleep(3) self.user_readback.put(self.user_setpoint.get()) self.motor_is_moving.put(False) diff --git a/typhos/examples/panel.py b/typhos/examples/panel.py index 972d6ed0..dd89a3f7 100644 --- a/typhos/examples/panel.py +++ b/typhos/examples/panel.py @@ -1,4 +1,5 @@ """Example to create a Panel of Ophyd Signals from an object""" + import sys import numpy as np @@ -12,16 +13,22 @@ class Sample(Device): """Simulated Device""" + readback = Cpt(SignalRO, value=1) setpoint = Cpt(Signal, value=2) - waveform = Cpt(SignalRO, value=np.random.randn(100, )) + waveform = Cpt( + SignalRO, + value=np.random.randn( + 100, + ), + ) image = Cpt(SignalRO, value=np.abs(np.random.randn(100, 100)) * 455) # Create my device without a prefix -sample = Sample('', name='sample') +sample = Sample("", name="sample") -if __name__ == '__main__': +if __name__ == "__main__": # Create my application app = QApplication(sys.argv) typhos.use_stylesheet() diff --git a/typhos/examples/positioner.py b/typhos/examples/positioner.py index 93cfbe57..e5d78bd1 100644 --- a/typhos/examples/positioner.py +++ b/typhos/examples/positioner.py @@ -10,13 +10,13 @@ def main(): """ get_qapp() devices = [ - ExamplePositioner(name='example_motor'), - ExampleComboPositioner(name='example_combo'), + ExamplePositioner(name="example_motor"), + ExampleComboPositioner(name="example_combo"), ] suite = TyphosSuite.from_devices(devices) use_stylesheet() launch_suite(suite) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/typhos/export.py b/typhos/export.py new file mode 100644 index 00000000..bef2c098 --- /dev/null +++ b/typhos/export.py @@ -0,0 +1,386 @@ +""" +Export a typhos screen as a PyDM Screen +""" + +import json +import logging + +from lxml import etree +from ophyd.device import Device +from ophyd.signal import EpicsSignalBase +from qtpy.QtWidgets import QWidget + +from .display import TyphosDeviceDisplay, TyphosDisplayTitle +from .panel import TyphosCompositeSignalPanel, TyphosSignalPanel +from .utils import get_variety_metadata, is_signal_ro +from .widgets import determine_widget_type + +logger = logging.getLogger(__name__) + + +def export_as_ui(display: TyphosDeviceDisplay, export_filename: str): + """ + Main starting point for the export routine called from cli. + + This is meant to be run after generating a typhos display but instead of + building the suite and main window and executing the QApplication. + + Parameters + ---------- + display : TyphosDeviceDisplay + The display we'll use as an export. + export_filename : str + The destination filepath to save the .ui file. + """ + device: Device = display.devices[0] + + tree = from_display(display) + etree.indent(tree, space=" ", level=0) + text = etree.tostring(tree, pretty_print=True, encoding="unicode") + + if display.macros: + all_macros = { + key: value + for key, value in display.macros.items() + if isinstance(key, str) and isinstance(value, str) and not key.startswith("_") + } + else: + all_macros = {"prefix": device.prefix, "name": device.name} + + un_macros = {value: f"${{{key}}}" for key, value in all_macros.items()} + + for unm, macro in un_macros.items(): + text = text.replace(unm, macro) + + with open(export_filename, "w") as fd: + fd.write(text) + + logger.info(f"Wrote file {export_filename}") + + used_macros = {key: value for key, value in all_macros.items() if un_macros[value] in text} + + logger.info("File must be opened and re-saved in designer before it can be run!") + logger.info(f"After re-save, run as pydm --macro '{json.dumps(used_macros)}' {export_filename}") + + +def from_display(display: TyphosDeviceDisplay) -> etree._ElementTree: + """ + Generate a corresponding ui file xml tree from a source display. + """ + template = str(display.current_template) + tree = etree.parse(template) + root = tree.getroot() + + logger.debug(f"Parsing display: searching for widgets in template {template}") + + # Replace each typhos designer widget as appropriate + for elem in root.findall(".//widget"): + name = str(elem.get("name")) + logger.debug(f"Found widget named {name}") + widget_obj = display.findChild(QWidget, name) + try: + new_elem = convert_widget_to_element(widget_obj, name) + except TypeError: + logger.debug(f"Widget {name} was not a replaceable widget type, skipping") + continue + parent_elem = elem.getparent() + if parent_elem is None: + continue + parent_elem.replace(elem, new_elem) + + # Finalize the customwidgets section + # TODO + return tree + + +def convert_widget_to_element(source_widget: QWidget, name: str) -> etree._Element: + """ + Choose which function to use to replace a typhos widget with an xml description of a standard widget. + """ + try: + type_name = source_widget.__class__.__name__ + except AttributeError: + type_name = str(source_widget) + match type_name: + case "TyphosSignalPanel" | "TyphosCompositeSignalPanel": + return from_typhos_composite_signal_panel(source_widget=source_widget, name=name) + case "TyphosDisplayTitle": + return from_typhos_display_title(source_widget=source_widget, name=name) + case ( + "TyphosAlarmCircle" + | "TyphosAlarmEllipse" + | "TyphosAlarmPolygon" + | "TyphosAlarmRectangle" + | "TyphosAlarmTriangle" + | "TyphosDisplaySwitcher" + | "TyphosHelpFrame" + | "TyphosMethodButton" + | "TyphosNotesEdit" + | "TyphosPositionerWidget" + | "TyphosPositionerRowWidget" + | "TyphosRelatedSuiteButton" + ): + return from_generic_widget(source_widget=source_widget, name=name) + case _: + err = f"Unhandled type for {source_widget} of type {type_name}" + logger.debug(err) + raise TypeError(err) + + +def from_generic_widget(source_widget: QWidget, name: str) -> etree._Element: + """ + Replace most typhos widgets with blank QWidgets + """ + logger.debug(f"Replace {name} with generic QWidget") + widget = create_widget_named(name=name, cls="QWidget") + + # Match the size of the original widget just so the screen is recognizable + original_size = source_widget.sizeHint() + add_size_property( + widget=widget, size_name="minimumSize", width=original_size.width(), height=original_size.height() + ) + add_size_property( + widget=widget, size_name="maximumSize", width=original_size.width(), height=original_size.height() + ) + + return widget + + +def from_typhos_display_title(source_widget: TyphosDisplayTitle, name: str) -> etree._Element: + """ + Just the device name I guess + """ + logger.debug(f"Replace {name} with title QLabel") + widget = create_widget_named(name=name, cls="QLabel") + add_string_property(widget=widget, prop_name="text", prop_value=source_widget.device_display.devices[0].name) + return widget + + +def from_typhos_composite_signal_panel( + source_widget: TyphosSignalPanel | TyphosCompositeSignalPanel, name: str +) -> etree._Element: + """ + Replace TyphosCompositeSignalPanel with repeated applications of what we do for typhos signal panel + + This also works from TyphosSignalPanel, which is a subset of the composite panel. + """ + # The composite signal panel works by: + # 1. call add_device once + # 2. For each top-level component in order, call add_sub_device if it's a device or _maybe_add_signal otherwise + # 2a. add_sub_device creates a mini TyphosDeviceDisplay with the subdevice + # this subdisplay is a whole new template for us to deal with + # 2b. _maybe_add_signal, for this purposes of this screen, will add the signal if it matches the kind settings + # it has some other behavior otherwise, but it is only relevant for the display switcher we won't support here + # Note that each thing is added as a new row in a grid layout- so that's our outer structure, a grid + # We'll try to build this using the primitives we implemented above + logger.debug(f"Exploring contents of composite signal panel {name}") + + widget = create_widget_named(name=name, cls="QWidget") + grid = add_grid_layout(widget=widget, layout_name=f"{name}_grid_layout") + + device_name = source_widget.devices[0].name + + # Iterate through the rows in the grid layout + # Three possibilities: + # 1. a TyphosDeviceDisplay spanning all columns + # 2. a Qlabel in col 0, then a readback widget in cols 1-2 + # 3. a Qlabel in col 0, a readback widget in col 1, a setpoint widget in col 2 + # For 1 we can use the display xml builder, but strip out everything except the main widget + # For 2 and 3 we can use the per-row behavior from the signal panel function + + grid_layout = source_widget._panel_layout + signal_info_list = list(grid_layout.signal_name_to_info.values()) + output_row = -1 + + for row_count in range(grid_layout.rowCount()): + logger.debug(f"Checking grid row index {row_count}") + first_item = grid_layout.itemAtPosition(row_count, 0) + if first_item is None: + logger.debug("No item in row, skipping") + continue + first_widget = first_item.widget() + if first_widget is None: + logger.debug("No widget in row, skipping") + continue + logger.debug(f"Found {first_widget} named {first_widget.objectName()} on row {row_count}") + if isinstance(first_widget, TyphosDeviceDisplay): + output_row += 1 + logger.debug(f"Expanding subdisplay on input row {row_count} for output row {output_row}") + # A device subdisplay + tree = from_display(display=first_widget) + root = tree.getroot() + top_widget = root.find("widget") + if top_widget is None: + raise RuntimeError("Display had no top-level widget?") + subdisplay_item = etree.SubElement(grid, "item") + subdisplay_item.set("row", str(output_row)) + subdisplay_item.set("column", "0") + subdisplay_item.set("colspan", "3") + subdisplay_item.append(top_widget) + else: + logger.debug(f"Expanding signal on row {row_count}") + # A signal row + signal_info = None + for info in signal_info_list: + if info["row"] == row_count: + # We found it + signal_info = info + if signal_info is None: + raise RuntimeError(f"No signal info for row {row_count}") + if signal_info["signal"] is None: + logger.debug(f"Skipping signal info {signal_info}, no signal created") + continue + logger.debug(f"Using signal info {signal_info}") + + signal = signal_info["signal"] + signal_name = signal.name + if not isinstance(signal, EpicsSignalBase): + logger.debug("Not an epics signal, skipping") + continue + output_row += 1 + logger.debug(f"Assigning output row count {output_row}") + add_signal_row_to_grid( + signal_name=signal_name, signal_info=signal_info, device_name=device_name, grid=grid, row=output_row + ) + + return widget + + +def add_signal_row_to_grid(signal_name: str, signal_info: dict, device_name: str, grid: etree._Element, row: int): + signal = signal_info["signal"] + if is_signal_ro(signal): + logger.debug("Picking read-only widgets") + read_cls, _ = determine_widget_type(signal=signal, read_only=True) + write_cls = None + else: + logger.debug("Picking read-write widgets") + read_cls, _ = determine_widget_type(signal=signal, read_only=True) + write_cls, _ = determine_widget_type(signal=signal, read_only=False) + + short_signal_name = signal_name.removeprefix(device_name + "_") + if short_signal_name == device_name: + short_signal_name = "device" + short_signal_text = device_name + else: + short_signal_text = short_signal_name + + # First item in row: signal name + label_widget = create_widget_in_grid(name=f"{short_signal_name}_label", cls="QLabel", grid=grid, row=row, col=0) + add_string_property(widget=label_widget, prop_name="text", prop_value=short_signal_text) + # Second item in row: readback widget + # Extend to end of no third item in row + if write_cls is None: + colspan = 2 + else: + colspan = 0 + readback_widget = create_widget_in_grid( + name=f"{short_signal_name}_readback", + cls=typhos_type_to_pydm_type(read_cls), + grid=grid, + row=row, + col=1, + colspan=colspan, + ) + add_string_property(widget=readback_widget, prop_name="channel", prop_value=f"ca://{signal_info['signal'].pvname}") + if write_cls is None: + return + # Third item in row: setpoint widget + setpoint_clsname = typhos_type_to_pydm_type(write_cls) + setpoint_widget = create_widget_in_grid( + name=f"{short_signal_name}_setpoint", cls=setpoint_clsname, grid=grid, row=row, col=2 + ) + add_string_property(widget=setpoint_widget, prop_name="channel", prop_value=f"ca://{signal._write_pv.pvname}") # type: ignore + if setpoint_clsname == "PyDMPushButton": + # Helpful to get the press value correct here in the translation so the button works as intended + press_value = get_variety_metadata(signal_info["signal"])["value"] + add_string_property(widget=setpoint_widget, prop_name="pressValue", prop_value=str(press_value)) + add_string_property(widget=setpoint_widget, prop_name="text", prop_value="Command") + + +def typhos_type_to_pydm_type(typhos_widget: QWidget) -> str: + match str(typhos_widget.__name__): + case "PyDMLabel" | "TyphosLabel" | "WaveformDialogButton" | "ImageDialogButton": + return "PyDMLabel" + case "TyphosComboBox": + return "PyDMEnumComboBox" + case "PyDMPushButton" | "TyphosCommandButton": + return "PyDMPushButton" + case "PyDMEnumButton" | "TyphosCommandEnumButton": + return "PyDMEnumButton" + case "PyDMByteIndicator" | "TyphosByteIndicator" | "TyphosCommandIndicator" | "TyphosByteSetpoint": + return "PyDMByteIndicator" + case "PyDMSlider" | "TyphosScalarRange": + return "PyDMSlider" + case "PyDMWaveformTable" | "TyphosArrayTable": + return "PyDMWaveformTable" + case _: + return "PyDMLineEdit" + + +def create_widget_named(name: str, cls: str, parent: etree._Element | None = None) -> etree._Element: + """ + Building block: returns a new widget element. + """ + if parent is None: + widget = etree.Element("widget") + else: + widget = etree.SubElement(parent, "widget") + # Set outside of kwargs to mimic designer order and avoid "class" keyword + widget.set("class", cls) + widget.set("name", name) + return widget + + +def add_string_property(widget: etree._Element, prop_name: str, prop_value: str) -> etree._Element: + """ + Building block: adds a string property to a widget element and returns the property. + """ + prop_elem = etree.SubElement(widget, "property", name=prop_name) + string_elem = etree.SubElement(prop_elem, "string") + string_elem.text = prop_value + return prop_elem + + +def add_size_property(widget: etree._Element, size_name: str, width: int, height: int) -> etree._Element: + """ + Building block: adds a property with a width and height and returns the property. + """ + prop_elem = etree.SubElement(widget, "property", name=size_name) + size_elem = etree.SubElement(prop_elem, "size") + width_elem = etree.SubElement(size_elem, "width") + width_elem.text = str(width) + height_elem = etree.SubElement(size_elem, "height") + height_elem.text = str(height) + return prop_elem + + +def add_grid_layout(widget: etree._Element, layout_name: str) -> etree._Element: + """ + Building block: adds a grid layout to a widget element and returns the grid layout. + """ + grid_layout = etree.SubElement(widget, "layout") + # Set outside of kwargs to mimic designer order and avoid "class" keyword + grid_layout.set("class", "QGridLayout") + grid_layout.set("name", layout_name) + return grid_layout + + +def add_item_to_grid(grid: etree._Element, row: int, col: int, colspan: int = 0) -> etree._Element: + """ + Building block: adds an item to a grid layout and returns it. Items can hold widgets. + """ + grid_item = etree.SubElement(grid, "item", row=str(row), column=str(col)) + if colspan: + grid_item.set("colspan", str(colspan)) + return grid_item + + +def create_widget_in_grid( + name: str, cls: str, grid: etree._Element, row: int, col: int, colspan: int = 0 +) -> etree._Element: + """ + Building block: creates a new widget and a new grid item all at once. Returns the widget. + """ + grid_item = add_item_to_grid(grid=grid, row=row, col=col, colspan=colspan) + widget = create_widget_named(name=name, cls=cls, parent=grid_item) + return widget diff --git a/typhos/func.py b/typhos/func.py index 91113834..ffd1a4e1 100644 --- a/typhos/func.py +++ b/typhos/func.py @@ -12,6 +12,7 @@ where these widgets find that the user has entered inappropriate values, in this case they should return np.nan to halt the function from being called. """ + import inspect import logging from functools import partial @@ -20,9 +21,18 @@ from numpydoc import docscrape from qtpy.QtCore import Property, QSize, Qt, Slot from qtpy.QtGui import QFont -from qtpy.QtWidgets import (QCheckBox, QGroupBox, QHBoxLayout, QLabel, - QLineEdit, QPushButton, QSizePolicy, QSpacerItem, - QVBoxLayout, QWidget) +from qtpy.QtWidgets import ( + QCheckBox, + QGroupBox, + QHBoxLayout, + QLabel, + QLineEdit, + QPushButton, + QSizePolicy, + QSpacerItem, + QVBoxLayout, + QWidget, +) from .status import TyphosStatusThread from .utils import clean_attr, raise_to_operator @@ -38,6 +48,7 @@ class ParamWidget(QWidget): This creates the QLabel for the parameter and defines the interface required for subclasses of the ParamWidget. """ + def __init__(self, parameter, default=inspect._empty, parent=None): super().__init__(parent=parent) # Store parameter information @@ -74,6 +85,7 @@ class ParamCheckBox(ParamWidget): parent : QWidget, optional """ + def __init__(self, parameter, default=inspect._empty, parent=None): super().__init__(parameter, default=default, parent=parent) self.param_control = QCheckBox(parent=self) @@ -110,7 +122,8 @@ class ParamLineEdit(ParamWidget): parent : QWidget, optional """ - def __init__(self, parameter, _type, default='', parent=None): + + def __init__(self, parameter, _type, default="", parent=None): super().__init__(parameter, default=default, parent=parent) # Store type information self._type = _type @@ -137,14 +150,13 @@ def get_param_value(self): val = self._type(self.param_edit.text()) # If not possible, capture the exception and report `np.nan` except ValueError: - logger.exception("Could not convert text to %r", - self._type.__name__) + logger.exception("Could not convert text to %r", self._type.__name__) val = np.nan return val def parse_numpy_docstring(docstring): - ''' + """ Parse a numpy docstring for summary and parameter information. Parameters @@ -158,15 +170,15 @@ def parse_numpy_docstring(docstring): info['summary'] is a string summary. info['params'] is a dictionary of parameter name to a list of description lines. - ''' + """ info = {} parsed = docscrape.NumpyDocString(docstring) - info['summary'] = '\n'.join(parsed['Summary']) - params = parsed['Parameters'] + info["summary"] = "\n".join(parsed["Summary"]) + params = parsed["Parameters"] # numpydoc v0.8.0 uses just a tuple for parameters, but later versions use # a namedtuple. here, only assume a tuple: - info['params'] = {name: lines for name, type_, lines in params} + info["params"] = {name: lines for name, type_, lines in params} return info @@ -216,19 +228,18 @@ class FunctionDisplay(QGroupBox): parent : QWidget, optional """ + accepted_types = [bool, str, int, float] - def __init__(self, func, name=None, annotations=None, - hide_params=None, parent=None): + def __init__(self, func, name=None, annotations=None, hide_params=None, parent=None): # Function information self.func = func self.signature = inspect.signature(func) self.name = name or self.func.__name__ # Initialize parent - super().__init__(f'{clean_attr(self.name)} Parameters', - parent=parent) + super().__init__(f"{clean_attr(self.name)} Parameters", parent=parent) # Ignore certain parameters, args and kwargs by default - self.hide_params = ['self', 'args', 'kwargs'] + self.hide_params = ["self", "args", "kwargs"] if hide_params: self.hide_params.extend(hide_params) # Create basic layout @@ -240,18 +251,15 @@ def __init__(self, func, name=None, annotations=None, # Add our button to execute the function self.execute_button = QPushButton() - self.docs = {'summary': func.__doc__ or '', - 'params': {} - } + self.docs = {"summary": func.__doc__ or "", "params": {}} if func.__doc__ is not None: try: self.docs.update(**parse_numpy_docstring(func.__doc__)) except Exception as ex: - logger.warning('Unable to parse docstring for function %s: %s', - name, ex, exc_info=ex) + logger.warning("Unable to parse docstring for function %s: %s", name, ex, exc_info=ex) - self.execute_button.setToolTip(self.docs['summary']) + self.execute_button.setToolTip(self.docs["summary"]) self.execute_button.setText(clean_attr(self.name)) self.execute_button.clicked.connect(self.execute) @@ -267,31 +275,26 @@ def __init__(self, func, name=None, annotations=None, self._layout.addItem(QSpacerItem(10, 5, vPolicy=QSizePolicy.Expanding)) # Create parameters from function signature annotations = annotations or dict() - for param in [param for param in self.signature.parameters.values() - if param.name not in self.hide_params]: + for param in [param for param in self.signature.parameters.values() if param.name not in self.hide_params]: logger.debug("Adding parameter %s ", param.name) # See if we received a manual annotation for this parameter if param.name in annotations: _type = annotations[param.name] - logger.debug("Found manually specified type %r", - _type.__name__) + logger.debug("Found manually specified type %r", _type.__name__) # Try and get the type from the function annotation elif param.annotation != inspect._empty: _type = param.annotation - logger.debug("Found annotated type %r ", - _type.__name__) + logger.debug("Found annotated type %r ", _type.__name__) # Try and get the type from the default value elif param.default != inspect._empty: _type = type(param.default) - logger.debug("Gathered type %r from parameter default ", - _type.__name__) + logger.debug("Gathered type %r from parameter default ", _type.__name__) # If we don't have a default value or an annotation, # we can not make a widget for this parameter. Since # this is a required variable (no default), the function # will not work without it. Raise an Exception else: - raise TypeError("Parameter {} has an unspecified " - "type".format(param.name)) + raise TypeError("Parameter {} has an unspecified type".format(param.name)) # Add our parameter self.add_parameter(param.name, _type, default=param.default) @@ -305,8 +308,9 @@ def required_params(self): Required parameters. """ parameters = self.signature.parameters - return [param.parameter for param in self.param_controls - if parameters[param.parameter].default == inspect._empty] + return [ + param.parameter for param in self.param_controls if parameters[param.parameter].default == inspect._empty + ] @property def optional_params(self): @@ -314,8 +318,9 @@ def optional_params(self): Optional parameters. """ parameters = self.signature.parameters - return [param.parameter for param in self.param_controls - if parameters[param.parameter].default != inspect._empty] + return [ + param.parameter for param in self.param_controls if parameters[param.parameter].default != inspect._empty + ] @Slot() def execute(self): @@ -336,15 +341,13 @@ def execute(self): kwargs = dict() # Gather information from parameter widgets for button in self.param_controls: - logger.debug("Gathering parameters for %s ...", - button.parameter) + logger.debug("Gathering parameters for %s ...", button.parameter) val = button.get_param_value() logger.debug("Received %s", val) # Watch for NaN values returned from widgets # This indicates that there was improper information given if np.isnan(val): - logger.error("Invalid information supplied for %s " - "parameter", button.parameter) + logger.error("Invalid information supplied for %s parameter", button.parameter) return kwargs[button.parameter] = val # Button up function call with partial to try below @@ -383,20 +386,16 @@ def add_parameter(self, name, _type, default=inspect._empty, tooltip=None): The generated widget. """ if tooltip is None: - tooltip_header = f'{name} - {_type.__name__}' - tooltip = [ - tooltip_header, - '-' * len(tooltip_header) - ] + tooltip_header = f"{name} - {_type.__name__}" + tooltip = [tooltip_header, "-" * len(tooltip_header)] if default != inspect._empty: - tooltip.append(f'Default: {default}') + tooltip.append(f"Default: {default}") try: - doc_param = self.docs['params'][name] + doc_param = self.docs["params"][name] except KeyError: - logger.debug('Parameter information is not available ' - 'for %s(%s)', self.name, name) + logger.debug("Parameter information is not available for %s(%s)", self.name, name) else: if doc_param: tooltip.extend(doc_param) @@ -404,18 +403,18 @@ def add_parameter(self, name, _type, default=inspect._empty, tooltip=None): # If the tooltip is just the header, remove the dashes underneath: if len(tooltip) == 2: tooltip = tooltip[:1] - tooltip = '\n'.join(tooltip) + tooltip = "\n".join(tooltip) # Create our parameter control widget # QCheckBox field - if _type == bool: + if _type is bool: cntrl = ParamCheckBox(name, default=default) else: # Check if this is a valid type if _type not in self.accepted_types: - raise TypeError("Parameter {} has type {} which can not " - "be represented in a widget" - "".format(name, _type.__name__)) + raise TypeError( + "Parameter {} has type {} which can not be represented in a widget".format(name, _type.__name__) + ) # Create our QLineEdit cntrl = ParamLineEdit(name, default=default, _type=_type) # Add our button to the widget @@ -455,6 +454,7 @@ class FunctionPanel(TogglePanel): parent : QWidget """ + def __init__(self, methods=None, parent=None): # Initialize parent super().__init__("Functions", parent=parent) @@ -486,7 +486,7 @@ def add_method(self, func, *args, **kwargs): :class:`.FunctionDisplay` constructor. """ # Create method display - func_name = kwargs.get('name', func.__name__) + func_name = kwargs.get("name", func.__name__) logger.debug("Adding method %s ...", func_name) widget = FunctionDisplay(func, *args, **kwargs) # Store for posterity @@ -494,8 +494,7 @@ def add_method(self, func, *args, **kwargs): # Add to panel. Make sure that if this is # the first added method that the panel is visible self.show_contents(True) - self.contents.layout().insertWidget(len(self.methods), - widget) + self.contents.layout().insertWidget(len(self.methods), widget) class TyphosMethodButton(QPushButton, TyphosDesignerMixin): @@ -506,11 +505,12 @@ class TyphosMethodButton(QPushButton, TyphosDesignerMixin): will be run when the button is clicked. If ``use_status`` is set to True, the button will be disabled while the ``Status`` object is active. """ + _min_visible_operation = 0.1 _max_allowed_operation = 10.0 def __init__(self, parent=None): - self._method = '' + self._method = "" self._use_status = False super().__init__(parent=parent) self._status_thread = None @@ -555,15 +555,13 @@ def execute(self): logger.error("No device loaded into the object") return device = self.devices[0] - logger.debug("Grabbing method %r from %r ...", - self.method_name, device.name) + logger.debug("Grabbing method %r from %r ...", self.method_name, device.name) try: method = getattr(device, self.method_name) logger.debug("Executing method ...") status = method() except Exception as exc: - logger.exception("Error executing method %r.", - self.method_name) + logger.exception("Error executing method %r.", self.method_name) raise_to_operator(exc) return if self.use_status: @@ -579,7 +577,8 @@ def execute(self): self._status_thread = None logger.debug("Setting up new status thread ...") self._status_thread = TyphosStatusThread( - status, start_delay=self._min_visible_operation, + status, + start_delay=self._min_visible_operation, timeout=self._max_allowed_operation, parent=self, ) diff --git a/typhos/jira.py b/typhos/jira.py index 94015799..ab999479 100644 --- a/typhos/jira.py +++ b/typhos/jira.py @@ -23,7 +23,7 @@ def _failsafe_call(func, *args, value_on_failure=None, **kwargs): return func(*args, **kwargs) except Exception as ex: if value_on_failure is None: - return f'FAILURE: {type(ex).__name__}: {ex}' + return f"FAILURE: {type(ex).__name__}: {ex}" return value_on_failure @@ -42,13 +42,12 @@ def __init__(self, device=None, parent=None): self.name = QtWidgets.QLineEdit(getpass.getuser()) layout.addRow("Your &name", self.name) # TODO jira suffix - self.email = QtWidgets.QLineEdit( - f"{getpass.getuser()}{utils.JIRA_EMAIL_SUFFIX}" - ) + self.email = QtWidgets.QLineEdit(f"{getpass.getuser()}{utils.JIRA_EMAIL_SUFFIX}") layout.addRow("Your &e-mail", self.email) self.summary = QtWidgets.QLineEdit("") layout.addRow("Issue &summary", self.summary) - self.details = QtWidgets.QPlainTextEdit("""\ + self.details = QtWidgets.QPlainTextEdit( + """\ * What were you trying to do? * What did the device/interface/etc do? @@ -56,7 +55,8 @@ def __init__(self, device=None, parent=None): * What should have happened? * Please provide additional context here: - """.strip()) + """.strip() + ) layout.addRow("Issue &details", self.details) self.submit = QtWidgets.QPushButton("Submit") @@ -67,9 +67,7 @@ def __init__(self, device=None, parent=None): self.device = device if device is not None: - self.summary.setText( - f"Device {device.name} ({device.__class__.__name__})" - ) + self.summary.setText(f"Device {device.name} ({device.__class__.__name__})") self.setWindowTitle(f"Typhos issue reporting ({device.name})") else: self.setWindowTitle("Typhos issue reporting") @@ -77,6 +75,7 @@ def __init__(self, device=None, parent=None): @staticmethod def get_environment(): """Get the default environment information.""" + def monospace(text): return "{{%s}}" % text @@ -93,10 +92,7 @@ def monospace(text): @property def anything_provided(self) -> bool: """Were any fields filled out whatsoever?""" - return any( - bool(value.strip()) - for value in self.get_dictionary().values() - ) + return any(bool(value.strip()) for value in self.get_dictionary().values()) def get_dictionary(self, full=False) -> dict[str, str]: """Return all issue details as a dictionary.""" @@ -111,7 +107,7 @@ def get_dictionary(self, full=False) -> dict[str, str]: email=self.email.text().strip(), summary=self.summary.text().strip(), description=self.details.toPlainText().strip(), - **(auto_generated if full else {}) + **(auto_generated if full else {}), ) def _check_submission(self): @@ -121,11 +117,7 @@ def _check_submission(self): summary="Summary", description="Description", ) - errors = [ - f"Missing field: {desc}" - for key, desc in required_fields.items() - if not as_dict[key] - ] + errors = [f"Missing field: {desc}" for key, desc in required_fields.items() if not as_dict[key]] if errors: self.status.setText("\n".join(errors)) @@ -143,10 +135,7 @@ def _check_submission(self): self.status.setText("All fields OK. Submitting...") try: with urllib.request.urlopen(self.request, timeout=5) as fp: - logger.info( - "Jira collector response: %s", - fp.read().decode('utf-8') - ) + logger.info("Jira collector response: %s", fp.read().decode("utf-8")) except Exception as ex: # Sorry, please don't hold it against us... raise_to_operator(ex) @@ -173,9 +162,9 @@ def closeEvent(self, event): result = QtWidgets.QMessageBox.question( self, - 'Cancel issue submission', - 'Cancel issue submission? Nothing will be saved or reported.', - QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No + "Cancel issue submission", + "Cancel issue submission? Nothing will be saved or reported.", + QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, ) if result == QtWidgets.QMessageBox.Yes: event.accept() diff --git a/typhos/notes.py b/typhos/notes.py index f9d515ed..785367d6 100644 --- a/typhos/notes.py +++ b/typhos/notes.py @@ -47,7 +47,7 @@ def get_data_from_yaml(device_name: str, path: Path) -> Optional[Dict[str, str]] with open(path) as f: device_notes = yaml.full_load(f) except Exception as ex: - logger.warning(f'failed to load device notes: {ex}') + logger.warning(f"failed to load device notes: {ex}") return return device_notes.get(device_name, None) @@ -73,7 +73,7 @@ def get_notes_data(device_name: str) -> Tuple[NotesSource, Dict[str, str]]: The source of the device notes, and a dictionary containing the device note information """ - data = {'note': '', 'timestamp': ''} + data = {"note": "", "timestamp": ""} source = NotesSource.USER # try env var @@ -85,7 +85,7 @@ def get_notes_data(device_name: str) -> Tuple[NotesSource, Dict[str, str]]: source = NotesSource.ENV # try user directory - user_data_path = platformdirs.user_data_path() / 'device_notes.yaml' + user_data_path = platformdirs.user_data_path() / "device_notes.yaml" if user_data_path.exists(): note_data = get_data_from_yaml(device_name, user_data_path) if note_data: @@ -103,30 +103,26 @@ def get_notes_data(device_name: str) -> Tuple[NotesSource, Dict[str, str]]: def insert_into_yaml(path: Path, device_name: str, data: dict[str, str]) -> None: try: - with open(path, 'r') as f: + with open(path, "r") as f: device_notes = yaml.full_load(f) except FileNotFoundError: - logger.info(f'No existing device notes found at {path}. ' - 'Creating new notes file.') + logger.info(f"No existing device notes found at {path}. Creating new notes file.") device_notes = {} except Exception as ex: - logger.warning(f'Unable to open existing device notes, aborting: {ex}') + logger.warning(f"Unable to open existing device notes, aborting: {ex}") return device_notes[device_name] = data directory = os.path.dirname(path) temp_path = Path(directory) / ( - f".{getpass.getuser()}" - f"_{int(time.time())}" - f"_{str(uuid.uuid4())[:8]}" - f"_{os.path.basename(path)}" + f".{getpass.getuser()}_{int(time.time())}_{str(uuid.uuid4())[:8]}_{os.path.basename(path)}" ) try: - with open(temp_path, 'w') as f: + with open(temp_path, "w") as f: yaml.dump(device_notes, f) except Exception as ex: - logger.warning(f'unable to write device info: {ex}') + logger.warning(f"unable to write device info: {ex}") return if os.path.exists(path): @@ -134,11 +130,7 @@ def insert_into_yaml(path: Path, device_name: str, data: dict[str, str]) -> None shutil.move(temp_path, path) -def write_notes_data( - source: NotesSource, - device_name: str, - data: dict[str, str] -) -> None: +def write_notes_data(source: NotesSource, device_name: str, data: dict[str, str]) -> None: """ Write the notes ``data`` to the specified ``source`` under the key ``device_name`` @@ -152,14 +144,12 @@ def write_notes_data( The notes data. Expected to contain the 'note' and 'timestamp' keys """ if source == NotesSource.USER: - user_data_path = platformdirs.user_data_path() / 'device_notes.yaml' + user_data_path = platformdirs.user_data_path() / "device_notes.yaml" insert_into_yaml(user_data_path, device_name, data) elif source == NotesSource.ENV: notes_var = os.environ.get(NOTES_VAR) if not notes_var: - raise RuntimeError( - f"Unable to save notes as env var {NOTES_VAR!r} was not set" - ) + raise RuntimeError(f"Unable to save notes as env var {NOTES_VAR!r} was not set") insert_into_yaml(Path(notes_var), device_name, data) @@ -180,7 +170,7 @@ class TyphosNotesEdit( def __init__(self, *args, refresh_time: float = 5.0, **kwargs): super().__init__(*args, **kwargs) self.editingFinished.connect(self.save_note) - self.setPlaceholderText('no notes...') + self.setPlaceholderText("no notes...") self.edit_filter = utils.FrameOnEditFilter(parent=self) self.setFrame(False) self.installEventFilter(self.edit_filter) @@ -189,19 +179,15 @@ def __init__(self, *args, refresh_time: float = 5.0, **kwargs): # to be initialized later self.device_name: Optional[str] = None self.notes_source: Optional[NotesSource] = None - self.data = {'note': '', 'timestamp': ''} + self.data = {"note": "", "timestamp": ""} self.setPlaceholderText("Enter notes here") - self.setSizePolicy( - QtWidgets.QSizePolicy.MinimumExpanding, - QtWidgets.QSizePolicy.Preferred - ) + self.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Preferred) def update_tooltip(self) -> None: - if self.data['note'] and self.notes_source is not None: - self.setToolTip(f"({self.data['timestamp']}, {self.notes_source.name}):\n" - f"{self.data['note']}") + if self.data["note"] and self.notes_source is not None: + self.setToolTip(f"({self.data['timestamp']}, {self.notes_source.name}):\n{self.data['note']}") else: - self.setToolTip('click to edit note') + self.setToolTip("click to edit note") def add_device(self, device: ophyd.Device) -> None: super().add_device(device) @@ -247,7 +233,7 @@ def setup_data(self, device_name: Optional[str] = None) -> None: self._last_updated = time.time() self.notes_source, self.data = get_notes_data(self.device_name) - self.setText(self.data.get('note', '')) + self.setText(self.data.get("note", "")) self.update_tooltip() def save_note(self) -> None: @@ -258,16 +244,15 @@ def save_note(self) -> None: note_text = self.text() curr_time = datetime.now().ctime() - self.data['note'] = note_text - self.data['timestamp'] = curr_time + self.data["note"] = note_text + self.data["timestamp"] = curr_time self.update_tooltip() write_notes_data(self.notes_source, self.device_name, self.data) def event(self, event: QtCore.QEvent) -> bool: - """ Overload event method to update data on tooltip-request """ + """Overload event method to update data on tooltip-request""" # Catch relevant events to update status tooltip - if event.type() in (QtCore.QEvent.ToolTip, QtCore.QEvent.Paint, - QtCore.QEvent.FocusAboutToChange): + if event.type() in (QtCore.QEvent.ToolTip, QtCore.QEvent.Paint, QtCore.QEvent.FocusAboutToChange): self.setup_data() return super().event(event) diff --git a/typhos/panel.py b/typhos/panel.py index 62463ce3..b687a517 100644 --- a/typhos/panel.py +++ b/typhos/panel.py @@ -69,9 +69,7 @@ def name_sorter(walk): """Sort by name.""" return walk.dotted_name - return {SignalOrder.byKind: kind_sorter, - SignalOrder.byName: name_sorter - }.get(signal_order, name_sorter) + return {SignalOrder.byKind: kind_sorter, SignalOrder.byName: name_sorter}.get(signal_order, name_sorter) class SignalPanelRowLabel(QtWidgets.QLabel): @@ -138,7 +136,8 @@ def __init__(self, signals=None): self.setColumnStretch(self.COL_SETPOINT, 1) get_global_widget_type_cache().widgets_determined.connect( - self._got_signal_widget_info, QtCore.Qt.QueuedConnection) + self._got_signal_widget_info, QtCore.Qt.QueuedConnection + ) if signals: for name, sig in signals.items(): @@ -155,9 +154,7 @@ def signals(self): With the form: ``{signal_name: signal}``. """ return { - name: info['signal'] - for name, info in list(self.signal_name_to_info.items()) - if info['signal'] is not None + name: info["signal"] for name, info in list(self.signal_name_to_info.items()) if info["signal"] is not None } @property @@ -171,9 +168,9 @@ def visible_signals(self): With the form: ``{signal_name: signal}``. """ return { - name: info['signal'] + name: info["signal"] for name, info in list(self.signal_name_to_info.items()) - if info['signal'] is not None and info['visible'] + if info["signal"] is not None and info["visible"] } visible_elements = visible_signals @@ -201,13 +198,13 @@ def _got_signal_widget_info(self, obj, info): except KeyError: return - if sig_info['widget_info'] is not None: + if sig_info["widget_info"] is not None: # Only add widgets on the first callback # TODO: debug why multiple calls happen return - sig_info['widget_info'] = info - row = sig_info['row'] + sig_info["widget_info"] = info + row = sig_info["row"] # Remove the 'loading...' animation if it's there item = self.itemAtPosition(row, self.COL_SETPOINT) @@ -226,13 +223,12 @@ def _got_signal_widget_info(self, obj, info): self._update_row(row, widgets) - visible = sig_info['visible'] + visible = sig_info["visible"] for widget in widgets[1:]: widget.setVisible(visible) signal_pairs = list(self.signal_name_to_info.items()) - if all(sig_info['widget_info'] is not None - for _, sig_info in signal_pairs): + if all(sig_info["widget_info"] is not None for _, sig_info in signal_pairs): self.loading_complete.emit([name for name, _ in signal_pairs]) def _create_row_label(self, attr, dotted_name, tooltip, long_name=None): @@ -260,7 +256,7 @@ def _create_row_label(self, attr, dotted_name, tooltip, long_name=None): label.setObjectName(dotted_name) if tooltip is not None: if long_name: - _tooltip = dotted_name + '
' + round(1.75*len(dotted_name))*'-' + '
' + tooltip + _tooltip = dotted_name + "
" + round(1.75 * len(dotted_name)) * "-" + "
" + tooltip else: _tooltip = tooltip label.setToolTip(_tooltip) @@ -286,11 +282,11 @@ def _get_long_name(self, device, attr, dotted_name): str or None """ try: - if hasattr(getattr(device, attr), 'long_name'): + if hasattr(getattr(device, attr), "long_name"): return getattr(device, attr).long_name except AttributeError: # Then maybe we have a nested component and can't touch the signal - if hasattr(getattr(device, dotted_name), 'long_name'): + if hasattr(getattr(device, dotted_name), "long_name"): return getattr(device, dotted_name).long_name def add_signal(self, signal, name=None, long_name=None, *, tooltip=None): @@ -327,15 +323,12 @@ def add_signal(self, signal, name=None, long_name=None, *, tooltip=None): logger.debug("Adding signal %s (%s)", signal.name, name) label = self._create_row_label(attr=name, dotted_name=name, long_name=long_name, tooltip=tooltip) - loading = utils.TyphosLoading( - timeout_message='Connection timed out.' - ) + loading = utils.TyphosLoading(timeout_message="Connection timed out.") - loading_tooltip = ['Connecting to:'] + list({ - getattr(signal, attr) - for attr in ('setpoint_pvname', 'pvname') if hasattr(signal, attr) - }) - loading.setToolTip('\n'.join(loading_tooltip)) + loading_tooltip = ["Connecting to:"] + list( + {getattr(signal, attr) for attr in ("setpoint_pvname", "pvname") if hasattr(signal, attr)} + ) + loading.setToolTip("\n".join(loading_tooltip)) row = self.add_row(label, loading) self.signal_name_to_info[signal.name] = dict( @@ -384,8 +377,8 @@ def _add_component(self, device, attr, dotted_name, component): # Workaround until Ophyd.Component.long_name PR comes through long_name = self._get_long_name(device, attr, dotted_name) label = self._create_row_label( - attr=attr, dotted_name=dotted_name, long_name=long_name, - tooltip=component.doc or '') + attr=attr, dotted_name=dotted_name, long_name=long_name, tooltip=component.doc or "" + ) row = self.add_row(label, None) # utils.TyphosLoading()) self.signal_name_to_info[dotted_name] = dict( row=row, @@ -550,10 +543,10 @@ def _should_show( kind = Kind(kind) if kind not in kinds: return False - for show_name in (show_names or []): + for show_name in show_names or []: if show_name and show_name in name: return True - for omit_name in (omit_names or []): + for omit_name in omit_names or []: if omit_name and omit_name in name: return False return self._apply_name_filter(name_filter, name) @@ -571,8 +564,8 @@ def _set_visible(self, signal_name, visible): Change the visibility of the row to this. """ info = self.signal_name_to_info[signal_name] - info['visible'] = bool(visible) - row = info['row'] + info["visible"] = bool(visible) + row = info["row"] for col in range(self.NUM_COLS): item = self.itemAtPosition(row, col) if item: @@ -580,25 +573,24 @@ def _set_visible(self, signal_name, visible): if widget is not None: widget.setVisible(visible) - if not visible or info['signal'] is not None: + if not visible or info["signal"] is not None: return # Create the signal if we're displaying it for the first time. - create_func = info['create_signal'] + create_func = info["create_signal"] if create_func is None: # A signal we shouldn't try to create again return try: - info['signal'] = signal = create_func() + info["signal"] = signal = create_func() except Exception as ex: - logger.exception('Failed to create signal %s: %s', signal_name, ex) + logger.exception("Failed to create signal %s: %s", signal_name, ex) # Stop it from another attempt - info['create_signal'] = None + info["create_signal"] = None return - logger.debug('Instantiating a not-yet-created signal from a ' - 'component: %s', signal.name) + logger.debug("Instantiating a not-yet-created signal from a component: %s", signal.name) if signal.name != signal_name: # This is, for better or worse, possible; does not support the case # of changing the name after __init__ @@ -632,7 +624,7 @@ def filter_signals( Names to explicitly omit. """ for name, info in list(self.signal_name_to_info.items()): - item = info['signal'] or info['component'] + item = info["signal"] or info["component"] visible = self._should_show( item.kind, name, @@ -658,14 +650,11 @@ def add_device(self, device): sorter = _get_component_sorter(self.parent().sortBy) non_devices = [ - walk - for walk in sorted(device.walk_components(), key=sorter) - if not issubclass(walk.item.cls, ophyd.Device) + walk for walk in sorted(device.walk_components(), key=sorter) if not issubclass(walk.item.cls, ophyd.Device) ] for walk in non_devices: - self._maybe_add_signal(device, walk.item.attr, walk.dotted_name, - walk.item) + self._maybe_add_signal(device, walk.item.attr, walk.dotted_name, walk.item) self.setSizeConstraint(self.SetMinimumSize) @@ -700,8 +689,9 @@ def _maybe_add_signal(self, device, attr, dotted_name, component): try: signal = getattr(device, dotted_name) except Exception as ex: - logger.warning('Failed to get signal %r from device %s: %s', - dotted_name, device.name, ex, exc_info=True) + logger.warning( + "Failed to get signal %r from device %s: %s", dotted_name, device.name, ex, exc_info=True + ) return kind = signal.kind @@ -711,13 +701,13 @@ def _maybe_add_signal(self, device, attr, dotted_name, component): with ophyd.do_not_wait_for_lazy_connection(device): signal = getattr(device, dotted_name) except Exception as ex: - logger.warning('Failed to get signal %r from device %s: %s', - dotted_name, device.name, ex, exc_info=True) + logger.warning( + "Failed to get signal %r from device %s: %s", dotted_name, device.name, ex, exc_info=True + ) return # Workaround until Ophyd.Component.long_name PR comes through long_name = self._get_long_name(device, attr, dotted_name) - return self.add_signal(signal=signal, name=attr, long_name=long_name, - tooltip=component.doc) + return self.add_signal(signal=signal, name=attr, long_name=long_name, tooltip=component.doc) return self._add_component(device, attr, dotted_name, component) @@ -746,16 +736,15 @@ class TyphosSignalPanel(TyphosBase, TyphosDesignerMixin, SignalOrder): SignalOrder = SignalOrder # For convenience _kinds: Dict[str, Kind] # From top of page to bottom - kind_order = (Kind.hinted, Kind.normal, - Kind.config, Kind.omitted) + kind_order = (Kind.hinted, Kind.normal, Kind.config, Kind.omitted) _panel_class = SignalPanel updated = QtCore.Signal() _kind_to_property = { - 'hinted': 'showHints', - 'normal': 'showNormal', - 'config': 'showConfig', - 'omitted': 'showOmitted', + "hinted": "showHints", + "normal": "showNormal", + "config": "showConfig", + "omitted": "showOmitted", } def __init__(self, parent=None, init_channel=None): @@ -763,7 +752,7 @@ def __init__(self, parent=None, init_channel=None): # Create a SignalPanel layout to be modified later self._panel_layout = self._panel_class() self.setLayout(self._panel_layout) - self._name_filter = '' + self._name_filter = "" self._show_names = [] self._omit_names = [] # Add default Kind values @@ -814,22 +803,21 @@ def show_kinds(self) -> List[Kind]: return [Kind[kind] for kind, show in self._kinds.items() if show] # Kind Configuration pyqtProperty - showHints = Property(bool, - partial(_get_kind, kind='hinted'), - partial(_set_kind, kind='hinted'), - doc='Show ophyd.Kind.hinted signals') - showNormal = Property(bool, - partial(_get_kind, kind='normal'), - partial(_set_kind, kind='normal'), - doc='Show ophyd.Kind.normal signals') - showConfig = Property(bool, - partial(_get_kind, kind='config'), - partial(_set_kind, kind='config'), - doc='Show ophyd.Kind.config signals') - showOmitted = Property(bool, - partial(_get_kind, kind='omitted'), - partial(_set_kind, kind='omitted'), - doc='Show ophyd.Kind.omitted signals') + showHints = Property( + bool, partial(_get_kind, kind="hinted"), partial(_set_kind, kind="hinted"), doc="Show ophyd.Kind.hinted signals" + ) + showNormal = Property( + bool, partial(_get_kind, kind="normal"), partial(_set_kind, kind="normal"), doc="Show ophyd.Kind.normal signals" + ) + showConfig = Property( + bool, partial(_get_kind, kind="config"), partial(_set_kind, kind="config"), doc="Show ophyd.Kind.config signals" + ) + showOmitted = Property( + bool, + partial(_get_kind, kind="omitted"), + partial(_set_kind, kind="omitted"), + doc="Show ophyd.Kind.omitted signals", + ) @Property(str) def nameFilter(self) -> str: @@ -897,12 +885,13 @@ def set_device_display(self, display): def generate_context_menu(self): """Generate a context menu for this TyphosSignalPanel.""" menu = QtWidgets.QMenu(parent=self) - menu.addSection('Kinds') + menu.addSection("Kinds") for kind, property_name in self._kind_to_property.items(): + def selected(new_value, *, name=property_name): setattr(self, name, new_value) - action = menu.addAction('Show &' + kind) + action = menu.addAction("Show &" + kind) action.setCheckable(True) action.setChecked(getattr(self, property_name)) action.triggered.connect(selected) @@ -997,8 +986,7 @@ def add_sub_device(self, device, name): name : str The name/label to go with the device. """ - logger.debug('%s adding sub-device: %s (%s)', self.__class__.__name__, - device.name, device.__class__.__name__) + logger.debug("%s adding sub-device: %s (%s)", self.__class__.__name__, device.name, device.__class__.__name__) container = display.TyphosDeviceDisplay( scrollable=False, nested=True, @@ -1013,11 +1001,10 @@ def add_device(self, device): # super().add_device(device) self._devices.append(device) - logger.debug('%s signals from device: %s', self.__class__.__name__, - device.name) + logger.debug("%s signals from device: %s", self.__class__.__name__, device.name) for attr, component in utils._get_top_level_components(type(device)): - dotted_name = f'{device.name}.{attr}' + dotted_name = f"{device.name}.{attr}" if issubclass(component.cls, ophyd.Device): sub_device = getattr(device, attr) self.add_sub_device(sub_device, name=dotted_name) @@ -1028,10 +1015,7 @@ def add_device(self, device): def visible_elements(self): """Return all visible signals and components.""" sigs = self.visible_signals - containers = { - name: cont - for name, cont in self._containers.items() if cont.isVisible() - } + containers = {name: cont for name, cont in self._containers.items() if cont.isVisible()} sigs.update(containers) return sigs diff --git a/typhos/plugins/core.py b/typhos/plugins/core.py index 55907b42..0ab94abc 100644 --- a/typhos/plugins/core.py +++ b/typhos/plugins/core.py @@ -1,6 +1,7 @@ """ Module Docstring """ + import logging import numpy as np @@ -35,7 +36,7 @@ def register_signal(signal): # .dotted_name does not include the root device's name names = ( signal.name, - '.'.join((signal.root.name, signal.dotted_name)), + ".".join((signal.root.name, signal.dotted_name)), ) # Warn the user if they are adding twice for name in names: @@ -75,6 +76,7 @@ class SignalConnection(PyDMConnection): signal : ophyd.Signal Stored signal object. """ + supported_types = [int, float, str, np.ndarray] def __init__(self, channel, address, protocol=None, parent=None): @@ -138,11 +140,10 @@ def cast(self, value): # We make the assumption that signals do not change types during a # connection if not self.signal_type: - dtype = self.signal.describe()[self.signal.name]['dtype'] + dtype = self.signal.describe()[self.signal.name]["dtype"] # Only way this raises a KeyError is if ophyd is confused self.signal_type = _type_map[dtype][0] - logger.debug("Found signal type %r for %r. Using Python type %r", - dtype, self.signal.name, self.signal_type) + logger.debug("Found signal type %r for %r. Using Python type %r", dtype, self.signal.name, self.signal_type) logger.debug("Casting %r to %r", value, self.signal_type) if self.enum_strs: @@ -161,9 +162,7 @@ def cast(self, value): except (TypeError, ValueError): value = str(value) else: - raise TypeError( - f"Invalid combination: enum_strs={self.enum_strs} with signal_type={self.signal_type}" - ) + raise TypeError(f"Invalid combination: enum_strs={self.enum_strs} with signal_type={self.signal_type}") elif self.signal_type is np.ndarray: value = np.asarray(value) else: @@ -201,18 +200,10 @@ def send_new_value(self, value=None, **kwargs): value = self.cast(value) self.new_value_signal[self.signal_type].emit(value) except Exception: - logger.exception("Unable to update %r with value %r.", - self.signal.name, value) + logger.exception("Unable to update %r with value %r.", self.signal.name, value) def send_new_meta( - self, - connected=None, - write_access=None, - severity=None, - precision=None, - units=None, - enum_strs=None, - **kwargs + self, connected=None, write_access=None, severity=None, precision=None, units=None, enum_strs=None, **kwargs ): """ Update the UI with new metadata from the Signal. @@ -272,9 +263,9 @@ def add_listener(self, channel): # Gather metadata signal_meta = self.signal.metadata except Exception: - logger.exception("Failed to gather proper information " - "from signal %r to initialize %r", - self.signal.name, channel) + logger.exception( + "Failed to gather proper information from signal %r to initialize %r", self.signal.name, channel + ) return if isinstance(signal_val, (float, np.floating)): # Precision is commonly omitted from non-epics signals @@ -299,8 +290,7 @@ def add_listener(self, channel): val_sig = channel.value_signal[_typ] val_sig.connect(self.put_value, Qt.QueuedConnection) except KeyError: - logger.debug("%s has no value_signal for type %s", - channel.address, _typ) + logger.debug("%s has no value_signal for type %s", channel.address, _typ) def remove_listener(self, channel, destroying=False, **kwargs): """ @@ -316,8 +306,7 @@ def remove_listener(self, channel, destroying=False, **kwargs): try: channel.value_signal[_typ].disconnect(self.put_value, destroying) except (KeyError, TypeError): - logger.debug("Unable to disconnect value_signal from %s " - "for type %s", channel.address, _typ) + logger.debug("Unable to disconnect value_signal from %s for type %s", channel.address, _typ) # Disconnect any other signals super().remove_listener(channel, destroying=destroying, **kwargs) logger.debug("Successfully removed %r", channel) @@ -330,7 +319,8 @@ def close(self): class SignalPlugin(PyDMPlugin): """Plugin registered with PyDM to handle SignalConnection.""" - protocol = 'sig' + + protocol = "sig" connection_class = SignalConnection def add_connection(self, channel): @@ -342,12 +332,11 @@ def add_connection(self, channel): # don't add this to our list of good to go connections. The next # attempt we try again. except KeyError: - logger.error("Unable to find signal for %r in signal registry." - "Use typhos.plugins.register_signal()", - channel) + logger.error( + "Unable to find signal for %r in signal registry.Use typhos.plugins.register_signal()", channel + ) except Exception: - logger.exception("Unable to create a connection to %r", - channel) + logger.exception("Unable to create a connection to %r", channel) def remove_connection(self, channel, destroying=False): try: diff --git a/typhos/plugins/happi.py b/typhos/plugins/happi.py index 82ef6c57..0e9fc1d0 100644 --- a/typhos/plugins/happi.py +++ b/typhos/plugins/happi.py @@ -26,6 +26,7 @@ def register_client(client): class HappiConnection(PyDMConnection): """A PyDMConnection to the Happi Database.""" + tx = QtCore.Signal(dict) def __init__(self, channel, address, protocol=None, parent=None): @@ -38,8 +39,8 @@ def add_listener(self, channel): # Connect our channel to the signal self.tx.connect(channel.tx_slot, QtCore.Qt.QueuedConnection) logger.debug("Loading %r from happi Client", channel) - if '.' in self.address: - device, child = self.address.split('.', 1) + if "." in self.address: + device, child = self.address.split(".", 1) else: device, child = self.address, None # Load the device from the Client @@ -48,12 +49,11 @@ def add_listener(self, channel): md = md.post() # If we have a child grab it if child: - logger.debug("Retrieving child %r from %r", - child, obj.name) + logger.debug("Retrieving child %r from %r", child, obj.name) obj = getattr(obj, child) - md = {'name': obj.name} + md = {"name": obj.name} # Send the device and metdata to all of our subscribers - self.tx.emit({'obj': obj, 'md': md}) + self.tx.emit({"obj": obj, "md": md}) def remove_listener(self, channel, destroying=False, **kwargs): """Remove a channel from the database connection.""" @@ -63,7 +63,7 @@ def remove_listener(self, channel, destroying=False, **kwargs): class HappiPlugin(PyDMPlugin): - protocol = 'happi' + protocol = "happi" connection_class = HappiConnection def add_connection(self, channel): @@ -74,10 +74,8 @@ def add_connection(self, channel): try: super().add_connection(channel) except SearchError: - logger.error("Unable to find device for %r in happi database.", - channel) + logger.error("Unable to find device for %r in happi database.", channel) except AttributeError as exc: - logger.exception("Invalid attribute %r for address %r", - exc, channel.address) + logger.exception("Invalid attribute %r for address %r", exc, channel.address) except Exception: logger.exception("Unable to load %r from happi", channel.address) diff --git a/typhos/positioner.py b/typhos/positioner.py index 7e92c847..1afba1b0 100644 --- a/typhos/positioner.py +++ b/typhos/positioner.py @@ -137,29 +137,30 @@ class TyphosPositionerWidget( ``hinted`` signals. ============== =========================================================== """ + QtCore.Q_ENUMS(_KindLevel) KindLevel = KindLevel ui: _TyphosPositionerUI - ui_template = os.path.join(utils.ui_dir, 'widgets', 'positioner.ui') - _readback_attr = 'user_readback' - _setpoint_attr = 'user_setpoint' - _low_limit_switch_attr = 'low_limit_switch' - _high_limit_switch_attr = 'high_limit_switch' - _low_limit_travel_attr = 'low_limit_travel' - _high_limit_travel_attr = 'high_limit_travel' - _velocity_attr = 'velocity' - _acceleration_attr = 'acceleration' - _moving_attr = 'motor_is_moving' - _error_message_attr = 'error_message' + ui_template = os.path.join(utils.ui_dir, "widgets", "positioner.ui") + _readback_attr = "user_readback" + _setpoint_attr = "user_setpoint" + _low_limit_switch_attr = "low_limit_switch" + _high_limit_switch_attr = "high_limit_switch" + _low_limit_travel_attr = "low_limit_travel" + _high_limit_travel_attr = "high_limit_travel" + _velocity_attr = "velocity" + _acceleration_attr = "acceleration" + _moving_attr = "motor_is_moving" + _error_message_attr = "error_message" _min_visible_operation = 0.1 alarm_text = { - AlarmLevel.NO_ALARM: 'no alarm', - AlarmLevel.MINOR: 'minor', - AlarmLevel.MAJOR: 'major', - AlarmLevel.DISCONNECTED: 'no conn', - AlarmLevel.INVALID: 'invalid', + AlarmLevel.NO_ALARM: "no alarm", + AlarmLevel.MINOR: "minor", + AlarmLevel.MAJOR: "major", + AlarmLevel.DISCONNECTED: "no conn", + AlarmLevel.INVALID: "invalid", } def __init__(self, parent=None): @@ -289,11 +290,13 @@ def _get_timeout( mult = rescale # This time is always greater than the kinematic calc return ( - math.ceil(rescale * (dist/speed + 2 * abs(acc_time)) + abs(settle_time)), - ("an upper bound on the expected time based on the speed, distance traveled, " - "and acceleration time. Numerically, this is " - f"{mult=}*({dist=:.2f}{units}/{speed=:.2f}{units}/s) + " - f"2*{acc_time=:.2f}s + {settle_time=}s, rounded up."), + math.ceil(rescale * (dist / speed + 2 * abs(acc_time)) + abs(settle_time)), + ( + "an upper bound on the expected time based on the speed, distance traveled, " + "and acceleration time. Numerically, this is " + f"{mult=}*({dist=:.2f}{units}/{speed=:.2f}{units}/s) + " + f"2*{acc_time=:.2f}s + {settle_time=}s, rounded up." + ), ) def _set(self, value): @@ -310,10 +313,9 @@ def _set(self, value): timeout, desc = self._get_timeout(set_position, settle_time=5, rescale=1.2) except Exception: # Something went wrong, just run without a timeout. - logger.exception('Unable to estimate motor timeout.') + logger.exception("Unable to estimate motor timeout.") timeout = None - logger.debug("Setting device %r to %r with timeout %r", - self.device, value, timeout) + logger.debug("Setting device %r to %r with timeout %r", self.device, value, timeout) try: status = self.device.set(set_position) except Exception as exc: @@ -352,7 +354,7 @@ def tweak(self, offset): try: setpoint = self._get_position() + float(offset) except Exception: - logger.exception('Tweak failed') + logger.exception("Tweak failed") return self.ui.set_value.setText(str(setpoint)) @@ -364,7 +366,7 @@ def positive_tweak(self): try: self.tweak(float(self.tweak_value.text())) except Exception: - logger.exception('Tweak failed') + logger.exception("Tweak failed") @QtCore.Slot() def negative_tweak(self): @@ -372,7 +374,7 @@ def negative_tweak(self): try: self.tweak(-float(self.tweak_value.text())) except Exception: - logger.exception('Tweak failed') + logger.exception("Tweak failed") @QtCore.Slot() def stop(self): @@ -394,7 +396,7 @@ def clear_error(self): """ for device in self.devices: clear_error_in_background(device) - self._set_status_text('') + self._set_status_text("") # This variable holds True if last move was good, False otherwise # It also controls whether or not we have a red box on the widget # False = Red, True = Green, None = no box (in motion is yellow) @@ -406,43 +408,40 @@ def _get_position(self): raise Exception("No Device configured for widget!") return self._readback.get() - @utils.linked_attribute('readback_attribute', 'ui.user_readback', True) + @utils.linked_attribute("readback_attribute", "ui.user_readback", True) def _link_readback(self, signal, widget): """Link the positioner readback with the ui element.""" self._readback = signal - @utils.linked_attribute('setpoint_attribute', 'ui.user_setpoint', True) + @utils.linked_attribute("setpoint_attribute", "ui.user_setpoint", True) def _link_setpoint(self, signal, widget): """Link the positioner setpoint with the ui element.""" self._setpoint = signal if signal is not None: # Seed the set_value text with the user_setpoint channel value. - if hasattr(widget, 'textChanged'): + if hasattr(widget, "textChanged"): widget.textChanged.connect(self._user_setpoint_update) - @utils.linked_attribute('low_limit_switch_attribute', - 'ui.low_limit_switch', True) + @utils.linked_attribute("low_limit_switch_attribute", "ui.low_limit_switch", True) def _link_low_limit_switch(self, signal, widget): """Link the positioner lower limit switch with the ui element.""" if signal is None: widget.hide() self._show_lowlim = False - @utils.linked_attribute('high_limit_switch_attribute', - 'ui.high_limit_switch', True) + @utils.linked_attribute("high_limit_switch_attribute", "ui.high_limit_switch", True) def _link_high_limit_switch(self, signal, widget): """Link the positioner high limit switch with the ui element.""" if signal is None: widget.hide() self._show_highlim = False - @utils.linked_attribute('low_limit_travel_attribute', 'ui.low_limit', True) + @utils.linked_attribute("low_limit_travel_attribute", "ui.low_limit", True) def _link_low_travel(self, signal, widget): """Link the positioner lower travel limit with the ui element.""" return signal is not None - @utils.linked_attribute('high_limit_travel_attribute', 'ui.high_limit', - True) + @utils.linked_attribute("high_limit_travel_attribute", "ui.high_limit", True) def _link_high_travel(self, signal, widget): """Link the positioner high travel limit with the ui element.""" return signal is not None @@ -470,7 +469,7 @@ def _link_limits_by_limits_attr(self): self._show_lowtrav = False self._show_hightrav = False - @utils.linked_attribute('moving_attribute', 'ui.moving_indicator', True) + @utils.linked_attribute("moving_attribute", "ui.moving_indicator", True) def _link_moving(self, signal, widget): """Link the positioner moving indicator with the ui element.""" if signal is None: @@ -488,7 +487,7 @@ def _link_moving(self, signal, widget): self._moving_channel.connect() return True - @utils.linked_attribute('error_message_attribute', 'ui.error_label', True) + @utils.linked_attribute("error_message_attribute", "ui.error_label", True) def _link_error_message(self, signal, widget): """Link the IOC error message with the ui element.""" if signal is None: @@ -522,17 +521,13 @@ def _define_setpoint_widget(self): self.ui.set_value.returnPressed.connect(self.set) self.ui.set_value.setSizePolicy(self.ui.user_setpoint.sizePolicy()) - self.ui.set_value.setMinimumWidth( - self.ui.user_setpoint.minimumWidth() - ) - self.ui.set_value.setMaximumWidth( - self.ui.user_setpoint.maximumWidth() - ) + self.ui.set_value.setMinimumWidth(self.ui.user_setpoint.minimumWidth()) + self.ui.set_value.setMaximumWidth(self.ui.user_setpoint.maximumWidth()) self.ui.setpoint_layout.addWidget( self.ui.set_value, alignment=QtCore.Qt.AlignHCenter, ) - self.ui.set_value.setObjectName('set_value') + self.ui.set_value.setObjectName("set_value") # Because set_value is used instead self.ui.user_setpoint.setVisible(False) @@ -558,7 +553,7 @@ def add_device(self, device): # If the stop method is missing, hide the button try: - device.stop + device.stop # noqa: B018 self.ui.stop_button.show() except AttributeError: self.ui.stop_button.hide() @@ -606,9 +601,9 @@ def _after_set_moving(self, value): """ utils.reload_widget_stylesheet(self, cascade=True) if value: - self.ui.moving_indicator_label.setText('moving') + self.ui.moving_indicator_label.setText("moving") else: - self.ui.moving_indicator_label.setText('done') + self.ui.moving_indicator_label.setText("done") def _set_moving(self, value): """ @@ -801,7 +796,7 @@ def _set_status_text( tooltip = f"{text}: {tooltip}" else: tooltip = text - text = text[:max_length] + '...' + text = text[:max_length] + "..." self.ui.status_label.setText(text) if tooltip and "\n" not in tooltip: # Force rich text, qt auto line wraps if it detects rich text @@ -832,7 +827,7 @@ def _status_finished(self, result: TyphosStatusResult | Exception) -> None: def _user_setpoint_update(self, text): """Qt slot - indicating the ``user_setpoint`` widget text changed.""" try: - text = text.strip().split(' ')[0] + text = text.strip().split(" ")[0] text = text.strip() except Exception: return @@ -845,7 +840,7 @@ def _user_setpoint_update(self, text): self.ui.set_value.setCurrentIndex(idx) self._initialized = True except ValueError: - logger.debug('Failed to convert value to int. %s', text) + logger.debug("Failed to convert value to int. %s", text) else: self._initialized = True self.ui.set_value.setText(text) @@ -884,10 +879,7 @@ def all_linked_attributes(self) -> list[str]: @property def all_linked_signals(self) -> list[ophyd.Signal]: """All linked signal names.""" - signals = [ - getattr(self.device, attr, None) - for attr in self.all_linked_attributes - ] + signals = [getattr(self.device, attr, None) for attr in self.all_linked_attributes] return [sig for sig in signals if sig is not None] def show_ui_type_hints(self): @@ -926,11 +918,11 @@ class TyphosPositionerRowWidget(TyphosPositionerWidget): ui_template = os.path.join(utils.ui_dir, "widgets", "positioner_row.ui") alarm_text = { - AlarmLevel.NO_ALARM: 'ok', - AlarmLevel.MINOR: 'minor', - AlarmLevel.MAJOR: 'major', - AlarmLevel.DISCONNECTED: 'conn', - AlarmLevel.INVALID: 'inv', + AlarmLevel.NO_ALARM: "ok", + AlarmLevel.MINOR: "minor", + AlarmLevel.MAJOR: "major", + AlarmLevel.DISCONNECTED: "conn", + AlarmLevel.INVALID: "inv", } def __init__(self, *args, **kwargs): @@ -1054,12 +1046,14 @@ def add_device(self, device: ophyd.Device) -> None: self.ui.switcher.help_toggle_button.setToolTip(self._get_tooltip()) self.ui.switcher.help_toggle_button.setEnabled(False) - if not any(( - self._show_lowlim, - self._show_highlim, - self._show_lowtrav, - self._show_hightrav, - )): + if not any( + ( + self._show_lowlim, + self._show_highlim, + self._show_lowtrav, + self._show_hightrav, + ) + ): # Hide the limit sections self.ui.low_limit_widget.hide() self.ui.high_limit_widget.hide() @@ -1070,26 +1064,15 @@ def _get_tooltip(self): tooltip = [] # BUG: I'm seeing two devices in `self.devices` for # $ typhos --fake-device 'ophyd.EpicsMotor[{"prefix":"b"}]' - for device in sorted( - set(self.devices), - key=lambda dev: self.devices.index(dev) - ): + for device in sorted(set(self.devices), key=lambda dev: self.devices.index(dev)): heading = device.name or type(device).__name__ - tooltip.extend([ - heading, - "-" * len(heading), - "" - ]) - - tooltip.append( - inspect.getdoc(device) or - inspect.getdoc(type(device)) or - "No docstring" - ) + tooltip.extend([heading, "-" * len(heading), ""]) + + tooltip.append(inspect.getdoc(device) or inspect.getdoc(type(device)) or "No docstring") tooltip.append("") return "\n".join(tooltip) - @utils.linked_attribute('error_message_attribute', 'ui.error_label', True) + @utils.linked_attribute("error_message_attribute", "ui.error_label", True) def _link_error_message(self, signal, widget): """Link the IOC error message with the ui element.""" if signal is None: @@ -1107,8 +1090,7 @@ def _define_setpoint_widget(self): dynamic_font.patch_widget(self.ui.set_value, pad_percent=0.18, min_size=4) # Consume the vertical space left by the missing tweak widgets self.ui.set_value.setMinimumHeight( - self.ui.user_setpoint.minimumHeight() - + self.ui.tweak_value.minimumHeight() + self.ui.user_setpoint.minimumHeight() + self.ui.tweak_value.minimumHeight() ) else: dynamic_font.patch_widget(self.ui.set_value, pad_percent=0.1, min_size=4) @@ -1158,9 +1140,9 @@ def update_status_visibility( if not has_status and not has_error: # We want to fill something in, check if we have alarms if alarm_level == AlarmLevel.NO_ALARM: - self.ui.status_label.setText('Status OK') + self.ui.status_label.setText("Status OK") else: - self.ui.status_label.setText('Check alarm') + self.ui.status_label.setText("Check alarm") has_status = True if has_status and has_error: # We want to avoid having duplicate information (low effort try) @@ -1189,6 +1171,7 @@ class RowDetails(QtWidgets.QWidget): """ Container class for floating window with positioner row's basic config info. """ + row: TyphosPositionerRowWidget resize_timer: QtCore.QTimer @@ -1201,9 +1184,7 @@ def __init__(self, row: TyphosPositionerRowWidget, parent: QtWidgets.QWidget | N font = self.label.font() font.setPointSize(font.pointSize() + 4) self.label.setFont(font) - self.label.setMaximumHeight( - QtGui.QFontMetrics(font).boundingRect(self.label.text()).height() - ) + self.label.setMaximumHeight(QtGui.QFontMetrics(font).boundingRect(self.label.text()).height()) self.panel = TyphosSignalPanel() self.panel.omitNames = row.get_names_to_omit() @@ -1235,7 +1216,7 @@ def hideEvent(self, event: QtGui.QHideEvent): """ After hide, update button text, even if we were hidden via clicking the "x". """ - self.row.ui.expand_button.setText('>') + self.row.ui.expand_button.setText(">") return super().hideEvent(event) def showEvent(self, event: QtGui.QShowEvent): @@ -1243,13 +1224,12 @@ def showEvent(self, event: QtGui.QShowEvent): Before show, update button text and move window to just under button. """ button = self.row.ui.expand_button - button.setText('v') + button.setText("v") self.move( button.mapToGlobal( QtCore.QPoint( button.pos().x(), - button.pos().y() + button.height() - + self.style().pixelMetric(QtWidgets.QStyle.PM_TitleBarHeight), + button.pos().y() + button.height() + self.style().pixelMetric(QtWidgets.QStyle.PM_TitleBarHeight), ) ) ) diff --git a/typhos/related_display.py b/typhos/related_display.py index 896718b2..f4093615 100644 --- a/typhos/related_display.py +++ b/typhos/related_display.py @@ -1,18 +1,19 @@ """ Widgets that open up typhos displays. """ + import logging from pydm.utilities import establish_widget_connections, is_qt_designer from qtpy import QtCore, QtWidgets from .suite import TyphosSuite -from .utils import (TyphosObject, no_device_lazy_load, raise_window, - use_stylesheet) +from .utils import TyphosObject, no_device_lazy_load, raise_window, use_stylesheet try: from happi.client import Client from happi.loader import load_devices + happi_loaded = True except ImportError: happi_loaded = False @@ -24,8 +25,7 @@ def happi_check(): if not happi_loaded: logger.warning( - 'The happi module is not in your Python environment, ' - 'happi TyphosRelatedSuiteButton features will not work.' + "The happi module is not in your Python environment, happi TyphosRelatedSuiteButton features will not work." ) return happi_loaded @@ -43,12 +43,12 @@ class TyphosRelatedSuiteButton(TyphosObject, QtWidgets.QPushButton): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._happi_names = [] - self._happi_cfg = '' + self._happi_cfg = "" self._preload = False self._suite = None self.clicked.connect(self.show_suite) - @QtCore.Property('QStringList') + @QtCore.Property("QStringList") def happi_names(self): """ List of devices to include in the suite. @@ -112,7 +112,7 @@ def create_suite(self): """ devices = self.devices + self.get_happi_devices() if not devices: - raise ValueError('There are no devices assigned to this button.') + raise ValueError("There are no devices assigned to this button.") self._suite = TyphosSuite.from_devices(devices) use_stylesheet(widget=self._suite) establish_widget_connections(self._suite) @@ -133,14 +133,12 @@ def get_happi_devices(self): search_result = happi_client.search(name=name)[0] except IndexError: raise ValueError( - f'Did not find device with name {name} in happi. ' - 'Please check your spelling and your database.' + f"Did not find device with name {name} in happi. Please check your spelling and your database." ) from None items.append(search_result.item) with no_device_lazy_load(): device_namespace = load_devices(*items, threaded=True) - return [getattr(device_namespace, name) - for name in self.happi_names] + return [getattr(device_namespace, name) for name in self.happi_names] return [] diff --git a/typhos/status.py b/typhos/status.py index a95d5f93..f8451947 100644 --- a/typhos/status.py +++ b/typhos/status.py @@ -7,8 +7,7 @@ import traceback from ophyd.status import Status -from ophyd.utils import (StatusTimeoutError, UnknownStatusFailure, - WaitTimeoutError) +from ophyd.utils import StatusTimeoutError, UnknownStatusFailure, WaitTimeoutError from qtpy.QtCore import QObject, QThread, Signal logger = logging.getLogger(__name__) @@ -76,6 +75,7 @@ class TyphosStatusThread(QThread): thread.start() """ + status_started = Signal() status_timeout = Signal() status_finished = Signal(TyphosStatusResult) @@ -87,7 +87,7 @@ def __init__( status: Status, error_context: str = "Status", timeout_calc: str = "", - start_delay: float = 0., + start_delay: float = 0.0, timeout: float = 10.0, parent: QObject | None = None, ): diff --git a/typhos/suite.py b/typhos/suite.py index a7bd05a6..81b6a729 100644 --- a/typhos/suite.py +++ b/typhos/suite.py @@ -1,6 +1,7 @@ """ The high-level Typhos Suite, which bundles tools and panels. """ + from __future__ import annotations import logging @@ -20,8 +21,7 @@ from . import utils, widgets from .display import DisplayTypes, ScrollOptions, TyphosDeviceDisplay from .tools import TyphosLogDisplay, TyphosTimePlot -from .utils import (TyphosBase, TyphosException, clean_attr, clean_name, - flatten_tree, save_suite) +from .utils import TyphosBase, TyphosException, clean_attr, clean_name, flatten_tree, save_suite logger = logging.getLogger(__name__) # Use non-None sentinel value since None means no tools @@ -73,16 +73,18 @@ def has_device(self, device: ophyd.Device): has_device : bool """ return any( - (device in self.devices, - device in getattr(self.value(), 'devices', []), - self.name() == device, - isinstance(device, str) and self.name() == clean_attr(device), - ) + ( + device in self.devices, + device in getattr(self.value(), "devices", []), + self.name() == device, + isinstance(device, str) and self.name() == clean_attr(device), + ) ) class TyphosDisplayNotCreatedError(TyphosException): """The given subdisplay has not yet been shown.""" + ... @@ -172,9 +174,9 @@ class DeviceParameter(SidebarParameter): def __init__(self, device, subdevices=True, **opts): # Set options for parameter - opts['name'] = clean_name(device, strip_parent=device.root) + opts["name"] = clean_name(device, strip_parent=device.root) self.device = device - opts['expanded'] = False + opts["expanded"] = False # Grab children from the given device children = list() if subdevices: @@ -183,28 +185,24 @@ def __init__(self, device, subdevices=True, **opts): if subdevice._sub_devices: # If that device has children, make sure they are also # displayed further in the tree - children.append( - DeviceParameter(subdevice, subdevices=False) - ) + children.append(DeviceParameter(subdevice, subdevices=False)) else: # Otherwise just make a regular parameter out of it - child_name = clean_name(subdevice, - strip_parent=subdevice.root) + child_name = clean_name(subdevice, strip_parent=subdevice.root) param = SidebarParameter( - value=partial(TyphosDeviceDisplay.from_device, - subdevice), + value=partial(TyphosDeviceDisplay.from_device, subdevice), name=child_name, embeddable=True, devices=[subdevice], ) children.append(param) - opts['children'] = children + opts["children"] = children super().__init__( value=partial(TyphosDeviceDisplay.from_device, device), - embeddable=opts.pop('embeddable', True), + embeddable=opts.pop("embeddable", True), devices=[device], - **opts + **opts, ) @@ -245,8 +243,8 @@ class TyphosSuite(TyphosBase): ``{'tool_name': ToolClass}``. """ - DEFAULT_TITLE = 'Typhos Suite' - DEFAULT_TITLE_DEVICE = 'Typhos Suite - {device.name}' + DEFAULT_TITLE = "Typhos Suite" + DEFAULT_TITLE_DEVICE = "Typhos Suite - {device.name}" default_tools = { "Log": TyphosLogDisplay, @@ -268,17 +266,13 @@ def __init__( self._tree = parametertree.ParameterTree(parent=self, showHeader=False) self._tree.setAlternatingRowColors(False) - self._save_action = ptypes.ActionParameter(name='Save Suite', value=None) + self._save_action = ptypes.ActionParameter(name="Save Suite", value=None) self._tree.addParameters(self._save_action) self._save_action.sigActivated.connect(self.save) - self._bar = pcdsutils.qt.QPopBar(title='Suite', parent=self, - widget=self._tree, pin=pin) + self._bar = pcdsutils.qt.QPopBar(title="Suite", parent=self, widget=self._tree, pin=pin) - self._tree.setSizePolicy( - QtWidgets.QSizePolicy.MinimumExpanding, - QtWidgets.QSizePolicy.MinimumExpanding - ) + self._tree.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding) self._tree.setMinimumSize(250, 150) self._content_frame = QtWidgets.QFrame(self) @@ -319,15 +313,12 @@ def add_subdisplay(self, name, display, category): The top level group to place the controls under in the tree. If the category does not exist, a new one will be made """ - logger.debug("Adding widget %r with %r to %r ...", - name, display, category) + logger.debug("Adding widget %r with %r to %r ...", name, display, category) # Create our parameter parameter = SidebarParameter(value=display, name=name) self._add_to_sidebar(parameter, category) - def add_lazy_subdisplay( - self, name: str, display_class: type[QtWidgets.QWidget], category: str - ): + def add_lazy_subdisplay(self, name: str, display_class: type[QtWidgets.QWidget], category: str): """ Add an arbitrary widget to the tree of available widgets and tools. @@ -343,13 +334,9 @@ def add_lazy_subdisplay( The top level group to place the controls under in the tree. If the category does not exist, a new one will be made """ - logger.debug("Adding lazy subdisplay %r with %r to %r ...", - name, display_class, category) + logger.debug("Adding lazy subdisplay %r with %r to %r ...", name, display_class, category) # Create our parameter - parameter = SidebarParameter( - value=LazySubdisplay(display_class), - name=name - ) + parameter = SidebarParameter(value=LazySubdisplay(display_class), name=name) self._add_to_sidebar(parameter, category) @property @@ -364,9 +351,7 @@ def top_level_groups(self): {'name': QGroupParameterItem} """ root = self._tree.invisibleRootItem() - return {root.child(idx).param.name(): - root.child(idx).param - for idx in range(root.childCount())} + return {root.child(idx).param.name(): root.child(idx).param for idx in range(root.childCount())} def add_tool(self, name: str, tool: type[QtWidgets.QWidget]): """ @@ -415,11 +400,7 @@ def get_subdisplay(self, display: Union[Device, str], instantiate: bool = True): if not isinstance(display, SidebarParameter): for group in self.top_level_groups.values(): tree = flatten_tree(group) - matches = [ - param for param in tree - if hasattr(param, 'has_device') and - param.has_device(display) - ] + matches = [param for param in tree if hasattr(param, "has_device") and param.has_device(display)] if matches: display = matches[0] @@ -432,9 +413,7 @@ def get_subdisplay(self, display: Union[Device, str], instantiate: bool = True): subdisplay = display.value() if isinstance(subdisplay, partial): if not instantiate: - raise TyphosDisplayNotCreatedError( - f"Subdisplay {display} has not been created yet" - ) + raise TyphosDisplayNotCreatedError(f"Subdisplay {display} has not been created yet") subdisplay = subdisplay() display.setValue(subdisplay) @@ -471,7 +450,7 @@ def show_subdisplay( self._show_sidebar(widget, dock) # Add the widget to the dock logger.debug("Showing widget %r ...", widget) - if hasattr(widget, 'scroll_option'): + if hasattr(widget, "scroll_option"): widget.scroll_option = self.scroll_option if hasattr(widget, "display_type"): # Setting a display_type implicitly loads the best template. @@ -482,12 +461,8 @@ def show_subdisplay( content_layout = self._content_frame.layout() content_layout.addWidget(dock) if isinstance(content_layout, QtWidgets.QGridLayout): - self._content_frame.layout().setAlignment( - dock, QtCore.Qt.AlignHCenter - ) - self._content_frame.layout().setAlignment( - dock, QtCore.Qt.AlignTop - ) + self._content_frame.layout().setAlignment(dock, QtCore.Qt.AlignHCenter) + self._content_frame.layout().setAlignment(dock, QtCore.Qt.AlignTop) self._new_template() if isinstance(widget, TyphosDeviceDisplay): @@ -523,8 +498,7 @@ def embed_subdisplay(self, widget): widget.setVisible(True) widget.display_type = widget.embedded_screen widget_count = self.embedded_dock.widget().layout().count() - self.embedded_dock.widget().layout().insertWidget(widget_count - 1, - widget) + self.embedded_dock.widget().layout().insertWidget(widget_count - 1, widget) @QtCore.Slot() @QtCore.Slot(object) @@ -558,10 +532,8 @@ def hide_subdisplay(self, widget): logger.debug("Closing dock ...") widget.parent().close() # Hide the full dock if this is the last widget - elif (self.embedded_dock - and widget.parent() == self.embedded_dock.widget()): - logger.debug("Removing %r from embedded widget layout ...", - widget) + elif self.embedded_dock and widget.parent() == self.embedded_dock.widget(): + logger.debug("Removing %r from embedded widget layout ...", widget) self.embedded_dock.widget().layout().removeWidget(widget) widget.hide() if self.embedded_dock.widget().layout().count() == 1: @@ -582,9 +554,8 @@ def hide_subdisplays(self): @property def tools(self): """Tools loaded into the suite.""" - if 'Tools' in self.top_level_groups: - return [param.value() - for param in self.top_level_groups['Tools'].childs] + if "Tools" in self.top_level_groups: + return [param.value() for param in self.top_level_groups["Tools"].childs] return [] def _update_title(self, device=None): @@ -596,12 +567,11 @@ def _update_title(self, device=None): device : ophyd.Device, optional Device to indicate in the title. """ - title_fmt = (self.DEFAULT_TITLE if device is None - else self.DEFAULT_TITLE_DEVICE) + title_fmt = self.DEFAULT_TITLE if device is None else self.DEFAULT_TITLE_DEVICE self.setWindowTitle(title_fmt.format(self=self, device=device)) - def add_device(self, device, children=True, category='Devices'): + def add_device(self, device, children=True, category="Devices"): """ Add a device to the suite. @@ -631,8 +601,7 @@ def add_device(self, device, children=True, category='Devices'): try: tool.add_device(device) except Exception: - logger.exception("Unable to add %s to tool %s", - device.name, type(tool)) + logger.exception("Unable to add %s to tool %s", device.name, type(tool)) @classmethod def from_device( @@ -691,12 +660,17 @@ def from_device( **kwargs : Passed to :meth:`TyphosSuite.add_device` """ - return cls.from_devices([device], parent=parent, tools=tools, pin=pin, - content_layout=content_layout, - default_display_type=default_display_type, - scroll_option=scroll_option, - show_displays=show_displays, - **kwargs) + return cls.from_devices( + [device], + parent=parent, + tools=tools, + pin=pin, + content_layout=content_layout, + default_display_type=default_display_type, + scroll_option=scroll_option, + show_displays=show_displays, + **kwargs, + ) @classmethod def from_devices( @@ -779,8 +753,7 @@ def from_devices( if show_displays: suite.show_subdisplay(device) except Exception: - logger.exception("Unable to add %r to TyphosSuite", - device.name) + logger.exception("Unable to add %r to TyphosSuite", device.name) return suite def save(self): @@ -798,11 +771,10 @@ def save(self): logger.debug("Requesting file location for saved TyphosSuite") root_dir = os.getcwd() - filename = QtWidgets.QFileDialog.getSaveFileName( - self, 'Save TyphosSuite', root_dir, "Python (*.py)") + filename = QtWidgets.QFileDialog.getSaveFileName(self, "Save TyphosSuite", root_dir, "Python (*.py)") if filename: try: - with open(filename[0], 'w+') as handle: + with open(filename[0], "w+") as handle: save_suite(self, handle) except Exception as exc: logger.exception("Failed to save TyphosSuite") @@ -811,7 +783,7 @@ def save(self): logger.debug("No filename chosen") # Add the template to the docstring - save.__doc__ += textwrap.indent('\n' + utils.saved_template, '\t\t') + save.__doc__ += textwrap.indent("\n" + utils.saved_template, "\t\t") def save_screenshot( self, @@ -826,7 +798,8 @@ def save_screenshot( logger.info( "Saving screenshot of suite titled '%s' to '%s'", - self.windowTitle(), filename, + self.windowTitle(), + filename, ) image.save(filename) return True @@ -852,7 +825,8 @@ def save_device_screenshots( if image is None: logger.warning( "Failed to take screenshot of device: %s in %s", - device.name, suite_title, + device.name, + suite_title, ) continue @@ -864,7 +838,9 @@ def save_device_screenshots( ) logger.info( "Saving screenshot of '%s': '%s' to '%s'", - suite_title, widget_title, filename, + suite_title, + widget_title, + filename, ) image.save(filename) filenames[device.name] = filename @@ -874,7 +850,10 @@ def _get_sidebar(self, widget): items = {} for group in self.top_level_groups.values(): for item in flatten_tree(group): - items[item.value()] = item + try: + items[item.value()] = item + except ValueError: + ... return items.get(widget) def _show_sidebar(self, widget, dock): @@ -883,9 +862,7 @@ def _show_sidebar(self, widget, dock): for item in sidebar.items: item._mark_shown() # Make sure we react if the dock is closed outside of our menu - self._connect_partial_weakly( - dock, dock.closing, self.hide_subdisplay, sidebar - ) + self._connect_partial_weakly(dock, dock.closing, self.hide_subdisplay, sidebar) else: logger.warning("Unable to find sidebar item for %r", widget) @@ -900,8 +877,7 @@ def _add_to_sidebar(self, parameter, category=None): group = ptypes.GroupParameter(name=category, value=None) self._tree.addParameters(group) self._tree.sortItems(0, QtCore.Qt.AscendingOrder) - logger.debug("Adding %r to category %r ...", - parameter.name(), group.name()) + logger.debug("Adding %r to category %r ...", parameter.name(), group.name()) group.addChild(parameter) widget = parameter.value() @@ -911,14 +887,8 @@ def _add_to_sidebar(self, parameter, category=None): widget.setHidden(True) logger.debug("Connecting parameter signals ...") - self._connect_partial_weakly( - parameter, parameter.sigOpen, self.show_subdisplay, parameter - ) - self._connect_partial_weakly( - parameter, parameter.sigHide, self.hide_subdisplay, parameter - ) + self._connect_partial_weakly(parameter, parameter.sigOpen, self.show_subdisplay, parameter) + self._connect_partial_weakly(parameter, parameter.sigHide, self.hide_subdisplay, parameter) if parameter.embeddable: - self._connect_partial_weakly( - parameter, parameter.sigEmbed, self.embed_subdisplay, parameter - ) + self._connect_partial_weakly(parameter, parameter.sigEmbed, self.embed_subdisplay, parameter) return parameter diff --git a/typhos/tests/conftest.py b/typhos/tests/conftest.py index f9188994..8b84a080 100644 --- a/typhos/tests/conftest.py +++ b/typhos/tests/conftest.py @@ -38,38 +38,34 @@ def pytest_addoption(parser): - parser.addoption("--dark", action="store_true", default=False, - help="Use the dark stylesheet to display widgets") - parser.addoption("--show-ui", action="store_true", default=False, - help="Show the widgets produced by each test") + parser.addoption("--dark", action="store_true", default=False, help="Use the dark stylesheet to display widgets") + parser.addoption("--show-ui", action="store_true", default=False, help="Show the widgets produced by each test") # Create a fixture to configure whether widgets are shown or not -@pytest.fixture(scope='session', autouse=True) +@pytest.fixture(scope="session", autouse=True) def _show_widgets(pytestconfig): global show_widgets - show_widgets = pytestconfig.getoption('--show-ui') + show_widgets = pytestconfig.getoption("--show-ui") if show_widgets: logger.info("Running tests while showing created widgets ...") -@pytest.fixture(scope='session', autouse=True) +@pytest.fixture(scope="session", autouse=True) def qapp(pytestconfig): global application application = QtWidgets.QApplication.instance() if application is None: application = PyDMApplication(use_main_window=False) - typhos.use_stylesheet(pytestconfig.getoption('--dark')) + typhos.use_stylesheet(pytestconfig.getoption("--dark")) return application -@pytest.fixture(scope='function', autouse=True) +@pytest.fixture(scope="function", autouse=True) def noapp(monkeypatch): - monkeypatch.setattr(QtWidgets.QApplication, 'exec_', lambda x: 1) - monkeypatch.setattr(QtWidgets.QApplication, 'exit', lambda x: 1) - monkeypatch.setattr( - pydm.exception, 'raise_to_operator', lambda *_, **__: None - ) + monkeypatch.setattr(QtWidgets.QApplication, "exec_", lambda x: 1) + monkeypatch.setattr(QtWidgets.QApplication, "exit", lambda x: 1) + monkeypatch.setattr(pydm.exception, "raise_to_operator", lambda *_, **__: None) def get_top_level_widgets() -> List[weakref.ReferenceType[QtWidgets.QWidget]]: @@ -96,8 +92,7 @@ def _dump_widgets(widgets: List[weakref.ReferenceType[QtWidgets.QWidget]]) -> No continue logger.debug( - f"Widget remains live: {widget} {widget.windowTitle()} " - f"parent={widget.parent()} name={widget.objectName()}" + f"Widget remains live: {widget} {widget.windowTitle()} parent={widget.parent()} name={widget.objectName()}" ) @@ -140,8 +135,8 @@ def pytest_runtest_call(item: pytest.Item): time.sleep(0.1) widgets_to_check = list( - weakref.ref(w) for w in - set(_dereference_list(ending_widgets)) + weakref.ref(w) + for w in set(_dereference_list(ending_widgets)) - set(_dereference_list(starting_widgets)) - set(_dereference_list(qtbot_widgets)) ) @@ -186,15 +181,10 @@ def pytest_runtest_call(item: pytest.Item): # Everything's destructible! Yeah! ref_desc.append(f"(exception) {ex}") desc = f"{classname} {desc} {title} referrers={num_referrers}: " - cleanup_descriptions.append( - "\n".join((desc, "\n -> ".join([""] + ref_desc))) - ) + cleanup_descriptions.append("\n".join((desc, "\n -> ".join([""] + ref_desc)))) referrers.clear() - cleanup_text = ( - f"Not all widgets were cleaned up during {item.name}:\n" - + "\n".join(sorted(cleanup_descriptions)) - ) + cleanup_text = f"Not all widgets were cleaned up during {item.name}:\n" + "\n".join(sorted(cleanup_descriptions)) failure_text = f"{item.nodeid}: {cleanup_text}" if final_widgets: @@ -218,16 +208,18 @@ def pytest_runtest_call(item: pytest.Item): ... -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def test_images(): - return (os.path.join(os.path.dirname(__file__), 'utils/lenna.png'), - os.path.join(os.path.dirname(__file__), 'utils/python.png')) + return ( + os.path.join(os.path.dirname(__file__), "utils/lenna.png"), + os.path.join(os.path.dirname(__file__), "utils/python.png"), + ) def save_image(widget, name, delay=0.5): - ''' + """ Save `widget` to typhos/tests/artifacts/{name}.png after `delay` seconds. - ''' + """ widget.show() app = QtWidgets.QApplication.instance() @@ -237,8 +229,7 @@ def save_image(widget, name, delay=0.5): app.processEvents() time.sleep(0.1) - image = QtGui.QImage(widget.width(), widget.height(), - QtGui.QImage.Format_ARGB32_Premultiplied) + image = QtGui.QImage(widget.width(), widget.height(), QtGui.QImage.Format_ARGB32_Premultiplied) image.fill(qtpy.QtCore.Qt.transparent) pixmap = QtGui.QPixmap(image) @@ -247,18 +238,19 @@ def save_image(widget, name, delay=0.5): widget.render(image) painter.end() - artifacts_path = MODULE_PATH / 'artifacts' + artifacts_path = MODULE_PATH / "artifacts" artifacts_path.mkdir(exist_ok=True) - path = str(artifacts_path / f'{name}.png') + path = str(artifacts_path / f"{name}.png") image.save(path) - logger.debug('saved image to %s', path) + logger.debug("saved image to %s", path) def show_widget(func): """ Show a widget returned from arbitrary `func` """ + @wraps(func) def func_wrapper(*args, **kwargs): # Run function grab widget @@ -274,10 +266,11 @@ def func_wrapper(*args, **kwargs): pydm.utilities.close_widget_connections(widget) except Exception: logger.debug("Failed to close widget connections for %s", widget) + return func_wrapper -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def motor(): # Register all signals for sig in ophyd.sim.motor.component_names: @@ -286,13 +279,12 @@ def motor(): class RichSignal(Signal): - def __init__(self, *args, metadata=None, **kwargs): if metadata is None: metadata = { - 'enum_strs': ('a', 'b', 'c'), - 'precision': 2, - 'units': 'urad', + "enum_strs": ("a", "b", "c"), + "precision": 2, + "units": "urad", } super().__init__(*args, metadata=metadata, **kwargs) @@ -323,9 +315,9 @@ def describe(self, *args, **kwargs): class ConfiguredSynAxis(SynAxis): - velocity = Cpt(Signal, value=100, kind='normal') - acceleration = Cpt(Signal, value=10, kind='normal') - resolution = Cpt(Signal, value=5, kind='normal') + velocity = Cpt(Signal, value=100, kind="normal") + acceleration = Cpt(Signal, value=10, kind="normal") + resolution = Cpt(Signal, value=5, kind="normal") class RandomSignal(SynPeriodicSignal): @@ -334,38 +326,36 @@ class RandomSignal(SynPeriodicSignal): """ def __init__(self, *args, **kwargs): - super().__init__(func=lambda: np.random.uniform(0, 100), - period=10, period_jitter=4, **kwargs) + super().__init__(func=lambda: np.random.uniform(0, 100), period=10, period_jitter=4, **kwargs) self.start_simulation() class MockDevice(Device): # Device signals - readback = Cpt(RandomSignal, kind='normal') - noise = Cpt(RandomSignal, kind='normal') - transmorgifier = Cpt(SignalRO, value=4, kind='normal') - setpoint = Cpt(Signal, value=0, kind='normal') - - velocity = Cpt(Signal, value=1, kind='config') - flux = Cpt(RandomSignal, kind='config') - modified_flux = Cpt(RandomSignal, kind='config') - capacitance = Cpt(RandomSignal, kind='config') - acceleration = Cpt(Signal, value=3, kind='config') - limit = Cpt(Signal, value=4, kind='config') - inductance = Cpt(RandomSignal, kind='normal') - - transformed_inductance = Cpt(SignalRO, value=3, kind='omitted') - core_temperature = Cpt(RandomSignal, kind='omitted') - resolution = Cpt(Signal, value=5, kind='omitted') - duplicator = Cpt(Signal, value=6, kind='omitted') + readback = Cpt(RandomSignal, kind="normal") + noise = Cpt(RandomSignal, kind="normal") + transmorgifier = Cpt(SignalRO, value=4, kind="normal") + setpoint = Cpt(Signal, value=0, kind="normal") + + velocity = Cpt(Signal, value=1, kind="config") + flux = Cpt(RandomSignal, kind="config") + modified_flux = Cpt(RandomSignal, kind="config") + capacitance = Cpt(RandomSignal, kind="config") + acceleration = Cpt(Signal, value=3, kind="config") + limit = Cpt(Signal, value=4, kind="config") + inductance = Cpt(RandomSignal, kind="normal") + + transformed_inductance = Cpt(SignalRO, value=3, kind="omitted") + core_temperature = Cpt(RandomSignal, kind="omitted") + resolution = Cpt(Signal, value=5, kind="omitted") + duplicator = Cpt(Signal, value=6, kind="omitted") # Component Motors - x = FC(ConfiguredSynAxis, name='X Axis') - y = FC(ConfiguredSynAxis, name='Y Axis') - z = FC(ConfiguredSynAxis, name='Z Axis') + x = FC(ConfiguredSynAxis, name="X Axis") + y = FC(ConfiguredSynAxis, name="Y Axis") + z = FC(ConfiguredSynAxis, name="Z Axis") - def insert(self, width: float = 2.0, height: float = 2.0, - fast_mode: bool = False): + def insert(self, width: float = 2.0, height: float = 2.0, fast_mode: bool = False): """Fake insert function to display""" pass @@ -375,12 +365,12 @@ def remove(self, height: float, fast_mode: bool = False): @property def hints(self): - return {'fields': [self.name+'_readback']} + return {"fields": [self.name + "_readback"]} -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def device(): - dev = MockDevice('Tst:This', name='simulated_device') + dev = MockDevice("Tst:This", name="simulated_device") yield dev clear_handlers(dev) @@ -396,22 +386,21 @@ def clear_handlers(device): _logger.handlers.remove(handler) -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def client(): - client = Client(path=os.path.join(os.path.dirname(__file__), - 'happi.json')) + client = Client(path=os.path.join(os.path.dirname(__file__), "happi.json")) register_client(client) return client -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def happi_cfg(): - path = str(MODULE_PATH / 'happi.cfg') - os.environ['HAPPI_CFG'] = path + path = str(MODULE_PATH / "happi.cfg") + os.environ["HAPPI_CFG"] = path return path -@pytest.fixture(scope='function', autouse=True) +@pytest.fixture(scope="function", autouse=True) def reset_signal_plugin(): """ Completely restart the sig:// plugin. @@ -425,18 +414,18 @@ def reset_signal_plugin(): manipulated or tested. """ signal_registry.clear() - plugin = plugin_for_address('sig://test') + plugin = plugin_for_address("sig://test") for channel in list(plugin.channels): channel.disconnect(destroying=True) -@pytest.fixture(scope='function', autouse=True) +@pytest.fixture(scope="function", autouse=True) def show_test_name(request): - logger.debug(f'Running test named {request.node.name}') + logger.debug(f"Running test named {request.node.name}") # Remove this later please pydm_version_xfail = pytest.mark.xfail( - condition=pydm.__version__ == '1.17.0', - reason='PyDM bug in v1.17.0', + condition=pydm.__version__ == "1.17.0", + reason="PyDM bug in v1.17.0", ) diff --git a/typhos/tests/plugins/test_core.py b/typhos/tests/plugins/test_core.py index 74f1fd1a..e94a41b5 100644 --- a/typhos/tests/plugins/test_core.py +++ b/typhos/tests/plugins/test_core.py @@ -10,24 +10,23 @@ from pydm.widgets import PyDMChannel, PyDMLineEdit from pytestqt.qtbot import QtBot -from typhos.plugins.core import (SignalConnection, register_signal, - signal_registry) +from typhos.plugins.core import SignalConnection, register_signal, signal_registry from ..conftest import DeadSignal, RichSignal def test_signal_connection(qapp, qtbot): # Create a signal and attach our listener - sig = Signal(name='my_signal', value=1) + sig = Signal(name="my_signal", value=1) register_signal(sig) widget = PyDMLineEdit() qtbot.addWidget(widget) - widget.channel = 'sig://my_signal' + widget.channel = "sig://my_signal" listener = widget.channels()[0] # If PyDMChannel can not connect, we need to connect it ourselves # In PyDM > 1.5.0 this will not be neccesary as the widget will be # connected after we set the channel name - if not hasattr(listener, 'connect'): + if not hasattr(listener, "connect"): pydm.utilities.establish_widget_connections(widget) # Check that our widget receives the initial value qapp.processEvents() @@ -46,7 +45,7 @@ def test_signal_connection(qapp, qtbot): qapp.processEvents() # Must be called twice. Multiple rounds of signals # In PyDM > 1.5.0 we will not need the application to disconnect the # widget, but until then we have to check for the attribute - if hasattr(listener, 'disconnect'): + if hasattr(listener, "disconnect"): listener.disconnect() else: qapp.close_widget_connections(widget) @@ -64,45 +63,45 @@ def test_dotted_name(): class TestDevice(Device): test = Cpt(Signal) - device = TestDevice(name='test') + device = TestDevice(name="test") register_signal(device.test) - assert 'test.test' in signal_registry + assert "test.test" in signal_registry def test_metadata(qapp, qtbot): widget = PyDMLineEdit() qtbot.addWidget(widget) - widget.channel = 'sig://md_signal' + widget.channel = "sig://md_signal" listener = widget.channels()[0] # Create a signal and attach our listener - sig = RichSignal(name='md_signal', value=1) + sig = RichSignal(name="md_signal", value=1) register_signal(sig) - _ = SignalConnection(listener, 'md_signal') + _ = SignalConnection(listener, "md_signal") qapp.processEvents() # Check that metadata the metadata got there - assert widget.enum_strings == ('a', 'b', 'c') - assert widget._unit == 'urad' + assert widget.enum_strings == ("a", "b", "c") + assert widget._unit == "urad" assert widget._prec == 2 def test_find_signal(qapp, qtbot): widget = PyDMLineEdit() qtbot.addWidget(widget) - widget.channel = 'sig://md_signal' + widget.channel = "sig://md_signal" listener = widget.channels()[0] # Override the signal getter method to test - sig = RichSignal(name='md_signal', value=1) + sig = RichSignal(name="md_signal", value=1) class CustomConnection(SignalConnection): def find_signal(self, address): return sig - _ = CustomConnection(listener, 'md_signal') + _ = CustomConnection(listener, "md_signal") qapp.processEvents() # Check that metadata the metadata got there - assert widget.enum_strings == ('a', 'b', 'c') - assert widget._unit == 'urad' + assert widget.enum_strings == ("a", "b", "c") + assert widget._unit == "urad" assert widget._prec == 2 @@ -178,37 +177,37 @@ def test_precision_defaults( def test_disconnection(qtbot): widget = PyDMLineEdit() qtbot.addWidget(widget) - widget.channel = 'sig://invalid' + widget.channel = "sig://invalid" listener = widget.channels()[0] # Non-existant signal doesn't raise an error listener.connect() # Create a signal that will raise a TimeoutError - sig = DeadSignal(name='broken_signal', value=1) + sig = DeadSignal(name="broken_signal", value=1) register_signal(sig) - listener.address = 'sig://broken_signal' + listener.address = "sig://broken_signal" # This should fail on the subscribe listener.connect() # This should fail on the get sig.subscribable = True - _ = SignalConnection(listener, 'broken_signal') + _ = SignalConnection(listener, "broken_signal") def test_array_signal_send_value(qapp, qtbot): - sig = Signal(name='my_array', value=np.ones(4)) + sig = Signal(name="my_array", value=np.ones(4)) register_signal(sig) widget = PyDMLineEdit() qtbot.addWidget(widget) - widget.channel = 'sig://my_array' + widget.channel = "sig://my_array" qapp.processEvents() assert all(widget.value == np.ones(4)) def test_array_signal_put_value(qapp, qtbot): - sig = Signal(name='my_array_write', value=np.ones(4)) + sig = Signal(name="my_array_write", value=np.ones(4)) register_signal(sig) widget = PyDMLineEdit() qtbot.addWidget(widget) - widget.channel = 'sig://my_array_write' + widget.channel = "sig://my_array_write" widget.send_value_signal[np.ndarray].emit(np.zeros(4)) qapp.processEvents() assert all(sig.get() == np.zeros(4)) diff --git a/typhos/tests/plugins/test_happi.py b/typhos/tests/plugins/test_happi.py index 3202c46a..d7ecf3c0 100644 --- a/typhos/tests/plugins/test_happi.py +++ b/typhos/tests/plugins/test_happi.py @@ -39,7 +39,7 @@ def test_connection( # Register a channel and check we received object and metadata mock = Mock() - hc = HappiChannel(address='happi://test_device', tx_slot=mock) + hc = HappiChannel(address="happi://test_device", tx_slot=mock) hc.connect() assert set(happi_plugin.channels) == {hc} @@ -50,11 +50,11 @@ def mock_called(): qtbot.wait_until(mock_called) tx = mock.call_args[0][0] - assert isinstance(tx['obj'], ophyd.sim.SynAxis) - assert isinstance(tx['md'], dict) + assert isinstance(tx["obj"], ophyd.sim.SynAxis) + assert isinstance(tx["md"], dict) # Add another object and check that the connection does refire mock2 = Mock() - hc2 = HappiChannel(address='happi://test_device', tx_slot=mock2) + hc2 = HappiChannel(address="happi://test_device", tx_slot=mock2) hc2.connect() assert set(happi_plugin.channels) == {hc, hc2} @@ -77,7 +77,7 @@ def test_connection_for_child( happi_plugin: HappiPlugin, ): mock = Mock() - hc = HappiChannel(address='happi://test_motor.setpoint', tx_slot=mock) + hc = HappiChannel(address="happi://test_motor.setpoint", tx_slot=mock) hc.connect() def mock_called(): @@ -85,16 +85,16 @@ def mock_called(): qtbot.wait_until(mock_called) tx = mock.call_args[0][0] - assert tx['obj'].name == 'test_motor_setpoint' + assert tx["obj"].name == "test_motor_setpoint" def test_bad_address_smoke(client: happi.Client): - hc = HappiChannel(address='happi://not_a_device', tx_slot=lambda x: None) + hc = HappiChannel(address="happi://not_a_device", tx_slot=lambda x: None) hc.connect() def test_happi_is_optional(): - with patch.dict(sys.modules, {'happi': None}): + with patch.dict(sys.modules, {"happi": None}): importlib.reload(typhos.plugins) importlib.reload(typhos) - assert sys.modules['happi'] is None + assert sys.modules["happi"] is None diff --git a/typhos/tests/test_alarm.py b/typhos/tests/test_alarm.py index 6c5b7497..a64efd7d 100644 --- a/typhos/tests/test_alarm.py +++ b/typhos/tests/test_alarm.py @@ -7,9 +7,14 @@ from ophyd import Device from ophyd.utils.epics_pvs import AlarmSeverity -from typhos.alarm import (AlarmLevel, TyphosAlarmCircle, TyphosAlarmEllipse, - TyphosAlarmPolygon, TyphosAlarmRectangle, - TyphosAlarmTriangle) +from typhos.alarm import ( + AlarmLevel, + TyphosAlarmCircle, + TyphosAlarmEllipse, + TyphosAlarmPolygon, + TyphosAlarmRectangle, + TyphosAlarmTriangle, +) from typhos.plugins.core import register_signal from typhos.plugins.happi import HappiClientState, register_client @@ -17,7 +22,7 @@ @pytest.fixture( - scope='function', + scope="function", params=( TyphosAlarmCircle, TyphosAlarmRectangle, @@ -35,18 +40,18 @@ def alarm(qtbot, request): class SimpleDevice(Device): - hint_sig = Cpt(RichSignal, kind='hinted') - norm_sig = Cpt(RichSignal, kind='normal') - conf_sig = Cpt(RichSignal, kind='config') - omit_sig = Cpt(RichSignal, kind='omitted') + hint_sig = Cpt(RichSignal, kind="hinted") + norm_sig = Cpt(RichSignal, kind="normal") + conf_sig = Cpt(RichSignal, kind="config") + omit_sig = Cpt(RichSignal, kind="omitted") -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def device(): - return SimpleDevice(name='simple_' + str(uuid4())) + return SimpleDevice(name="simple_" + str(uuid4())) -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def alarm_add_device(alarm, device, qtbot): with qtbot.wait_signal(alarm.alarm_changed, timeout=1000): alarm.add_device(device) @@ -64,17 +69,15 @@ def test_alarm_add_device_basic(alarm_add_device): alarm_cases = [ - ({'connected': False}, AlarmLevel.DISCONNECTED), - ({'severity': AlarmSeverity.MINOR}, AlarmLevel.MINOR), - ({'severity': AlarmSeverity.MAJOR}, AlarmLevel.MAJOR), - ({'severity': AlarmSeverity.INVALID}, AlarmLevel.INVALID), + ({"connected": False}, AlarmLevel.DISCONNECTED), + ({"severity": AlarmSeverity.MINOR}, AlarmLevel.MINOR), + ({"severity": AlarmSeverity.MAJOR}, AlarmLevel.MAJOR), + ({"severity": AlarmSeverity.INVALID}, AlarmLevel.INVALID), ] @pytest.mark.parametrize("metadata,response", alarm_cases) -def test_one_alarm_add_device( - alarm_add_device, device, qtbot, metadata, response -): +def test_one_alarm_add_device(alarm_add_device, device, qtbot, metadata, response): alarm = alarm_add_device with qtbot.wait_signal(alarm.alarm_changed, timeout=1000): @@ -85,12 +88,12 @@ def test_one_alarm_add_device( @pytest.mark.parametrize("metadata,response", alarm_cases) def test_one_alarm_sig_ch(alarm, qtbot, metadata, response): - name = 'one_sig_ch_' + str(uuid4()) + name = "one_sig_ch_" + str(uuid4()) sig = RichSignal(name=name) register_signal(sig) with qtbot.wait_signal(alarm.alarm_changed, timeout=1000): - alarm.channel = 'sig://' + name + alarm.channel = "sig://" + name with qtbot.wait_signal(alarm.alarm_changed, timeout=1000): sig.update_metadata(metadata) @@ -98,7 +101,7 @@ def test_one_alarm_sig_ch(alarm, qtbot, metadata, response): assert alarm.alarm_summary == response -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def fake_client(): old_client = HappiClientState.client client = FakeClient() @@ -111,19 +114,19 @@ class FakeClient: def find_item(self, *args, name, **kwargs): return HappiItem( name=name, - device_class='typhos.tests.test_alarm.SimpleDevice', - kwargs={'name': '{{name}}'}, + device_class="typhos.tests.test_alarm.SimpleDevice", + kwargs={"name": "{{name}}"}, ) @pytest.mark.parametrize("metadata,response", alarm_cases) def test_one_alarm_happi_ch(alarm, qtbot, metadata, response, fake_client): - name = 'happi_test_device_' + str(uuid4()).replace('-', '_') + name = "happi_test_device_" + str(uuid4()).replace("-", "_") item = fake_client.find_item(name=name) device = from_container(item) with qtbot.wait_signal(alarm.alarm_changed, timeout=1000): - alarm.channel = 'happi://' + name + alarm.channel = "happi://" + name with qtbot.wait_signal(alarm.alarm_changed, timeout=1000): device.hint_sig.update_metadata(metadata) @@ -134,10 +137,10 @@ def test_one_alarm_happi_ch(alarm, qtbot, metadata, response, fake_client): def test_kinds_many_alarms_add_device(alarm_add_device, device, qtbot): alarm = alarm_add_device - device.hint_sig.update_metadata({'severity': AlarmSeverity.NO_ALARM}) - device.norm_sig.update_metadata({'severity': AlarmSeverity.MINOR}) - device.conf_sig.update_metadata({'severity': AlarmSeverity.MAJOR}) - device.omit_sig.update_metadata({'severity': AlarmSeverity.INVALID}) + device.hint_sig.update_metadata({"severity": AlarmSeverity.NO_ALARM}) + device.norm_sig.update_metadata({"severity": AlarmSeverity.MINOR}) + device.conf_sig.update_metadata({"severity": AlarmSeverity.MAJOR}) + device.omit_sig.update_metadata({"severity": AlarmSeverity.INVALID}) assert alarm.alarm_summary == AlarmLevel.NO_ALARM @@ -159,6 +162,6 @@ def test_kinds_many_alarms_add_device(alarm_add_device, device, qtbot): # Disconnect the no_alarm signal to look for a response with qtbot.wait_signal(alarm.alarm_changed, timeout=1000): - device.hint_sig.update_metadata({'connected': False}) + device.hint_sig.update_metadata({"connected": False}) assert alarm.alarm_summary == AlarmLevel.DISCONNECTED diff --git a/typhos/tests/test_benchmark.py b/typhos/tests/test_benchmark.py index f061b562..848716c9 100644 --- a/typhos/tests/test_benchmark.py +++ b/typhos/tests/test_benchmark.py @@ -18,15 +18,11 @@ def get_top_level_suites() -> list[TyphosSuite]: app = QtWidgets.QApplication.instance() assert app is not None - return list( - widget - for widget in app.topLevelWidgets() - if isinstance(widget, TyphosSuite) - ) + return list(widget for widget in app.topLevelWidgets() if isinstance(widget, TyphosSuite)) # Name the test cases using the keys, run using the values -@pytest.mark.parametrize('unit_test_name', unit_tests.keys()) +@pytest.mark.parametrize("unit_test_name", unit_tests.keys()) @pytest.mark.skipif( sys.version_info >= (3, 11), reason="Benchmarks not fully working on Python 3.11", @@ -41,7 +37,7 @@ def test_benchmark(unit_test_name, qapp, qtbot, benchmark, monkeypatch, request) assert len(get_top_level_suites()) == 0 PV.count = property(lambda self: 1) suite = benchmark(inner_benchmark, unit_test_name, qtbot, request) - save_image(suite, 'test_benchmark_' + unit_test_name) + save_image(suite, "test_benchmark_" + unit_test_name) def inner_benchmark(unit_test_name, qtbot, request): @@ -60,7 +56,7 @@ def test_profiler(capsys): pytest.xfail( reason="Known issue: profiler doesn't quite work properly on Python 3.12", ) - with profiler_context(['typhos.benchmark.utils']): + with profiler_context(["typhos.benchmark.utils"]): utils.get_native_functions(utils) output = capsys.readouterr() - assert 'get_native_functions' in output.out + assert "get_native_functions" in output.out diff --git a/typhos/tests/test_cache.py b/typhos/tests/test_cache.py index 96d8d080..35aa0407 100644 --- a/typhos/tests/test_cache.py +++ b/typhos/tests/test_cache.py @@ -38,7 +38,7 @@ def ensure_cache_clear(qtbot, signal, cache): cache.clear() -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def describe_cache(qtbot): cache = typhos.cache.get_global_describe_cache() @@ -47,7 +47,7 @@ def describe_cache(qtbot): ensure_cache_clear(qtbot, cache.new_description, cache) -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def type_cache(qtbot, describe_cache): cache = typhos.cache.get_global_widget_type_cache() @@ -56,9 +56,9 @@ def type_cache(qtbot, describe_cache): ensure_cache_clear(qtbot, cache.widgets_determined, cache) -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def sig(): - name = f'test{random.randint(0, 10000)}' + name = f"test{random.randint(0, 10000)}" sig = ophyd.Signal(name=name) yield sig sig.destroy() diff --git a/typhos/tests/test_cli.py b/typhos/tests/test_cli.py index d184fe72..3fd9687b 100644 --- a/typhos/tests/test_cli.py +++ b/typhos/tests/test_cli.py @@ -10,35 +10,34 @@ def test_cli_version(capsys): - typhos_cli(['--version']) + typhos_cli(["--version"]) readout = capsys.readouterr() assert str(typhos.__version__) in readout.out def test_cli_happi_cfg(qtbot, happi_cfg): - window = typhos_cli(['test_motor', '--happi-cfg', happi_cfg]) + window = typhos_cli(["test_motor", "--happi-cfg", happi_cfg]) qtbot.addWidget(window) - assert 'test_motor' == window.centralWidget().devices[0].name + assert "test_motor" == window.centralWidget().devices[0].name def test_cli_bad_entry(qtbot, happi_cfg): - window = typhos_cli(['no_motor', '--happi-cfg', happi_cfg]) + window = typhos_cli(["no_motor", "--happi-cfg", happi_cfg]) assert window is None def test_cli_no_entry(qtbot, happi_cfg): - window = typhos_cli(['--happi-cfg', happi_cfg]) + window = typhos_cli(["--happi-cfg", happi_cfg]) qtbot.addWidget(window) assert window.centralWidget().devices == [] def test_cli_stylesheet(qapp, qtbot, happi_cfg): - with open('test.qss', 'w+') as handle: + with open("test.qss", "w+") as handle: handle.write("QLabel {color: red}") try: style = qapp.styleSheet() - window = typhos_cli(['test_motor', '--stylesheet', 'test.qss', - '--happi-cfg', happi_cfg]) + window = typhos_cli(["test_motor", "--stylesheet", "test.qss", "--happi-cfg", happi_cfg]) qtbot.addWidget(window) suite = window.centralWidget() qtbot.addWidget(suite) @@ -48,13 +47,12 @@ def test_cli_stylesheet(qapp, qtbot, happi_cfg): assert color.red() == 255 finally: qapp.setStyleSheet(style) - os.remove('test.qss') + os.remove("test.qss") -@pytest.mark.parametrize('klass, name', [ - ("ophyd.sim.SynAxis[]", "SynAxis"), - ("ophyd.sim.SynAxis[{'name':'foo'}]", "foo") -]) +@pytest.mark.parametrize( + "klass, name", [("ophyd.sim.SynAxis[]", "SynAxis"), ("ophyd.sim.SynAxis[{'name':'foo'}]", "foo")] +) def test_cli_class(qtbot, klass, name, happi_cfg): window = typhos_cli([klass]) qtbot.addWidget(window) @@ -72,28 +70,25 @@ def test_cli_class_invalid(qtbot): def test_cli_profile_modules(capsys, qtbot): - window = typhos_cli(['ophyd.sim.SynAxis[]', '--profile-modules', - 'typhos.suite']) + window = typhos_cli(["ophyd.sim.SynAxis[]", "--profile-modules", "typhos.suite"]) qtbot.addWidget(window) output = capsys.readouterr() - assert 'add_device' in output.out + assert "add_device" in output.out def test_cli_benchmark(capsys, qtbot): - windows = typhos_cli(['ophyd.sim.SynAxis[]', '--benchmark', - 'flat_soft']) + windows = typhos_cli(["ophyd.sim.SynAxis[]", "--benchmark", "flat_soft"]) qtbot.addWidget(windows[0]) output = capsys.readouterr() - assert 'add_device' in output.out + assert "add_device" in output.out def test_cli_profile_output(capsys, qtbot): - path_obj = conftest.MODULE_PATH / 'artifacts' / 'prof' + path_obj = conftest.MODULE_PATH / "artifacts" / "prof" if not path_obj.parent.exists(): path_obj.parent.mkdir(parents=True) - window = typhos_cli(['ophyd.sim.SynAxis[]', '--profile-output', - str(path_obj)]) + window = typhos_cli(["ophyd.sim.SynAxis[]", "--profile-output", str(path_obj)]) qtbot.addWidget(window) output = capsys.readouterr() - assert 'add_device' not in output.out + assert "add_device" not in output.out assert path_obj.exists() diff --git a/typhos/tests/test_display.py b/typhos/tests/test_display.py index 3baba6c3..316b08d9 100644 --- a/typhos/tests/test_display.py +++ b/typhos/tests/test_display.py @@ -14,7 +14,7 @@ from .conftest import show_widget -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def display(request, qtbot): display = typhos.display.TyphosDeviceDisplay() display.setObjectName(display.objectName() + request.node.nodeid) @@ -22,7 +22,7 @@ def display(request, qtbot): yield display -@pytest.fixture(scope='function', params=[False, True]) +@pytest.fixture(scope="function", params=[False, True]) def show_switcher(request): return request.param @@ -47,26 +47,23 @@ def filter_by(sig): return False return sig.item.kind in kinds - return { - sig.name for sig in - utils.get_all_signals_from_device(device, filter_by=filter_by) - } + return {sig.name for sig in utils.get_all_signals_from_device(device, filter_by=filter_by)} def check_hint_panel(device): device_signals = signals_from_device(device, ophyd.Kind.hinted) - if 'motor_setpoint' in device_signals: + if "motor_setpoint" in device_signals: # Signal is renamed and not reflected here - device_signals.remove('motor_setpoint') - device_signals.add('motor') - assert device_signals == signals_from_panel('hint_panel') + device_signals.remove("motor_setpoint") + device_signals.add("motor") + assert device_signals == signals_from_panel("hint_panel") def check_read_panel(device): device_signals = signals_from_device(device, ophyd.Kind.normal) - assert device_signals == signals_from_panel('normal_panel') + assert device_signals == signals_from_panel("normal_panel") def check_config_panel(device): device_signals = signals_from_device(device, ophyd.Kind.config) - assert device_signals == signals_from_panel('config_panel') + assert device_signals == signals_from_panel("config_panel") print("Creating signal panel") panel = typhos.display.TyphosDeviceDisplay.from_device(motor) @@ -77,7 +74,7 @@ def check_config_panel(device): check_read_panel(motor) check_config_panel(motor) - device.name = 'test' + device.name = "test" print("Adding a new device") panel.add_device(device) check_read_panel(device) @@ -90,30 +87,29 @@ def test_display_without_md(motor, display): # Add a generic motor display.add_device(motor) assert display.devices[0] == motor - assert display.current_template == display.templates['detailed_screen'][0] + assert display.current_template == display.templates["detailed_screen"][0] def test_display_with_md(motor, display): - screen = 'engineering_screen.ui' + screen = "engineering_screen.ui" display.display_type = DisplayTypes.detailed_screen - display.add_device( - motor, macros={'detailed_screen': screen}) + display.add_device(motor, macros={"detailed_screen": screen}) display.load_best_template() assert display.current_template.name == screen - assert display.templates['detailed_screen'][0].name == screen + assert display.templates["detailed_screen"][0].name == screen def test_display_type_change(motor, display): # Changing template type changes template display.add_device(motor) display.display_type = display.embedded_screen - assert display.current_template == display.templates['embedded_screen'][0] + assert display.current_template == display.templates["embedded_screen"][0] def test_display_modified_templates(display, motor): display.add_device(motor) - eng_ui = display.templates['engineering_screen'] - display.templates['embedded_screen'] = eng_ui + eng_ui = display.templates["detailed_screen"] + display.templates["embedded_screen"] = eng_ui display.display_type = display.embedded_screen assert display.current_template == eng_ui[0] @@ -121,7 +117,7 @@ def test_display_modified_templates(display, motor): def test_display_force_template(display, motor): # Check that we use the forced template display.add_device(motor) - to_force = display.templates['engineering_screen'][0] + to_force = display.templates["engineering_screen"][0] display.force_template = to_force # Top-level screens always get detailed tree if nothing else is available assert display.force_template.name == to_force.name @@ -131,8 +127,8 @@ def test_display_force_template(display, motor): def test_display_with_channel(client, qtbot): panel = typhos.display.TyphosDeviceDisplay() qtbot.addWidget(panel) - panel.channel = 'happi://test_motor' - assert panel.channel == 'happi://test_motor' + panel.channel = "happi://test_motor" + assert panel.channel == "happi://test_motor" def device_added(): assert len(panel.devices) == 1 @@ -142,31 +138,31 @@ def device_added(): def test_display_device_class_property(motor, display, qtbot): qtbot.add_widget(display) - assert display.device_class == '' + assert display.device_class == "" display.add_device(motor) - assert display.device_class == 'ophyd.sim.SynAxis' + assert display.device_class == "ophyd.sim.SynAxis" def test_display_device_name_property(motor, display, qtbot): qtbot.add_widget(display) - assert display.device_name == '' + assert display.device_name == "" display.add_device(motor) assert display.device_name == motor.name def test_display_with_py_file(display, motor, qtbot): qtbot.add_widget(display) - py_file = str(conftest.MODULE_PATH / 'utils' / 'display.py') + py_file = str(conftest.MODULE_PATH / "utils" / "display.py") display.display_type = DisplayTypes.detailed_screen - display.add_device(motor, macros={'detailed_screen': py_file}) + display.add_device(motor, macros={"detailed_screen": py_file}) display.load_best_template() assert isinstance(display.display_widget, Display) - assert getattr(display.display_widget, 'is_from_test_file', False) + assert getattr(display.display_widget, "is_from_test_file", False) def test_display_with_sig_template(display, device, qapp, qtbot): qtbot.add_widget(display) - display.force_template = str(conftest.MODULE_PATH / 'utils' / 'sig.ui') + display.force_template = str(conftest.MODULE_PATH / "utils" / "sig.ui") display.add_device(device) qapp.processEvents() for num in range(10): @@ -187,7 +183,7 @@ def test_display_with_sig_template(display, device, qapp, qtbot): (Path("user/module/Potato.embedded.ui"), DisplayTypes.embedded_screen), (Path("user/module/Potato.detailed.ui"), DisplayTypes.detailed_screen), (Path("user/module/Potato.engineering.ui"), DisplayTypes.engineering_screen), - ] + ], ) def test_get_template_display_type_good(path: Path, expected: DisplayTypes): assert get_template_display_type(path) == expected @@ -198,7 +194,7 @@ def test_get_template_display_type_good(path: Path, expected: DisplayTypes): [ (Path("user/module/enigma.ui"), ValueError), (Path("user/module/not_very_detailed.ui"), ValueError), - ] + ], ) def test_get_template_display_type_bad(path: Path, expected: type[Exception]): with pytest.raises(expected): @@ -208,11 +204,11 @@ def test_get_template_display_type_bad(path: Path, expected: type[Exception]): def test_display_effective_display_type(display, device, qapp, qtbot): qtbot.add_widget(display) assert display.effective_display_type == display.display_type - display.force_template = str(conftest.MODULE_PATH / 'utils' / 'sig.ui') + display.force_template = str(conftest.MODULE_PATH / "utils" / "sig.ui") assert display.effective_display_type == display.display_type - display.force_template = str(conftest.MODULE_PATH.parent / 'ui' / 'core' / 'embedded_screen.ui') + display.force_template = str(conftest.MODULE_PATH.parent / "ui" / "core" / "embedded_screen.ui") assert display.effective_display_type == DisplayTypes.embedded_screen - display.force_template = str(conftest.MODULE_PATH.parent / 'ui' / 'core' / 'detailed_tree.ui') + display.force_template = str(conftest.MODULE_PATH.parent / "ui" / "core" / "detailed_tree.ui") assert display.effective_display_type == DisplayTypes.detailed_screen - display.force_template = str(conftest.MODULE_PATH.parent / 'ui' / 'core' / 'engineering_screen.ui') + display.force_template = str(conftest.MODULE_PATH.parent / "ui" / "core" / "engineering_screen.ui") assert display.effective_display_type == DisplayTypes.engineering_screen diff --git a/typhos/tests/test_dynamic_font.py b/typhos/tests/test_dynamic_font.py index c4c253f8..5fb9603a 100644 --- a/typhos/tests/test_dynamic_font.py +++ b/typhos/tests/test_dynamic_font.py @@ -9,8 +9,7 @@ from typhos.tests import conftest -from ..dynamic_font import (is_patched, patch_style_font_size, patch_widget, - unpatch_style_font_size, unpatch_widget) +from ..dynamic_font import is_patched, patch_style_font_size, patch_widget, unpatch_style_font_size, unpatch_widget logger = logging.getLogger(__name__) @@ -22,7 +21,7 @@ QtWidgets.QPushButton, QtWidgets.QComboBox, PyDMLabel, - ] + ], ) def test_patching( request: pytest.FixtureRequest, @@ -62,7 +61,7 @@ def test_patching( # Font size case 2: text is updated (not supported in combobox yet) if not isinstance(widget, QtWidgets.QComboBox): - widget.setText(widget.text()*100) + widget.setText(widget.text() * 100) new_text_font_size = widget.font().pointSizeF() logger.debug(f"setText patched font size is {new_text_font_size}") assert resized_font_size != new_text_font_size @@ -84,7 +83,7 @@ def test_patching( "background: red", "QLabel { background: blue }", "QWidget { font-size: 12 pt }", - ] + ], ) def test_style_patching(stylesheet: str, qtbot: pytestqt.qtbot.QtBot): widget = QtWidgets.QWidget() diff --git a/typhos/tests/test_func.py b/typhos/tests/test_func.py index 99de82dc..a89c729a 100644 --- a/typhos/tests/test_func.py +++ b/typhos/tests/test_func.py @@ -13,21 +13,19 @@ kwargs = dict() -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def func_display(qtbot): # Create mock function def foo(first, second: float = 3.14, hide: bool = True, third=False): - kwargs.update({"first": first, "second": second, - "hide": hide, "third": third}) + kwargs.update({"first": first, "second": second, "hide": hide, "third": third}) + # Create display - func_dis = FunctionDisplay(foo, annotations={'first': int}, - hide_params=['hide']) + func_dis = FunctionDisplay(foo, annotations={"first": int}, hide_params=["hide"]) qtbot.addWidget(func_dis) return func_dis class MyDevice(Device): - def __init__(self, *args, **kwargs): self.mock = Mock() super().__init__(*args, **kwargs) @@ -45,12 +43,12 @@ def sleep_and_finish(): return status -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def method_button(qtbot): - dev = MyDevice(name='test') + dev = MyDevice(name="test") button = TyphosMethodButton.from_device(dev) qtbot.addWidget(button) - button.method_name = 'my_method' + button.method_name = "my_method" return button @@ -59,26 +57,24 @@ def test_func_display_creation(func_display, qtbot): # Check we made the proper number of control widgets assert len(func_display.param_controls) == 3 # Check our hidden parameter is not available - assert 'hide' not in [widget.parameter - for widget in func_display.param_controls] + assert "hide" not in [widget.parameter for widget in func_display.param_controls] # Check that we sorted our parameters correctly - assert 'first' in func_display.required_params - assert all([key in func_display.optional_params - for key in ['second', 'third']]) + assert "first" in func_display.required_params + assert all([key in func_display.optional_params for key in ["second", "third"]]) return func_display def test_func_execution(func_display): # Configure parameters - func_display.param_controls[0].param_edit.setText('1') - func_display.param_controls[1].param_edit.setText('3.14159') + func_display.param_controls[0].param_edit.setText("1") + func_display.param_controls[1].param_edit.setText("3.14159") func_display.param_controls[2].param_control.setChecked(True) # Check function execution func_display.execute() - assert kwargs['first'] == 1 - assert kwargs['second'] == 3.14159 - assert kwargs['hide'] - assert kwargs['third'] + assert kwargs["first"] == 1 + assert kwargs["second"] == 3.14159 + assert kwargs["hide"] + assert kwargs["third"] def test_func_exceptions(func_display): @@ -86,8 +82,8 @@ def test_func_exceptions(func_display): kwargs.clear() # Configure parameters # Improper typing - func_display.param_controls[0].param_edit.setText('Invalid') - func_display.param_controls[1].param_edit.setText('3.14159') + func_display.param_controls[0].param_edit.setText("Invalid") + func_display.param_controls[1].param_edit.setText("3.14159") func_display.param_controls[2].param_control.setChecked(True) # Check function execution func_display.execute() @@ -103,17 +99,18 @@ def foo(a: int, b: bool = False, c: bool = True): def foobar(a: float, b: str, c: float = 3.14, d: bool = False): pass + # Create Panel fp = FunctionPanel([foo, foobar]) qtbot.addWidget(fp) # Check that all our methods made it in - assert 'foo' in fp.methods - assert 'foobar' in fp.methods + assert "foo" in fp.methods + assert "foobar" in fp.methods return fp def test_method_button_execute(method_button): - assert method_button.method_name == 'my_method' + assert method_button.method_name == "my_method" method_button.execute() dev = method_button.devices[0] assert dev.mock.called @@ -132,7 +129,7 @@ def test_method_button_use_status(qtbot, method_button): def test_func_docstrings(qtbot): # Mock functions def foo(a: int, b: bool = False, c: bool = True): - ''' + """ The function foo Parameters @@ -150,22 +147,22 @@ def foo(a: int, b: bool = False, c: bool = True): ----- Note #1 Note #2 - ''' + """ pass def foobar(a: float, b: str, c: float = 3.14, d: bool = False): - 'docstring2' + "docstring2" pass # Create Panel fp = FunctionPanel([foo, foobar]) qtbot.addWidget(fp) # Check that all our methods made it in - assert fp.methods['foo'].docs['summary'] == 'The function foo' - params = fp.methods['foo'].docs['params'] - assert params['a'] == ['A special A'] - assert params['b'] == ['The infamous B parameter (default: False)'] - assert params['c'] == ['The ill-named C parameter (default: True)'] + assert fp.methods["foo"].docs["summary"] == "The function foo" + params = fp.methods["foo"].docs["params"] + assert params["a"] == ["A special A"] + assert params["b"] == ["The infamous B parameter (default: False)"] + assert params["c"] == ["The ill-named C parameter (default: True)"] - assert fp.methods['foobar'].docs['summary'] == 'docstring2' + assert fp.methods["foobar"].docs["summary"] == "docstring2" return fp diff --git a/typhos/tests/test_log.py b/typhos/tests/test_log.py index d12a278e..2c18ba10 100644 --- a/typhos/tests/test_log.py +++ b/typhos/tests/test_log.py @@ -5,18 +5,18 @@ def get_handlers(dev): - if hasattr(dev.log, 'handlers'): + if hasattr(dev.log, "handlers"): return dev.log.handlers return dev.log.logger.handlers def test_log_display(qtbot): - dev = Device(name='test') + dev = Device(name="test") log_tool = TyphosLogDisplay.from_device(dev) qtbot.addWidget(log_tool) dev.log.error(dev.name) assert log_tool.logdisplay.handler in get_handlers(dev) - dev2 = Device(name='blah') + dev2 = Device(name="blah") log_tool.add_device(dev2) assert log_tool.logdisplay.handler in get_handlers(dev2) for device in (dev, dev2): diff --git a/typhos/tests/test_notes.py b/typhos/tests/test_notes.py index 5da958ef..cd40a135 100644 --- a/typhos/tests/test_notes.py +++ b/typhos/tests/test_notes.py @@ -12,27 +12,26 @@ from .conftest import MODULE_PATH -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def user_notes_path(monkeypatch, tmp_path: Path): # copy user_device_notes.yaml to a temp file # monkeypatch platformdirs to look for device_notes.yaml # provide the new path for confirmation - user_path = tmp_path / 'device_notes.yaml' - notes_path = MODULE_PATH / 'utils' / 'user_device_notes.yaml' + user_path = tmp_path / "device_notes.yaml" + notes_path = MODULE_PATH / "utils" / "user_device_notes.yaml" shutil.copy(notes_path, user_path) - monkeypatch.setattr(platformdirs, 'user_data_path', - lambda: tmp_path) + monkeypatch.setattr(platformdirs, "user_data_path", lambda: tmp_path) yield user_path -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def env_notes_path(tmp_path: Path): # copy user env var device_notes.yaml to a temp file # add env var pointing to env device notes # provide the path for confirmation - env_path = tmp_path / 'env_device_notes.yaml' - notes_path = MODULE_PATH / 'utils' / 'env_device_notes.yaml' + env_path = tmp_path / "env_device_notes.yaml" + notes_path = MODULE_PATH / "utils" / "env_device_notes.yaml" shutil.copy(notes_path, env_path) os.environ[NOTES_VAR] = str(env_path) @@ -42,17 +41,16 @@ def env_notes_path(tmp_path: Path): def test_new_note(qtbot: QtBot, tmp_path: Path, monkeypatch): - monkeypatch.setattr(platformdirs, 'user_data_path', - lambda: tmp_path) + monkeypatch.setattr(platformdirs, "user_data_path", lambda: tmp_path) - user_path = tmp_path / 'device_notes.yaml' + user_path = tmp_path / "device_notes.yaml" assert not user_path.exists() notes_edit = TyphosNotesEdit() qtbot.addWidget(notes_edit) - notes_edit.setup_data('Syn:Motor') + notes_edit.setup_data("Syn:Motor") - notes_edit.setText('hello new text') + notes_edit.setText("hello new text") notes_edit.save_note() assert user_path.exists() @@ -62,55 +60,55 @@ def test_note_shadowing(qtbot: QtBot, user_notes_path: Path, env_notes_path: Pat # user data shadows all other sources notes_edit = TyphosNotesEdit() qtbot.addWidget(notes_edit) - notes_edit.setup_data('Syn:Motor') - assert 'user' in notes_edit.text() + notes_edit.setup_data("Syn:Motor") + assert "user" in notes_edit.text() # no data in user, so fall back to data specified in env var accel_edit = TyphosNotesEdit() qtbot.addWidget(accel_edit) - accel_edit.setup_data('Syn:Motor_acceleration') - assert 'env' in accel_edit.text() + accel_edit.setup_data("Syn:Motor_acceleration") + assert "env" in accel_edit.text() def test_env_note(qtbot: QtBot, env_notes_path: Path): # grab only data in env notes notes_edit = TyphosNotesEdit() qtbot.addWidget(notes_edit) - notes_edit.setup_data('Syn:Motor') - assert 'user' not in notes_edit.text() - assert 'env' in notes_edit.text() + notes_edit.setup_data("Syn:Motor") + assert "user" not in notes_edit.text() + assert "env" in notes_edit.text() accel_edit = TyphosNotesEdit() qtbot.addWidget(accel_edit) - accel_edit.setup_data('Syn:Motor_acceleration') - assert 'env' in accel_edit.text() + accel_edit.setup_data("Syn:Motor_acceleration") + assert "env" in accel_edit.text() def test_user_note(qtbot: QtBot, user_notes_path: Path): # user data shadows all other sources notes_edit = TyphosNotesEdit() qtbot.addWidget(notes_edit) - notes_edit.setup_data('Syn:Motor') - assert 'user' in notes_edit.text() - assert 'env' not in notes_edit.text() + notes_edit.setup_data("Syn:Motor") + assert "user" in notes_edit.text() + assert "env" not in notes_edit.text() # no data in user, and nothing to fallback to accel_edit = TyphosNotesEdit() qtbot.addWidget(accel_edit) - accel_edit.setup_data('Syn:Motor_acceleration') - assert '' == accel_edit.text() + accel_edit.setup_data("Syn:Motor_acceleration") + assert "" == accel_edit.text() def test_note_edit(qtbot: QtBot, user_notes_path: Path): notes_edit = TyphosNotesEdit() qtbot.addWidget(notes_edit) - notes_edit.setup_data('Syn:Motor') + notes_edit.setup_data("Syn:Motor") - assert 'user' in notes_edit.text() + assert "user" in notes_edit.text() - notes_edit.setText('new user note') + notes_edit.setText("new user note") notes_edit.editingFinished.emit() with open(user_notes_path) as f: user_notes = yaml.full_load(f) - assert 'new user note' == user_notes['Syn:Motor']['note'] + assert "new user note" == user_notes["Syn:Motor"]["note"] diff --git a/typhos/tests/test_panel.py b/typhos/tests/test_panel.py index d4f7b2ef..63fd0507 100644 --- a/typhos/tests/test_panel.py +++ b/typhos/tests/test_panel.py @@ -4,20 +4,18 @@ import pydm.utilities import pytest from ophyd.signal import Signal -from ophyd.sim import (FakeEpicsSignal, FakeEpicsSignalRO, SynSignal, - SynSignalRO) +from ophyd.sim import FakeEpicsSignal, FakeEpicsSignalRO, SynSignal, SynSignalRO from pydm.widgets import PyDMEnumComboBox from qtpy.QtWidgets import QWidget from typhos import cache, utils from typhos.panel import SignalPanel, TyphosSignalPanel -from typhos.widgets import (ImageDialogButton, WaveformDialogButton, - create_signal_widget) +from typhos.widgets import ImageDialogButton, WaveformDialogButton, create_signal_widget from .conftest import DeadSignal, RichSignal, show_widget -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def type_cache(): type_cache = cache.get_global_widget_type_cache() type_cache.clear() @@ -25,20 +23,20 @@ def type_cache(): return type_cache -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def panel(qtbot, type_cache, monkeypatch): panel = SignalPanel() yield panel -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def typhos_signal_panel(qtbot, monkeypatch, type_cache): typhos_panel = TyphosSignalPanel() qtbot.addWidget(typhos_panel) yield typhos_panel -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def panel_widget(qtbot, panel): widget = QWidget() qtbot.addWidget(widget) @@ -50,25 +48,23 @@ def wait_panel(qtbot, panel, signal_names): def condition(loaded_signals): return set(loaded_signals) == signal_names - blocker = qtbot.wait_signal(panel.loading_complete, - check_params_cb=condition) + blocker = qtbot.wait_signal(panel.loading_complete, check_params_cb=condition) blocker.wait() print() - print('Panel loaded all required signals.', signal_names) - print('Panel layout:') + print("Panel loaded all required signals.", signal_names) + print("Panel layout:") print(utils.dump_grid_layout(panel)) print() @show_widget def test_panel_creation(qtbot, panel, panel_widget): - standard = FakeEpicsSignal('Tst:Pv', name='standard') - read_and_write = FakeEpicsSignal('Tst:Read', write_pv='Tst:Write', - name='read_and_write') - read_only = FakeEpicsSignalRO('Tst:Pv:RO', name='read_only') - simulated = SynSignal(func=random.random, name='simul') - simulated_ro = SynSignalRO(func=random.random, name='simul_ro') + standard = FakeEpicsSignal("Tst:Pv", name="standard") + read_and_write = FakeEpicsSignal("Tst:Read", write_pv="Tst:Write", name="read_and_write") + read_only = FakeEpicsSignalRO("Tst:Pv:RO", name="read_only") + simulated = SynSignal(func=random.random, name="simul") + simulated_ro = SynSignalRO(func=random.random, name="simul_ro") standard.sim_put(1) read_and_write.sim_put(2) @@ -77,22 +73,19 @@ def test_panel_creation(qtbot, panel, panel_widget): signals = { # Signal is its own write - 'Standard': standard, + "Standard": standard, # Signal has separate write/read - 'Read and Write': read_and_write, - 'Read Only': read_only, - 'Simulated': simulated, - 'SimulatedRO': simulated_ro, - 'Array': Signal(name='array', value=np.ones((5, 10))) + "Read and Write": read_and_write, + "Read Only": read_only, + "Simulated": simulated, + "SimulatedRO": simulated_ro, + "Array": Signal(name="array", value=np.ones((5, 10))), } for name, signal in signals.items(): panel.add_signal(signal, name=name) - wait_panel( - qtbot, panel, - signal_names={sig.name for sig in signals.values()} - ) + wait_panel(qtbot, panel, signal_names={sig.name for sig in signals.values()}) def widget_at(row, col): return panel.itemAtPosition(row, col).widget() @@ -113,14 +106,14 @@ def widget_at(row, col): def test_panel_add_enum(qtbot, panel, panel_widget): # Create an enum signal - syn_sig = RichSignal(name='Syn:Enum', value=1) + syn_sig = RichSignal(name="Syn:Enum", value=1) row = panel.add_signal(syn_sig, "Sim Enum PV") # Check our signal was added a QCombobox wait_panel(qtbot, panel, signal_names={syn_sig.name}) def widget_at(row, col): - print('** widget_at ** panel is', panel, panel.signals) + print("** widget_at ** panel is", panel, panel.signals) return panel.itemAtPosition(row, col).widget() assert isinstance(widget_at(row, 2), PyDMEnumComboBox) @@ -128,16 +121,15 @@ def widget_at(row, col): def test_add_dead_signal(qtbot, panel, panel_widget): - dead_sig = DeadSignal(name='ded', value=0) - panel.add_signal(dead_sig, 'Dead Signal') + dead_sig = DeadSignal(name="ded", value=0) + panel.add_signal(dead_sig, "Dead Signal") assert dead_sig.name in panel.signals -@pytest.mark.xfail( - reason='PVs do not exist so widgets are not created post refactor') +@pytest.mark.xfail(reason="PVs do not exist so widgets are not created post refactor") def test_add_pv(qtbot, panel, panel_widget): - row = panel.add_pv('Tst:A', 'Read Only') - assert 'Read Only' in panel.signals + row = panel.add_pv("Tst:A", "Read Only") + assert "Read Only" in panel.signals def widget_at(row, col): return panel.itemAtPosition(row, col).widget() @@ -145,7 +137,7 @@ def widget_at(row, col): # Check read-only spans setpoint/readback cols assert widget_at(row, 1) is widget_at(row, 2) - row = panel.add_pv('Tst:A', "Write", write_pv='Tst:B') + row = panel.add_pv("Tst:A", "Write", write_pv="Tst:B") # Since it is not connected, it should show just the loading widget assert widget_at(row, 1) is widget_at(row, 2) @@ -158,10 +150,10 @@ def test_typhos_panel(qapp, client, qtbot, typhos_signal_panel): panel.showConfig = False panel.showConfig = True # Add a device channel - panel.channel = 'happi://test_device' - assert panel.channel == 'happi://test_device' + panel.channel = "happi://test_device" + assert panel.channel == "happi://test_device" # Reset channel and no smoke comes out - panel.channel = 'happi://test_motor' + panel.channel = "happi://test_motor" pydm.utilities.establish_widget_connections(panel) def have_device(): @@ -170,7 +162,7 @@ def have_device(): qtbot.wait_until(have_device) device = panel.devices[0] - num_hints = len(device.hints['fields']) + num_hints = len(device.hints["fields"]) num_read = len(device.read_attrs) def get_visible_signals(): @@ -198,7 +190,7 @@ def get_visible_signals(): def test_typhos_panel_sort_by_name(qapp, client, qtbot, typhos_signal_panel): panel = typhos_signal_panel panel.sortBy = panel.SignalOrder.byName - panel.channel = 'happi://test_motor' + panel.channel = "happi://test_motor" pydm.utilities.establish_widget_connections(panel) def have_device(): @@ -206,10 +198,7 @@ def have_device(): qtbot.wait_until(have_device) device = panel.devices[0] - sorted_names = [ - getattr(device, attr).name - for attr in sorted(device.component_names) - ] + sorted_names = [getattr(device, attr).name for attr in sorted(device.component_names)] assert list(panel.layout().signals.keys()) == sorted_names return typhos_signal_panel @@ -221,7 +210,7 @@ def test_typhos_panel_sort_by_kind(qapp, client, qtbot, typhos_signal_panel): panel = typhos_signal_panel panel.sortBy = panel.SignalOrder.byKind - panel.channel = 'happi://test_motor' + panel.channel = "happi://test_motor" pydm.utilities.establish_widget_connections(panel) def have_device(): @@ -229,14 +218,14 @@ def have_device(): qtbot.wait_until(have_device) key_order = list(panel.layout().signals.keys()) - assert key_order[0] == 'test_motor' - assert key_order[-1] == 'test_motor_unused' + assert key_order[0] == "test_motor" + assert key_order[-1] == "test_motor_unused" return panel @show_widget def test_signal_widget_waveform(qtbot): - signal = Signal(name='test_wave', value=np.zeros((4, ))) + signal = Signal(name="test_wave", value=np.zeros((4,))) widget = create_signal_widget(signal) qtbot.addWidget(widget) assert isinstance(widget, WaveformDialogButton) @@ -245,7 +234,7 @@ def test_signal_widget_waveform(qtbot): @show_widget def test_signal_widget_image(qtbot): - signal = Signal(name='test_img', value=np.zeros((400, 540))) + signal = Signal(name="test_img", value=np.zeros((400, 540))) widget = create_signal_widget(signal) qtbot.addWidget(widget) assert isinstance(widget, ImageDialogButton) diff --git a/typhos/tests/test_plot.py b/typhos/tests/test_plot.py index 49b2ddda..f16f2994 100644 --- a/typhos/tests/test_plot.py +++ b/typhos/tests/test_plot.py @@ -1,6 +1,7 @@ """ Tests for the plot tool. """ + import pytest from ophyd import EpicsSignal, Signal @@ -11,9 +12,9 @@ from .conftest import pydm_version_xfail -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def sim_signal(): - sim_sig = Signal(name='tst_this_2') + sim_sig = Signal(name="tst_this_2") sim_sig.put(3.14) register_signal(sim_sig) return sim_sig @@ -21,30 +22,30 @@ def sim_signal(): def test_add_signal(qtbot, sim_signal): # Create Signals - epics_sig = EpicsSignal('Tst:This') + epics_sig = EpicsSignal("Tst:This") # Create empty plot ttp = TyphosTimePlot() qtbot.addWidget(ttp) # Add to list of available signals - ttp.add_available_signal(epics_sig, 'Epics Signal') - assert ttp.signal_combo.itemText(0) == 'Epics Signal' - assert ttp.signal_combo.itemData(0) == 'ca://Tst:This' - ttp.add_available_signal(sim_signal, 'Simulated Signal') - assert ttp.signal_combo.itemText(1) == 'Simulated Signal' - assert ttp.signal_combo.itemData(1) == 'sig://tst_this_2' + ttp.add_available_signal(epics_sig, "Epics Signal") + assert ttp.signal_combo.itemText(0) == "Epics Signal" + assert ttp.signal_combo.itemData(0) == "ca://Tst:This" + ttp.add_available_signal(sim_signal, "Simulated Signal") + assert ttp.signal_combo.itemText(1) == "Simulated Signal" + assert ttp.signal_combo.itemData(1) == "sig://tst_this_2" @pydm_version_xfail def test_curve_methods(qtbot, sim_signal): ttp = TyphosTimePlot() qtbot.addWidget(ttp) - ttp.add_curve('sig://' + sim_signal.name, name=sim_signal.name) + ttp.add_curve("sig://" + sim_signal.name, name=sim_signal.name) # Check that our signal is stored in the mapping - assert 'sig://' + sim_signal.name in ttp.channel_to_curve + assert "sig://" + sim_signal.name in ttp.channel_to_curve # Check that our curve is live assert len(ttp.timechart.chart.curves) == 1 # Try and add again - ttp.add_curve('sig://' + sim_signal.name, name=sim_signal.name) + ttp.add_curve("sig://" + sim_signal.name, name=sim_signal.name) # Check we didn't duplicate assert len(ttp.timechart.chart.curves) == 1 ttp.remove_curve(channel_from_signal(sim_signal)) @@ -55,7 +56,7 @@ def test_curve_methods(qtbot, sim_signal): def test_curve_creation_button(qtbot, sim_signal): ttp = TyphosTimePlot() qtbot.addWidget(ttp) - ttp.add_available_signal(sim_signal, 'Sim Signal') + ttp.add_available_signal(sim_signal, "Sim Signal") ttp.creation_requested() # Check that our signal is stored in the mapping assert channel_from_signal(sim_signal) in ttp.channel_to_curve diff --git a/typhos/tests/test_positioner.py b/typhos/tests/test_positioner.py index 933a3a09..c918ea5f 100644 --- a/typhos/tests/test_positioner.py +++ b/typhos/tests/test_positioner.py @@ -8,6 +8,7 @@ Any test that uses a positioner widget needs to have this mark. This is not yet fully understood. """ + from unittest.mock import Mock import pytest @@ -31,8 +32,7 @@ class SimMotor(SynAxis): high_limit_switch = Cpt(SignalRO, value=0) low_limit = Cpt(Signal, value=-10) high_limit = Cpt(Signal, value=10) - motor_is_moving = Cpt(RichSignal, value=0, - metadata={'enum_strs': ('not moving', 'moving')}) + motor_is_moving = Cpt(RichSignal, value=0, metadata={"enum_strs": ("not moving", "moving")}) stop = Mock() clear_error = Mock() @@ -47,14 +47,14 @@ def set(self, value, timeout=None): def check_value(self, pos: float): if not self.low_limit.get() <= pos <= self.high_limit.get(): - raise LimitError('Sim limits error') + raise LimitError("Sim limits error") -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def motor_widget(qtbot): - motor = SimMotor(name='test') + motor = SimMotor(name="test") widget = TyphosPositionerWidget() - widget.readback_attribute = 'readback' + widget.readback_attribute = "readback" widget.add_device(motor) qtbot.addWidget(widget) yield motor, widget @@ -66,8 +66,7 @@ def motor_widget(qtbot): def test_positioner_widget_no_limits(qtbot, motor): setwidget = TyphosPositionerWidget.from_device(motor) qtbot.addWidget(setwidget) - for widget in ('low_limit', 'low_limit_switch', - 'high_limit', 'high_limit_switch'): + for widget in ("low_limit", "low_limit_switch", "high_limit", "high_limit_switch"): assert getattr(setwidget.ui, widget).isHidden() @@ -76,8 +75,8 @@ def test_positioner_widget_fixed_limits(qtbot, motor): motor.limits = (-10, 10) widget = TyphosPositionerWidget.from_device(motor) qtbot.addWidget(widget) - assert widget.ui.low_limit.text() == '-10' - assert widget.ui.high_limit.text() == '10' + assert widget.ui.low_limit.text() == "-10" + assert widget.ui.high_limit.text() == "10" @show_widget @@ -90,7 +89,7 @@ def test_positioner_widget_with_signal_limits(motor_widget): assert motor.low_limit_switch.name in low_limit_chan high_limit_chan = widget.ui.high_limit_switch.channel assert motor.high_limit_switch.name in high_limit_chan - motor.delay = 3. # Just for visual testing purposes + motor.delay = 3.0 # Just for visual testing purposes return widget @@ -115,14 +114,14 @@ class NoMoveSoftPos(SoftPositioner, Device): This must be a device for inclusion in the widget, as typhos calls "walk_signals". """ - def _setup_move(self, *args, **kwargs): - ... + + def _setup_move(self, *args, **kwargs): ... @pytest.mark.no_gc def test_positioner_widget_stop_no_error(motor_widget): _, widget = motor_widget - motor = NoMoveSoftPos(name='motor') + motor = NoMoveSoftPos(name="motor") widget.add_device(motor) # Calling stop on the motor directly is an error status status = motor.move(1, wait=False) @@ -141,7 +140,7 @@ def test_positioner_widget_stop_no_error(motor_widget): def test_positioner_widget_set(motor_widget): motor, widget = motor_widget # Check motion - widget.ui.set_value.setText('4') + widget.ui.set_value.setText("4") widget.ui.set() assert motor.position == 4 @@ -149,18 +148,18 @@ def test_positioner_widget_set(motor_widget): @pytest.mark.no_gc def test_positioner_widget_positive_tweak(motor_widget): motor, widget = motor_widget - widget.ui.tweak_value.setText('1') + widget.ui.tweak_value.setText("1") widget.positive_tweak() - assert widget.ui.set_value.text() == '1.0' + assert widget.ui.set_value.text() == "1.0" assert motor.position == 1 @pytest.mark.no_gc def test_positioner_widget_negative_tweak(motor_widget): motor, widget = motor_widget - widget.ui.tweak_value.setText('1') + widget.ui.tweak_value.setText("1") widget.negative_tweak() - assert widget.ui.set_value.text() == '-1.0' + assert widget.ui.set_value.text() == "-1.0" assert motor.position == -1 @@ -168,8 +167,8 @@ def test_positioner_widget_negative_tweak(motor_widget): def test_positioner_widget_moving_property(motor_widget, qtbot): motor, widget = motor_widget assert not widget.moving - motor.delay = 1. - widget.ui.set_value.setText('7') + motor.delay = 1.0 + widget.ui.set_value.setText("7") widget.set() qtbot.waitUntil(lambda: widget.moving, timeout=500) qtbot.waitUntil(lambda: not widget.moving, timeout=1000) @@ -222,8 +221,8 @@ def update_alarm(level, connected=True): ): motor.motor_is_moving.update_metadata( { - 'severity': level, - 'connected': connected, + "severity": level, + "connected": connected, } ) @@ -267,11 +266,11 @@ def test_positioner_widget_move_error(motor_widget, qtbot): with pytest.raises(LimitError): motor.check_value(bad_position) - assert widget.ui.status_label.text() == '' + assert widget.ui.status_label.text() == "" widget._set(bad_position) def has_limit_error(): - assert 'LimitError' in widget.ui.status_label.text() + assert "LimitError" in widget.ui.status_label.text() qtbot.waitUntil(has_limit_error, timeout=1000) @@ -280,7 +279,7 @@ def has_limit_error(): def test_positioner_widget_long_status_text(motor_widget): _, widget = motor_widget - assert widget.ui.status_label.text() == '' + assert widget.ui.status_label.text() == "" widget._set_status_text("Oh no! " * 1000, max_length=50) @@ -289,7 +288,7 @@ def test_positioner_widget_long_status_text(motor_widget): assert len(text) < 60 widget._set_status_text("") - assert widget.ui.status_label.text() == '' + assert widget.ui.status_label.text() == "" widget._set_status_text( TyphosStatusMessage( diff --git a/typhos/tests/test_related_display.py b/typhos/tests/test_related_display.py index e07a7b78..8b99a0c5 100644 --- a/typhos/tests/test_related_display.py +++ b/typhos/tests/test_related_display.py @@ -13,7 +13,7 @@ logger = logging.getLogger(__name__) -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def suite_button(qtbot: pytestqt.qtbot.QtBot, happi_cfg) -> TyphosRelatedSuiteButton: button = TyphosRelatedSuiteButton() button.happi_cfg = happi_cfg @@ -23,24 +23,24 @@ def suite_button(qtbot: pytestqt.qtbot.QtBot, happi_cfg) -> TyphosRelatedSuiteBu class Dummy(Device): sig1 = Cpt(Signal, value=1) - sig2 = Cpt(Signal, value='two') + sig2 = Cpt(Signal, value="two") def test_create_suite_happi(qtbot: pytestqt.qtbot.QtBot, suite_button: TyphosRelatedSuiteButton): - logger.debug('Make sure we can load a suite using happi.') - happi_names = ['test_motor', 'test_device'] + logger.debug("Make sure we can load a suite using happi.") + happi_names = ["test_motor", "test_device"] suite_button.happi_names = happi_names suite = suite_button.create_suite() qtbot.addWidget(suite) # Does the suite have the appropriate subdisplays? for name in happi_names: - assert suite.get_subdisplay(name.replace('_', ' ')).device_name == name + assert suite.get_subdisplay(name.replace("_", " ")).device_name == name def test_create_suite_add_devices(qtbot: pytestqt.qtbot.QtBot, suite_button: TyphosRelatedSuiteButton): - logger.debug('Make sure we can load a suite using add_devices.') - dev1 = Dummy(name='dummy1') - dev2 = Dummy(name='dummy2') + logger.debug("Make sure we can load a suite using add_devices.") + dev1 = Dummy(name="dummy1") + dev2 = Dummy(name="dummy2") suite_button.add_device(dev1) suite_button.add_device(dev2) suite = suite_button.create_suite() @@ -51,8 +51,8 @@ def test_create_suite_add_devices(qtbot: pytestqt.qtbot.QtBot, suite_button: Typ def test_preload(qtbot: pytestqt.qtbot.QtBot, suite_button: TyphosRelatedSuiteButton): - logger.debug('Make sure preload preloads.') - dev1 = Dummy(name='dummy1') + logger.debug("Make sure preload preloads.") + dev1 = Dummy(name="dummy1") suite_button.add_device(dev1) # A _suite should be created after preload is set assert suite_button._suite is None @@ -63,8 +63,8 @@ def test_preload(qtbot: pytestqt.qtbot.QtBot, suite_button: TyphosRelatedSuiteBu @show_widget def test_show_suite(qtbot: pytestqt.qtbot.QtBot, suite_button: TyphosRelatedSuiteButton): - logger.debug('Make sure no exception is raised when we show a suite.') - dev1 = Dummy(name='dummy1') + logger.debug("Make sure no exception is raised when we show a suite.") + dev1 = Dummy(name="dummy1") suite_button.add_device(dev1) suite = suite_button.create_suite() qtbot.addWidget(suite) @@ -72,13 +72,13 @@ def test_show_suite(qtbot: pytestqt.qtbot.QtBot, suite_button: TyphosRelatedSuit def test_suite_errors(suite_button: TyphosRelatedSuiteButton): - logger.debug('Make sure we raise exceptions for bad inputs.') + logger.debug("Make sure we raise exceptions for bad inputs.") # No devices configured with pytest.raises(ValueError): suite_button.create_suite() # A device is misspelled - suite_button.happi_names = ['test_motor', 'asdfasefasdc', 'test_device'] + suite_button.happi_names = ["test_motor", "asdfasefasdc", "test_device"] with pytest.raises(ValueError): suite_button.get_happi_devices() diff --git a/typhos/tests/test_status.py b/typhos/tests/test_status.py index 47fa03ce..8537df66 100644 --- a/typhos/tests/test_status.py +++ b/typhos/tests/test_status.py @@ -2,8 +2,7 @@ import pytest from ophyd.status import Status -from ophyd.utils import (StatusTimeoutError, UnknownStatusFailure, - WaitTimeoutError) +from ophyd.utils import StatusTimeoutError, UnknownStatusFailure, WaitTimeoutError from qtpy.QtWidgets import QWidget from typhos.status import TyphosStatusResult, TyphosStatusThread @@ -21,20 +20,20 @@ def __init__(self): self.exc = Mock() -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def status(qtbot): status = Status() return status -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def listener(qtbot, status): listener = Listener() qtbot.addWidget(listener) return listener -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def thread(qtbot, status, listener): thread = TyphosStatusThread(status) thread.status_started.connect(listener.started) @@ -72,7 +71,7 @@ def test_status_thread_completed(qtbot, listener, status, thread): assert not listener.timeout.called assert not listener.err_msg.called assert not listener.exc.called - res, = listener.finished.call_args[0] + (res,) = listener.finished.call_args[0] assert res == TyphosStatusResult.success @@ -84,15 +83,15 @@ def test_status_thread_wait_timeout(qtbot, listener, thread, status): thread.start() qtbot.waitUntil(lambda: listener.started.called, timeout=2000) qtbot.waitUntil(lambda: listener.timeout.called, timeout=2000) - msg, = listener.err_msg.call_args[0] - exc, = listener.exc.call_args[0] + (msg,) = listener.err_msg.call_args[0] + (exc,) = listener.exc.call_args[0] assert "taking longer than expected" in msg.text assert isinstance(exc, WaitTimeoutError) assert not listener.finished.called # and now we should be able to finish status.set_finished() qtbot.waitUntil(lambda: listener.finished.called, timeout=2000) - res, = listener.finished.call_args[0] + (res,) = listener.finished.call_args[0] assert res == TyphosStatusResult.success @@ -107,9 +106,9 @@ def test_status_thread_status_timeout(qtbot, listener, thread): qtbot.waitUntil(lambda: listener.err_msg.called, timeout=2000) qtbot.waitUntil(lambda: listener.exc.called, timeout=2000) qtbot.waitUntil(lambda: listener.finished.called, timeout=2000) - res, = listener.finished.call_args[0] - msg, = listener.err_msg.call_args[0] - exc, = listener.exc.call_args[0] + (res,) = listener.finished.call_args[0] + (msg,) = listener.err_msg.call_args[0] + (exc,) = listener.exc.call_args[0] assert res == TyphosStatusResult.failure assert "failed with timeout" in msg.text assert isinstance(exc, StatusTimeoutError) @@ -126,9 +125,9 @@ def test_status_thread_unk_failure(qtbot, listener, status, thread): qtbot.waitUntil(lambda: listener.err_msg.called, timeout=2000) qtbot.waitUntil(lambda: listener.exc.called, timeout=2000) qtbot.waitUntil(lambda: listener.finished.called, timeout=2000) - res, = listener.finished.call_args[0] - msg, = listener.err_msg.call_args[0] - exc, = listener.exc.call_args[0] + (res,) = listener.finished.call_args[0] + (msg,) = listener.err_msg.call_args[0] + (exc,) = listener.exc.call_args[0] assert res == TyphosStatusResult.failure assert "failed with no reason" in msg.text assert isinstance(exc, UnknownStatusFailure) @@ -145,9 +144,9 @@ def test_status_thread_specific_failure(qtbot, listener, status, thread): qtbot.waitUntil(lambda: listener.err_msg.called, timeout=2000) qtbot.waitUntil(lambda: listener.exc.called, timeout=2000) qtbot.waitUntil(lambda: listener.finished.called, timeout=2000) - res, = listener.finished.call_args[0] - msg, = listener.err_msg.call_args[0] - exc, = listener.exc.call_args[0] + (res,) = listener.finished.call_args[0] + (msg,) = listener.err_msg.call_args[0] + (exc,) = listener.exc.call_args[0] assert res == TyphosStatusResult.failure assert "test_error" in msg.text assert not isinstance(exc, WaitTimeoutError) diff --git a/typhos/tests/test_suite.py b/typhos/tests/test_suite.py index 7d99f775..47634459 100644 --- a/typhos/tests/test_suite.py +++ b/typhos/tests/test_suite.py @@ -15,7 +15,7 @@ from .conftest import MockDevice, show_widget -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def suite( device: MockDevice, qtbot: pytestqt.qtbot.QtBot, @@ -28,7 +28,7 @@ def suite( @show_widget def test_suite_with_child_devices(suite: TyphosSuite, device: MockDevice): assert device in suite.devices - device_group = suite.top_level_groups['Devices'] + device_group = suite.top_level_groups["Devices"] assert len(device_group.childs) == 1 child_displays = device_group.childs[0].childs assert len(child_displays) == len(device._sub_devices) @@ -41,7 +41,7 @@ def test_suite_without_children( ): childless = TyphosSuite.from_device(device, children=False) qtbot.addWidget(childless) - device_group = childless.top_level_groups['Devices'] + device_group = childless.top_level_groups["Devices"] childless_displays = device_group.childs[0].childs assert len(childless_displays) == 0 @@ -73,17 +73,15 @@ def test_suite_get_subdisplay_by_name(suite: TyphosSuite, device: MockDevice): def test_suite_show_display_by_device(suite: TyphosSuite, device: MockDevice): suite.show_subdisplay(device.x) - dock = suite._content_frame.layout().itemAt( - suite.layout().count() - 1).widget() + dock = suite._content_frame.layout().itemAt(suite.layout().count() - 1).widget() assert isinstance(dock, QtWidgets.QDockWidget) assert device.x in dock.widget().devices def test_suite_show_display_by_parameter(suite): - device_param = suite.top_level_groups['Devices'].childs[0] + device_param = suite.top_level_groups["Devices"].childs[0] suite.show_subdisplay(device_param) - dock = suite._content_frame.layout().itemAt( - suite.layout().count() - 1).widget() + dock = suite._content_frame.layout().itemAt(suite.layout().count() - 1).widget() assert isinstance(dock, QtWidgets.QDockWidget) assert device_param.device in dock.widget().devices assert dock.receivers(dock.closing) == 1 @@ -105,7 +103,7 @@ def test_suite_hide_subdisplay_by_parameter( suite: TyphosSuite, qtbot: pytestqt.qtbot.QtBot, ): - device_param = suite.top_level_groups['Devices'].childs[0] + device_param = suite.top_level_groups["Devices"].childs[0] qtbot.add_widget(suite.show_subdisplay(device_param)) display = suite.get_subdisplay(device_param.device) suite.show_subdisplay(device_param) @@ -134,7 +132,7 @@ def test_device_parameter_tree( qtbot: pytestqt.qtbot.QtBot, ): tree = ParameterTree(showHeader=False) - devices = ptypes.GroupParameter(name='Devices') + devices = ptypes.GroupParameter(name="Devices") tree.addParameters(devices) qtbot.addWidget(tree) # Device with no subdevices @@ -197,10 +195,8 @@ def test_suite_save_device_screenshots(suite: TyphosSuite, device: MockDevice): def test_suite_save(suite: TyphosSuite, monkeypatch: pytest.MonkeyPatch): - tfile = Path(tempfile.gettempdir()) / 'test.py' - monkeypatch.setattr(QtWidgets.QFileDialog, - 'getSaveFileName', - lambda *_: (str(tfile), str(tfile))) + tfile = Path(tempfile.gettempdir()) / "test.py" + monkeypatch.setattr(QtWidgets.QFileDialog, "getSaveFileName", lambda *_: (str(tfile), str(tfile))) suite.save() assert tfile.exists() devices = [device.name for device in suite.devices] @@ -210,9 +206,7 @@ def test_suite_save(suite: TyphosSuite, monkeypatch: pytest.MonkeyPatch): def test_suite_save_cancel_smoke(suite: TyphosSuite, monkeypatch: pytest.MonkeyPatch): - monkeypatch.setattr(QtWidgets.QFileDialog, - 'getSaveFileName', - lambda *_: None) + monkeypatch.setattr(QtWidgets.QFileDialog, "getSaveFileName", lambda *_: None) suite.save() diff --git a/typhos/tests/test_utils.py b/typhos/tests/test_utils.py index 0bb0bfbf..1252bcb1 100644 --- a/typhos/tests/test_utils.py +++ b/typhos/tests/test_utils.py @@ -14,9 +14,16 @@ from .. import utils from ..suite import TyphosSuite -from ..utils import (TyphosBase, apply_standard_stylesheets, clean_name, - compose_stylesheets, load_suite, no_device_lazy_load, - saved_template, use_stylesheet) +from ..utils import ( + TyphosBase, + apply_standard_stylesheets, + clean_name, + compose_stylesheets, + load_suite, + no_device_lazy_load, + saved_template, + use_stylesheet, +) from . import conftest @@ -29,13 +36,12 @@ class LayeredDevice(Device): def test_clean_name(): - device = LayeredDevice(name='test') - assert clean_name(device.radial, strip_parent=False) == 'test radial' - assert clean_name(device.radial, strip_parent=True) == 'radial' - assert clean_name(device.radial.phi, - strip_parent=False) == 'test radial phi' - assert clean_name(device.radial.phi, strip_parent=True) == 'phi' - assert clean_name(device.radial.phi, strip_parent=device) == 'radial phi' + device = LayeredDevice(name="test") + assert clean_name(device.radial, strip_parent=False) == "test radial" + assert clean_name(device.radial, strip_parent=True) == "radial" + assert clean_name(device.radial.phi, strip_parent=False) == "test radial phi" + assert clean_name(device.radial.phi, strip_parent=True) == "phi" + assert clean_name(device.radial.phi, strip_parent=device) == "radial phi" def test_compose_stylesheets(qtbot: pytestqt.qtbot.QtBot, qapp): @@ -64,15 +70,9 @@ def test_compose_stylesheets(qtbot: pytestqt.qtbot.QtBot, qapp): green_sheet = "QLineEdit { color: green }" blue_sheet = "QLineEdit { color: blue }" other_sheet = "QLineEdit { background-color: white }" - red_widget.setStyleSheet( - compose_stylesheets([red_sheet, green_sheet, blue_sheet, other_sheet]) - ) - green_widget.setStyleSheet( - compose_stylesheets([green_sheet, red_sheet, other_sheet, blue_sheet]) - ) - blue_widget.setStyleSheet( - compose_stylesheets([blue_sheet, other_sheet, green_sheet, red_sheet]) - ) + red_widget.setStyleSheet(compose_stylesheets([red_sheet, green_sheet, blue_sheet, other_sheet])) + green_widget.setStyleSheet(compose_stylesheets([green_sheet, red_sheet, other_sheet, blue_sheet])) + blue_widget.setStyleSheet(compose_stylesheets([blue_sheet, other_sheet, green_sheet, red_sheet])) qapp.processEvents() # Each widget should have a white background and a unique foreground color @@ -164,16 +164,16 @@ def test_typhosbase_repaint_smoke(qtbot: pytestqt.qtbot.QtBot): def test_load_suite(qtbot: pytestqt.qtbot.QtBot, happi_cfg): # Setup new saved file - module = saved_template.format(devices=['test_motor']) - module_file = str(pathlib.Path(tempfile.gettempdir()) / 'my_suite.py') - with open(module_file, 'w+') as handle: + module = saved_template.format(devices=["test_motor"]) + module_file = str(pathlib.Path(tempfile.gettempdir()) / "my_suite.py") + with open(module_file, "w+") as handle: handle.write(module) suite = load_suite(module_file, happi_cfg) qtbot.addWidget(suite) assert isinstance(suite, TyphosSuite) assert len(suite.devices) == 1 - assert suite.devices[0].name == 'test_motor' + assert suite.devices[0].name == "test_motor" os.remove(module_file) @@ -186,16 +186,16 @@ def test_load_suite_with_bad_py_file(): def test_no_device_lazy_load(): class TestDevice(Device): - c = Cpt(Device, suffix='Test') + c = Cpt(Device, suffix="Test") - dev = TestDevice(name='foo') + dev = TestDevice(name="foo") old_val = Device.lazy_wait_for_connection assert dev.lazy_wait_for_connection is old_val assert dev.c.lazy_wait_for_connection is old_val with no_device_lazy_load(): - dev2 = TestDevice(name='foo') + dev2 = TestDevice(name="foo") assert Device.lazy_wait_for_connection is False assert dev2.lazy_wait_for_connection is False @@ -206,61 +206,63 @@ class TestDevice(Device): assert dev.c.lazy_wait_for_connection is old_val -class Class1: - ... +class Class1: ... -Class1.full_name = Class1.__module__ + '.' + Class1.__name__ +Class1.full_name = Class1.__module__ + "." + Class1.__name__ @pytest.mark.parametrize( - 'cls, view_type, expected, create', - [pytest.param( - Class1, 'detailed', - # Expected - ['Class1.detailed.ui'], - # Create these: - ['foo.bar.ui', 'Class1.detailed.ui'], - ), + "cls, view_type, expected, create", + [ + pytest.param( + Class1, + "detailed", + # Expected + ["Class1.detailed.ui"], + # Create these: + ["foo.bar.ui", "Class1.detailed.ui"], + ), pytest.param( - Class1, 'detailed', - # Expected - [Class1.full_name + '.detailed.ui', 'Class1.detailed.ui', - 'Class1.ui'], - # Create these: - ['a.ui', Class1.full_name + '.detailed.ui', 'Class1.detailed.ui', - 'Class1.ui'], - ), + Class1, + "detailed", + # Expected + [Class1.full_name + ".detailed.ui", "Class1.detailed.ui", "Class1.ui"], + # Create these: + ["a.ui", Class1.full_name + ".detailed.ui", "Class1.detailed.ui", "Class1.ui"], + ), pytest.param( - Class1, 'detailed', - # Expected - [Class1.full_name + '.detailed.ui', 'Class1.detailed.ui'], - # Create these: - [Class1.full_name + '.detailed.ui', 'b.ui', 'Class1.detailed.ui'], - ), + Class1, + "detailed", + # Expected + [Class1.full_name + ".detailed.ui", "Class1.detailed.ui"], + # Create these: + [Class1.full_name + ".detailed.ui", "b.ui", "Class1.detailed.ui"], + ), pytest.param( - Class1, 'detailed', - # Expected - ['Class1.ui'], - # Create these: - ['Class1.ui', 'c.ui', 'Class1.engineering.ui'], - ), + Class1, + "detailed", + # Expected + ["Class1.ui"], + # Create these: + ["Class1.ui", "c.ui", "Class1.engineering.ui"], + ), pytest.param( - Class1, 'detailed', - # Expected - ['Class1.py', 'Class1.ui'], - # Create these: - ['Class1.ui', 'Class1.py', 'c.ui', 'Class1.engineering.ui'], - ), - ] + Class1, + "detailed", + # Expected + ["Class1.py", "Class1.ui"], + # Create these: + ["Class1.ui", "Class1.py", "c.ui", "Class1.engineering.ui"], + ), + ], ) def test_path_search(tmpdir, cls, view_type, create, expected): for to_create in create: file = tmpdir.join(to_create) - file.write('') + file.write("") - results = utils.find_templates_for_class( - cls, view_type, paths=[tmpdir]) + results = utils.find_templates_for_class(cls, view_type, paths=[tmpdir]) assert list(r.name for r in results) == expected diff --git a/typhos/tests/test_widgets.py b/typhos/tests/test_widgets.py index 206c96cf..16b7b33e 100644 --- a/typhos/tests/test_widgets.py +++ b/typhos/tests/test_widgets.py @@ -5,8 +5,7 @@ from typhos import widgets from typhos.suite import SidebarParameter -from typhos.widgets import (ImageDialogButton, QDialog, SignalDialogButton, - TyphosSidebarItem, WaveformDialogButton) +from typhos.widgets import ImageDialogButton, QDialog, SignalDialogButton, TyphosSidebarItem, WaveformDialogButton from .conftest import pydm_version_xfail @@ -21,16 +20,16 @@ def widget(self): return widget -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def widget_button(qtbot, monkeypatch): - monkeypatch.setattr(QDialog, 'exec_', lambda x: 1) - button = DialogButton('ca://Pv:1') + monkeypatch.setattr(QDialog, "exec_", lambda x: 1) + button = DialogButton("ca://Pv:1") qtbot.addWidget(button) return button def test_sidebar_item(): - param = SidebarParameter(name='test', embeddable=True) + param = SidebarParameter(name="test", embeddable=True) item = TyphosSidebarItem(param, 0) assert len(item.toolbar.actions()) == 3 assert item.open_action.isEnabled() @@ -68,11 +67,9 @@ def test_signal_dialog_button_repeated_show(qtbot, widget_button): @pydm_version_xfail @pytest.mark.no_cleanup_check -@pytest.mark.parametrize('button_type', [WaveformDialogButton, - ImageDialogButton], - ids=['Waveform', 'Image']) +@pytest.mark.parametrize("button_type", [WaveformDialogButton, ImageDialogButton], ids=["Waveform", "Image"]) def test_dialog_button_instances_smoke(qtbot, button_type): - button = button_type(init_channel='ca://Pv:2') + button = button_type(init_channel="ca://Pv:2") qtbot.addWidget(button) widget = button.widget() qtbot.addWidget(widget) @@ -83,7 +80,7 @@ def test_line_edit_history(qtbot, motor): widget = widgets.TyphosLineEdit() qtbot.addWidget(widget) - widget.channel = 'sig://' + ophyd.sim.motor.setpoint.name + widget.channel = "sig://" + ophyd.sim.motor.setpoint.name widget.channeltype = int # hack pydm.utilities.establish_widget_connections(widget) @@ -92,7 +89,7 @@ def test_line_edit_history(qtbot, motor): widget.setText(str(i)) widget.send_value() - expected = items[-widget.setpointHistoryCount:] + expected = items[-widget.setpointHistoryCount :] assert list(widget.setpoint_history) == [str(s) for s in expected] # Smoke test menu creation diff --git a/typhos/tests/variety_ioc.py b/typhos/tests/variety_ioc.py index 4d41f113..9d1306f5 100644 --- a/typhos/tests/variety_ioc.py +++ b/typhos/tests/variety_ioc.py @@ -16,161 +16,158 @@ class Variants(ophyd.Device): soft_delta = Cpt(ophyd.Signal, value=1) - tweakable_delta_source_by_name = Cpt(EpicsSignal, 'tweakable') + tweakable_delta_source_by_name = Cpt(EpicsSignal, "tweakable") set_metadata( tweakable_delta_source_by_name, - {'variety': 'scalar-tweakable', - 'delta.signal': 'soft_delta', - 'delta.source': 'signal', - 'range.source': 'value', - 'range.value': [-10, 10], - } + { + "variety": "scalar-tweakable", + "delta.signal": "soft_delta", + "delta.source": "signal", + "range.source": "value", + "range.value": [-10, 10], + }, ) - tweakable_delta_source_by_component = Cpt(EpicsSignal, 'tweakable') + tweakable_delta_source_by_component = Cpt(EpicsSignal, "tweakable") set_metadata( tweakable_delta_source_by_component, - {'variety': 'scalar-tweakable', - 'delta.signal': soft_delta, - 'delta.source': 'signal', - 'range.source': 'value', - 'range.value': [-10, 10], - } + { + "variety": "scalar-tweakable", + "delta.signal": soft_delta, + "delta.source": "signal", + "range.source": "value", + "range.value": [-10, 10], + }, ) - array_tabular = Cpt(EpicsSignal, 'array-tabular') + array_tabular = Cpt(EpicsSignal, "array-tabular") set_metadata( array_tabular, - {'variety': 'array-tabular', - 'shape': (3, 3), - } + { + "variety": "array-tabular", + "shape": (3, 3), + }, ) class MyDevice(ophyd.Device): - command = Cpt(EpicsSignal, 'command-with-enum') - set_metadata(command, - {'variety': 'command', - 'tags': {'confirm', 'protected'}, - 'value': 1, - } - ) - - command_proc = Cpt(EpicsSignal, 'command-without-enum') - set_metadata(command_proc, {'variety': 'command-proc'}) - - command_enum = Cpt(EpicsSignal, 'command-without-enum') - set_metadata(command_enum, - {'variety': 'command-enum', - 'enum_dict': {0: 'No', 1: 'Yes', 3: 'Metadata-defined'}, - } - ) - - command_setpoint_tracks_readback = Cpt(EpicsSignal, - 'command-setpoint-tracks-readback') - set_metadata(command_setpoint_tracks_readback, - {'variety': 'command-setpoint-tracks-readback'}) - - tweakable = Cpt(EpicsSignal, 'tweakable') + command = Cpt(EpicsSignal, "command-with-enum") set_metadata( - tweakable, - {'variety': 'scalar-tweakable', - 'delta.value': 0.5, - 'delta.range': [-1, 1], - 'range.source': 'value', - 'range.value': [-1, 1], - } + command, + { + "variety": "command", + "tags": {"confirm", "protected"}, + "value": 1, + }, ) - array_timeseries = Cpt(EpicsSignal, 'array-timeseries') - set_metadata(array_timeseries, {'variety': 'array-timeseries'}) - - array_histogram = Cpt(EpicsSignal, 'array-histogram') - set_metadata(array_histogram, {'variety': 'array-histogram'}) + command_proc = Cpt(EpicsSignal, "command-without-enum") + set_metadata(command_proc, {"variety": "command-proc"}) - array_image = Cpt(EpicsSignal, 'array-image') + command_enum = Cpt(EpicsSignal, "command-without-enum") set_metadata( - array_image, - {'variety': 'array-image', - 'shape': (32, 32) - } + command_enum, + { + "variety": "command-enum", + "enum_dict": {0: "No", 1: "Yes", 3: "Metadata-defined"}, + }, ) - array_nd = Cpt(EpicsSignal, 'array-nd') + command_setpoint_tracks_readback = Cpt(EpicsSignal, "command-setpoint-tracks-readback") + set_metadata(command_setpoint_tracks_readback, {"variety": "command-setpoint-tracks-readback"}) + + tweakable = Cpt(EpicsSignal, "tweakable") set_metadata( - array_nd, - {'variety': 'array-nd', - 'shape': (16, 16, 4) - } + tweakable, + { + "variety": "scalar-tweakable", + "delta.value": 0.5, + "delta.range": [-1, 1], + "range.source": "value", + "range.value": [-1, 1], + }, ) - scalar = Cpt(EpicsSignal, 'scalar') - set_metadata(scalar, {'variety': 'scalar'}) + array_timeseries = Cpt(EpicsSignal, "array-timeseries") + set_metadata(array_timeseries, {"variety": "array-timeseries"}) + + array_histogram = Cpt(EpicsSignal, "array-histogram") + set_metadata(array_histogram, {"variety": "array-histogram"}) - scalar_range = Cpt(EpicsSignal, 'scalar-range') - set_metadata(scalar_range, {'variety': 'scalar-range'}) + array_image = Cpt(EpicsSignal, "array-image") + set_metadata(array_image, {"variety": "array-image", "shape": (32, 32)}) - bitmask = Cpt(EpicsSignal, 'bitmask') - set_metadata(bitmask, {'variety': 'bitmask', - 'bits': 4, - 'style': dict(shape='circle', - on_color='yellow', - off_color='white'), - 'meaning': ['A', 'B', 'C'], - }) + array_nd = Cpt(EpicsSignal, "array-nd") + set_metadata(array_nd, {"variety": "array-nd", "shape": (16, 16, 4)}) - text = Cpt(EpicsSignal, 'text', string=True) - set_metadata(text, {'variety': 'text'}) + scalar = Cpt(EpicsSignal, "scalar") + set_metadata(scalar, {"variety": "scalar"}) + + scalar_range = Cpt(EpicsSignal, "scalar-range") + set_metadata(scalar_range, {"variety": "scalar-range"}) + + bitmask = Cpt(EpicsSignal, "bitmask") + set_metadata( + bitmask, + { + "variety": "bitmask", + "bits": 4, + "style": dict(shape="circle", on_color="yellow", off_color="white"), + "meaning": ["A", "B", "C"], + }, + ) - text_multiline = Cpt(EpicsSignal, 'text-multiline', string=True) - set_metadata(text_multiline, {'variety': 'text-multiline'}) + text = Cpt(EpicsSignal, "text", string=True) + set_metadata(text, {"variety": "text"}) - text_enum = Cpt(EpicsSignal, 'text-enum', string=True) - set_metadata(text_enum, {'variety': 'text-enum'}) + text_multiline = Cpt(EpicsSignal, "text-multiline", string=True) + set_metadata(text_multiline, {"variety": "text-multiline"}) - enum = Cpt(EpicsSignal, 'enum') - set_metadata(enum, {'variety': 'enum'}) + text_enum = Cpt(EpicsSignal, "text-enum", string=True) + set_metadata(text_enum, {"variety": "text-enum"}) - variants = Cpt(Variants, '') + enum = Cpt(EpicsSignal, "enum") + set_metadata(enum, {"variety": "enum"}) + + variants = Cpt(Variants, "") class VarietyIOC(PVGroup): - """ - """ - command_without_enum = pvproperty(value=0, name='command-without-enum') - command_with_enum = pvproperty(value=0, name='command-with-enum', - enum_strings=['Off', 'On'], - dtype=caproto.ChannelType.ENUM) - - command_setpoint_tracks_readback = pvproperty( - value=0, name='command-setpoint-tracks-readback') - tweakable = pvproperty(value=0, name='tweakable', - lower_ctrl_limit=-5, - upper_ctrl_limit=5, - ) - array_tabular = pvproperty(value=[1.5] * (3 * 3), name='array-tabular') - array_timeseries = pvproperty(value=[0.5] * 30, name='array-timeseries') - array_histogram = pvproperty(value=[0.5] * 30, name='array-histogram') - array_image = pvproperty(value=[200] * 1024, name='array-image') - array_nd = pvproperty(value=[200] * 1024, name='array-nd') - scalar = pvproperty(value=1.2, name='scalar') - scalar_range = pvproperty(value=1.3, name='scalar-range', - lower_ctrl_limit=-3.14, - upper_ctrl_limit=3.14, - precision=3, - ) - bitmask = pvproperty(value=0, name='bitmask') - text = pvproperty(value='the text', name='text') - text_multiline = pvproperty(value='multiline\ntext', name='text-multiline') - text_enum = pvproperty(value='enum value 0', name='text-enum') - enum = pvproperty( - value='enum1', enum_strings=['enum1', 'enum2'], - dtype=caproto.ChannelType.ENUM, name='enum') - - -if __name__ == '__main__': - ioc_options, run_options = ioc_arg_parser( - default_prefix='variety:', - desc=dedent(VarietyIOC.__doc__)) + """ """ + + command_without_enum = pvproperty(value=0, name="command-without-enum") + command_with_enum = pvproperty( + value=0, name="command-with-enum", enum_strings=["Off", "On"], dtype=caproto.ChannelType.ENUM + ) + + command_setpoint_tracks_readback = pvproperty(value=0, name="command-setpoint-tracks-readback") + tweakable = pvproperty( + value=0, + name="tweakable", + lower_ctrl_limit=-5, + upper_ctrl_limit=5, + ) + array_tabular = pvproperty(value=[1.5] * (3 * 3), name="array-tabular") + array_timeseries = pvproperty(value=[0.5] * 30, name="array-timeseries") + array_histogram = pvproperty(value=[0.5] * 30, name="array-histogram") + array_image = pvproperty(value=[200] * 1024, name="array-image") + array_nd = pvproperty(value=[200] * 1024, name="array-nd") + scalar = pvproperty(value=1.2, name="scalar") + scalar_range = pvproperty( + value=1.3, + name="scalar-range", + lower_ctrl_limit=-3.14, + upper_ctrl_limit=3.14, + precision=3, + ) + bitmask = pvproperty(value=0, name="bitmask") + text = pvproperty(value="the text", name="text") + text_multiline = pvproperty(value="multiline\ntext", name="text-multiline") + text_enum = pvproperty(value="enum value 0", name="text-enum") + enum = pvproperty(value="enum1", enum_strings=["enum1", "enum2"], dtype=caproto.ChannelType.ENUM, name="enum") + + +if __name__ == "__main__": + ioc_options, run_options = ioc_arg_parser(default_prefix="variety:", desc=dedent(VarietyIOC.__doc__)) ioc = VarietyIOC(**ioc_options) run(ioc.pvdb, **run_options) diff --git a/typhos/textedit.py b/typhos/textedit.py index 70239ef7..b828b358 100644 --- a/typhos/textedit.py +++ b/typhos/textedit.py @@ -4,6 +4,7 @@ Variety support pending: - Text format """ + import logging import numpy as np @@ -16,7 +17,7 @@ @variety.uses_key_handlers -@variety.use_for_variety_write('text-multiline') +@variety.use_for_variety_write("text-multiline") class TyphosTextEdit(QtWidgets.QWidget, PyDMWritableWidget): """ A writable, multiline text editor with support for PyDM Channels. @@ -30,14 +31,13 @@ class TyphosTextEdit(QtWidgets.QWidget, PyDMWritableWidget): The channel to be used by the widget. """ - def __init__(self, parent=None, init_channel=None, variety_metadata=None, - ophyd_signal=None): + def __init__(self, parent=None, init_channel=None, variety_metadata=None, ophyd_signal=None): self._display_text = None self._encoding = "utf-8" - self._delimiter = '\n' + self._delimiter = "\n" self._ophyd_signal = ophyd_signal - self._format = 'plain' + self._format = "plain" self._raw_value = None QtWidgets.QWidget.__init__(self, parent) @@ -53,10 +53,10 @@ def _setup_ui(self): self.setLayout(layout) self._text_edit = QtWidgets.QTextEdit() - self._send_button = QtWidgets.QPushButton('Send') + self._send_button = QtWidgets.QPushButton("Send") self._send_button.clicked.connect(self._send_clicked) - self._revert_button = QtWidgets.QPushButton('Revert') + self._revert_button = QtWidgets.QPushButton("Revert") self._revert_button.clicked.connect(self._revert_clicked) self._button_layout = QtWidgets.QHBoxLayout() @@ -86,8 +86,7 @@ def _to_wire(self, text=None): text = self._text_edit.toPlainText() text = self._delimiter.join(text.splitlines()) - return np.array(list(text.encode(self._encoding)), - dtype=np.uint8) + return np.array(list(text.encode(self._encoding)), dtype=np.uint8) def _from_wire(self, value): """numpy array/string/bytes -> string.""" @@ -107,8 +106,10 @@ def send_value(self): except ValueError: logger.exception( "send_value error %r with type %r and format %r (widget %r).", - send_value, self.channeltype, self._display_format_type, - self.objectName() + send_value, + self.channeltype, + self._display_format_type, + self.objectName(), ) self._text_edit.document().setModified(False) @@ -137,18 +138,18 @@ def _reinterpret_text(self): if self._raw_value is not None: self.value_changed(self._raw_value) - @variety.key_handler('delimiter') + @variety.key_handler("delimiter") def _variety_key_handler_delimiter(self, delimiter): self._delimiter = delimiter - @variety.key_handler('encoding') + @variety.key_handler("encoding") def _variety_key_handler_encoding(self, encoding): self._encoding = encoding self._reinterpret_text() - @variety.key_handler('format') + @variety.key_handler("format") def _variety_key_handler_format(self, format_): self._format = format_ - if format_ != 'plain': - logger.warning('Non-plain formats not yet implemented.') + if format_ != "plain": + logger.warning("Non-plain formats not yet implemented.") self._reinterpret_text() diff --git a/typhos/tools/__init__.py b/typhos/tools/__init__.py index 52b50196..528b9d04 100644 --- a/typhos/tools/__init__.py +++ b/typhos/tools/__init__.py @@ -1,4 +1,5 @@ """Module for all insertable Typhos tools""" + __all__ = ["TyphosLogDisplay", "TyphosTimePlot"] from .log import TyphosLogDisplay diff --git a/typhos/tools/log.py b/typhos/tools/log.py index 1e7bc489..0406f455 100644 --- a/typhos/tools/log.py +++ b/typhos/tools/log.py @@ -8,14 +8,14 @@ class TyphosLogDisplay(TyphosBase): """Typhos Logging Display.""" + def __init__(self, level=logging.INFO, parent=None): super().__init__(parent=parent) # Set the logname to be non-existant so that we do not attach to the # root logger. This causes issue if this widget is closed before the # end of the Python session. For the long term this issue will be # resolved with https://github.com/slaclab/pydm/issues/474 - self.logdisplay = PyDMLogDisplay(logname='not_set', level=level, - parent=self) + self.logdisplay = PyDMLogDisplay(logname="not_set", level=level, parent=self) self.setLayout(QVBoxLayout()) self.layout().addWidget(self.logdisplay) @@ -29,5 +29,5 @@ def add_device(self, device): # the existing handler do all the filtering else: device.log.setLevel(logging.NOTSET) - logger = getattr(device.log, 'logger', device.log) + logger = getattr(device.log, "logger", device.log) logger.addHandler(self.logdisplay.handler) diff --git a/typhos/tools/plot.py b/typhos/tools/plot.py index ad68b22f..2037a8a5 100644 --- a/typhos/tools/plot.py +++ b/typhos/tools/plot.py @@ -1,12 +1,12 @@ """ Typhos Plotting Interface """ + import logging from qtpy import QtCore, QtGui from qtpy.QtCore import Qt, Slot -from qtpy.QtWidgets import (QComboBox, QHBoxLayout, QLabel, QPushButton, - QVBoxLayout) +from qtpy.QtWidgets import QComboBox, QHBoxLayout, QLabel, QPushButton, QVBoxLayout from timechart.displays.main_display import TimeChartDisplay from timechart.utilities.utils import random_color @@ -42,9 +42,9 @@ def __init__(self, parent=None): self.signal_combo = QComboBox() self.signal_combo.setModel(self._proxy_model) - self.signal_combo_label = QLabel('Available Signals: ') + self.signal_combo_label = QLabel("Available Signals: ") - self.signal_create = QPushButton('Connect') + self.signal_create = QPushButton("Connect") self.signal_combo_layout = QHBoxLayout() self.signal_combo_layout.addWidget(self.signal_combo_label, 0) self.signal_combo_layout.addWidget(self.signal_combo, 1) @@ -55,8 +55,7 @@ def __init__(self, parent=None): self.timechart = TimeChartDisplay(show_pv_add_panel=False) self.layout().addWidget(self.timechart) cache = get_global_describe_cache() - cache.new_description.connect(self._new_description, - Qt.QueuedConnection) + cache.new_description.connect(self._new_description, Qt.QueuedConnection) @property def channel_to_curve(self): @@ -85,7 +84,7 @@ def add_available_signal(self, signal, name): If a signal of the same name already is available. """ if name in self._available_signals: - raise ValueError('Signal already available') + raise ValueError("Signal already available") channel = utils.channel_from_signal(signal) self._available_signals[name] = (signal, channel) @@ -119,8 +118,7 @@ def add_curve(self, channel, name=None, color=None, **kwargs): if not color: color = random_color() logger.debug("Adding %s to plot ...", channel) - self.timechart.add_y_channel(pv_name=channel, curve_name=name, - color=color, **kwargs) + self.timechart.add_y_channel(pv_name=channel, curve_name=name, color=color, **kwargs) @Slot() def remove_curve(self, name): @@ -153,14 +151,14 @@ def creation_requested(self): @Slot(object, dict) def _new_description(self, signal, desc): - name = f'{signal.root.name}.{signal.dotted_name}' - if 'dtype' not in desc: + name = f"{signal.root.name}.{signal.dotted_name}" + if "dtype" not in desc: # Marks an error in retrieving the description logger.debug("Ignoring signal without description %s", name) return # Only include scalars - if desc['dtype'] not in ('integer', 'number'): + if desc["dtype"] not in ("integer", "number"): logger.debug("Ignoring non-scalar signal %s", name) return @@ -176,8 +174,7 @@ def add_device(self, device): super().add_device(device) cache = get_global_describe_cache() - for signal in utils.get_all_signals_from_device(device, - include_lazy=False): + for signal in utils.get_all_signals_from_device(device, include_lazy=False): desc = cache.get(signal) if desc is not None: self._new_description(signal, desc) diff --git a/typhos/tweakable.py b/typhos/tweakable.py index 27284c1e..f4396a85 100644 --- a/typhos/tweakable.py +++ b/typhos/tweakable.py @@ -4,6 +4,7 @@ Variety support pending: - everything """ + import logging import qtpy @@ -15,7 +16,7 @@ @variety.uses_key_handlers -@variety.use_for_variety_write('scalar-tweakable') +@variety.use_for_variety_write("scalar-tweakable") class TyphosTweakable(utils.TyphosBase): # TODO rearrange package: widgets.TyphosDesignerMixin): """ @@ -33,12 +34,11 @@ class TyphosTweakable(utils.TyphosBase): ----- """ - ui_template = utils.ui_dir / 'widgets' / 'tweakable.ui' - _readback_attr = 'readback' - _setpoint_attr = 'setpoint' + ui_template = utils.ui_dir / "widgets" / "tweakable.ui" + _readback_attr = "readback" + _setpoint_attr = "setpoint" - def __init__(self, parent=None, init_channel=None, variety_metadata=None, - ophyd_signal=None): + def __init__(self, parent=None, init_channel=None, variety_metadata=None, ophyd_signal=None): self._ophyd_signal = ophyd_signal super().__init__(parent=parent) @@ -65,7 +65,7 @@ def tweak(self, offset): try: setpoint = float(self.readback.text()) + float(offset) except Exception: - logger.exception('Tweak failed') + logger.exception("Tweak failed") return self.ui.setpoint.setText(str(setpoint)) @@ -77,7 +77,7 @@ def positive_tweak(self): try: self.tweak(float(self.tweak_value.text())) except Exception: - logger.exception('Tweak failed') + logger.exception("Tweak failed") @QtCore.Slot() def negative_tweak(self): @@ -85,4 +85,4 @@ def negative_tweak(self): try: self.tweak(-float(self.tweak_value.text())) except Exception: - logger.exception('Tweak failed') + logger.exception("Tweak failed") diff --git a/typhos/utils.py b/typhos/utils.py index 3175cde2..ce13347b 100644 --- a/typhos/utils.py +++ b/typhos/utils.py @@ -1,6 +1,7 @@ """ Utility functions for typhos """ + from __future__ import annotations import atexit @@ -31,8 +32,7 @@ from pydm.config import STYLESHEET as PYDM_USER_STYLESHEET from pydm.config import STYLESHEET_INCLUDE_DEFAULT as PYDM_INCLUDE_DEFAULT from pydm.exception import raise_to_operator # noqa -from pydm.utilities.stylesheet import \ - GLOBAL_STYLESHEET as PYDM_DEFAULT_STYLESHEET +from pydm.utilities.stylesheet import GLOBAL_STYLESHEET as PYDM_DEFAULT_STYLESHEET from pydm.widgets.base import PyDMWritableWidget from qtpy import QtCore, QtGui, QtWidgets from qtpy.QtCore import QSize @@ -53,42 +53,38 @@ # - str # - pathlib.Path # - list of such objects -TYPHOS_ENTRY_POINT_KEY = 'typhos.ui' +TYPHOS_ENTRY_POINT_KEY = "typhos.ui" MODULE_PATH = pathlib.Path(__file__).parent.resolve() -ui_dir = MODULE_PATH / 'ui' -ui_core_dir = ui_dir / 'core' +ui_dir = MODULE_PATH / "ui" +ui_core_dir = ui_dir / "core" -GrabKindItem = collections.namedtuple('GrabKindItem', - ('attr', 'component', 'signal')) -DEBUG_MODE = bool(os.environ.get('TYPHOS_DEBUG', False)) +GrabKindItem = collections.namedtuple("GrabKindItem", ("attr", "component", "signal")) +DEBUG_MODE = bool(os.environ.get("TYPHOS_DEBUG", False)) # Help settings: # TYPHOS_HELP_URL (str): The help URL format string -HELP_URL = os.environ.get('TYPHOS_HELP_URL', "").strip() +HELP_URL = os.environ.get("TYPHOS_HELP_URL", "").strip() HELP_WEB_ENABLED = bool(HELP_URL.strip()) # TYPHOS_HELP_HEADERS (json): headers to pass to HELP_URL -HELP_HEADERS = json.loads(os.environ.get('TYPHOS_HELP_HEADERS', "") or "{}") +HELP_HEADERS = json.loads(os.environ.get("TYPHOS_HELP_HEADERS", "") or "{}") HELP_HEADERS_HOSTS = os.environ.get("TYPHOS_HELP_HEADERS_HOSTS", "").split(",") # TYPHOS_HELP_TOKEN (str): An optional token for the bearer authentication # scheme - e.g., personal access tokens with Confluence -HELP_TOKEN = os.environ.get('TYPHOS_HELP_TOKEN', None) +HELP_TOKEN = os.environ.get("TYPHOS_HELP_TOKEN", None) if HELP_TOKEN: HELP_HEADERS["Authorization"] = f"Bearer {HELP_TOKEN}" # TYPHOS_JIRA_URL (str): The jira REST API collector URL -JIRA_URL = os.environ.get('TYPHOS_JIRA_URL', "").strip() +JIRA_URL = os.environ.get("TYPHOS_JIRA_URL", "").strip() # TYPHOS_JIRA_HEADERS (json): headers to pass to JIRA_URL -JIRA_HEADERS = json.loads( - os.environ.get('TYPHOS_JIRA_HEADERS', '{"X-Atlassian-Token": "no-check"}') - or "{}" -) +JIRA_HEADERS = json.loads(os.environ.get("TYPHOS_JIRA_HEADERS", '{"X-Atlassian-Token": "no-check"}') or "{}") # TYPHOS_JIRA_TOKEN (str): An optional token for the bearer authentication # scheme - e.g., personal access tokens with Confluence -JIRA_TOKEN = os.environ.get('TYPHOS_JIRA_TOKEN', None) +JIRA_TOKEN = os.environ.get("TYPHOS_JIRA_TOKEN", None) # TYPHOS_JIRA_EMAIL_SUFFIX (str): The default e-mail address suffix -JIRA_EMAIL_SUFFIX = os.environ.get('TYPHOS_JIRA_EMAIL_SUFFIX', "").strip() +JIRA_EMAIL_SUFFIX = os.environ.get("TYPHOS_JIRA_EMAIL_SUFFIX", "").strip() if JIRA_TOKEN: JIRA_HEADERS["Authorization"] = f"Bearer {JIRA_TOKEN}" @@ -96,8 +92,7 @@ logger.info("happi is not installed; some features may be unavailable") -class TyphosException(Exception): - ... +class TyphosException(Exception): ... def _get_display_paths(): @@ -110,7 +105,7 @@ def _get_display_paths(): - The typhos.ui entry point - typhos built-ins """ - paths = os.environ.get('PYDM_DISPLAYS_PATH', '') + paths = os.environ.get("PYDM_DISPLAYS_PATH", "") for path in paths.split(os.pathsep): path = pathlib.Path(path).expanduser().resolve() if path.exists() and path.is_dir(): @@ -123,8 +118,7 @@ def _get_display_paths(): try: obj = entry.load() except Exception: - msg = (f'Failed to load {TYPHOS_ENTRY_POINT_KEY} ' - f'entry: {entry.name}.') + msg = f"Failed to load {TYPHOS_ENTRY_POINT_KEY} entry: {entry.name}." logger.error(msg) logger.debug(msg, exc_info=True) continue @@ -137,19 +131,18 @@ def _get_display_paths(): try: yield pathlib.Path(obj) except Exception: - msg = (f'{TYPHOS_ENTRY_POINT_KEY} entry point ' - f'{entry.name}: {obj} is not a valid path!') + msg = f"{TYPHOS_ENTRY_POINT_KEY} entry point {entry.name}: {obj} is not a valid path!" logger.error(msg) logger.debug(msg, exc_info=True) - yield ui_dir / 'core' - yield ui_dir / 'devices' + yield ui_dir / "core" + yield ui_dir / "devices" DISPLAY_PATHS = list(_get_display_paths()) -if hasattr(ophyd.signal, 'SignalRO'): +if hasattr(ophyd.signal, "SignalRO"): SignalRO = ophyd.signal.SignalRO else: # SignalRO was re-introduced to ophyd.signal in December 2019 (1f83a055). @@ -188,7 +181,7 @@ def channel_from_signal(signal, read=True): if pvname is not None and isinstance(pvname, str): return channel_name(pvname) - return channel_name(signal.name, protocol='sig') + return channel_name(signal.name, protocol="sig") def is_signal_ro(signal): @@ -201,18 +194,18 @@ def is_signal_ro(signal): return isinstance(signal, (SignalRO, EpicsSignalRO, ophyd.sim.SynSignalRO)) -def channel_name(pv, protocol='ca'): +def channel_name(pv, protocol="ca"): """ Create a valid PyDM channel from a PV name """ - return protocol + '://' + pv + return protocol + "://" + pv def clean_attr(attr): """ Create a nicer, human readable alias from a Python attribute name """ - return attr.replace('.', ' ').replace('_', ' ') + return attr.replace(".", " ").replace("_", " ") def clean_name(device, strip_parent=True): @@ -235,7 +228,7 @@ def clean_name(device, strip_parent=True): parent_name = strip_parent.name else: parent_name = device.parent.name - name = name.replace(parent_name + '_', '') + name = name.replace(parent_name + "_", "") # Return the cleaned alias return clean_attr(name) @@ -262,14 +255,14 @@ def use_stylesheet( # Dark Style if dark: import qdarkstyle + style = qdarkstyle.load_stylesheet_pyqt5() # Light Style else: # Load the path to the file - style_path = os.path.join(ui_dir, 'style.qss') + style_path = os.path.join(ui_dir, "style.qss") if not os.path.exists(style_path): - raise OSError("Unable to find Typhos stylesheet in {}" - "".format(style_path)) + raise OSError("Unable to find Typhos stylesheet in {}".format(style_path)) # Load the stylesheet from the file with open(style_path) as handle: style = handle.read() @@ -277,7 +270,7 @@ def use_stylesheet( widget = QtWidgets.QApplication.instance() # We can set Fusion style if it is an application if isinstance(widget, QtWidgets.QApplication): - widget.setStyle(QtWidgets.QStyleFactory.create('Fusion')) + widget.setStyle(QtWidgets.QStyleFactory.create("Fusion")) # Set Stylesheet widget.setStyleSheet(style) @@ -364,7 +357,7 @@ def apply_standard_stylesheets( If omitted, apply to the whole QApplication. """ if isinstance(widget, QtWidgets.QApplication): - widget.setStyle(QtWidgets.QStyleFactory.create('Fusion')) + widget.setStyle(QtWidgets.QStyleFactory.create("Fusion")) stylesheets = [] @@ -380,9 +373,10 @@ def apply_standard_stylesheets( if dark: import qdarkstyle + stylesheets.append(qdarkstyle.load_stylesheet_pyqt5()) else: - stylesheets.append(ui_dir / 'style.qss') + stylesheets.append(ui_dir / "style.qss") if include_pydm and PYDM_INCLUDE_DEFAULT: stylesheets.append(PYDM_DEFAULT_STYLESHEET) @@ -392,9 +386,7 @@ def apply_standard_stylesheets( def random_color(): """Return a random hex color description""" - return QColor(random.randint(0, 255), - random.randint(0, 255), - random.randint(0, 255)) + return QColor(random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)) class TyphosLoading(QtWidgets.QLabel): @@ -408,6 +400,7 @@ class TyphosLoading(QtWidgets.QLabel): and replace it with a default timeout message. """ + LOADING_TIMEOUT_MS = 10000 loading_gif = None @@ -416,15 +409,14 @@ def __init__(self, timeout_message, *, parent=None, **kwargs): super().__init__(parent=parent, **kwargs) self._icon_size = QSize(32, 32) if TyphosLoading.loading_gif is None: - loading_path = os.path.join(ui_dir, 'loading.gif') + loading_path = os.path.join(ui_dir, "loading.gif") TyphosLoading.loading_gif = QMovie(loading_path) self._animation = TyphosLoading.loading_gif self._animation.setScaledSize(self._icon_size) self.setMovie(self._animation) self._animation.start() if self.LOADING_TIMEOUT_MS > 0: - QtCore.QTimer.singleShot(self.LOADING_TIMEOUT_MS, - self._handle_timeout) + QtCore.QTimer.singleShot(self.LOADING_TIMEOUT_MS, self._handle_timeout) self.setContextMenuPolicy(QtCore.Qt.DefaultContextMenu) @@ -435,17 +427,14 @@ def copy_to_clipboard(*, text): clipboard = QtWidgets.QApplication.instance().clipboard() clipboard.setText(text) - menu.addSection('Copy to clipboard') - action = menu.addAction('&All') - action.triggered.connect(functools.partial(copy_to_clipboard, - text=self.toolTip())) + menu.addSection("Copy to clipboard") + action = menu.addAction("&All") + action.triggered.connect(functools.partial(copy_to_clipboard, text=self.toolTip())) menu.addSeparator() for line in self.toolTip().splitlines(): action = menu.addAction(line) - action.triggered.connect( - functools.partial(copy_to_clipboard, text=line) - ) + action.triggered.connect(functools.partial(copy_to_clipboard, text=line)) menu.exec_(self.mapToGlobal(event.pos())) @@ -487,8 +476,7 @@ def paintEvent(self, event): opt.initFrom(self) painter = QPainter() painter.begin(self) - self.style().drawPrimitive(QtWidgets.QStyle.PE_Widget, opt, painter, - self) + self.style().drawPrimitive(QtWidgets.QStyle.PE_Widget, opt, painter, self) super().paintEvent(event) @classmethod @@ -536,23 +524,13 @@ class WeakPartialMethodSlot: **kwargs : Keyword arguments to pass to the method. """ - def __init__( - self, - signal_owner: QtCore.QObject, - signal: QtCore.Signal, - method: MethodType, - *args, - **kwargs - ): + + def __init__(self, signal_owner: QtCore.QObject, signal: QtCore.Signal, method: MethodType, *args, **kwargs): self.signal = signal self.signal.connect(self._call, QtCore.Qt.QueuedConnection) self.method = weakref.WeakMethod(method) - self._method_finalizer = weakref.finalize( - method.__self__, self._method_destroyed - ) - self._signal_finalizer = weakref.finalize( - signal_owner, self._signal_destroyed - ) + self._method_finalizer = weakref.finalize(method.__self__, self._method_destroyed) + self._signal_finalizer = weakref.finalize(signal_owner, self._signal_destroyed) self.partial_args = args self.partial_kwargs = kwargs @@ -609,12 +587,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def _connect_partial_weakly( - self, - signal_owner: QtCore.QObject, - signal: QtCore.Signal, - method: MethodType, - *args, - **kwargs + self, signal_owner: QtCore.QObject, signal: QtCore.Signal, method: MethodType, *args, **kwargs ): """ Connect the provided signal to an instance method via @@ -633,9 +606,7 @@ def _connect_partial_weakly( **kwargs : Keyword arguments to pass to the method. """ - slot = WeakPartialMethodSlot( - signal_owner, signal, method, *args, **kwargs - ) + slot = WeakPartialMethodSlot(signal_owner, signal, method, *args, **kwargs) self._weak_partials_.append(slot) @@ -649,11 +620,11 @@ def make_identifier(name): # Leading / following whitespace name = name.strip() # Intermediate whitespace should be underscores - name = re.sub('[\\s\\t\\n]+', '_', name) + name = re.sub("[\\s\\t\\n]+", "_", name) # Remove invalid characters - name = re.sub('[^0-9a-zA-Z_]', '', name) + name = re.sub("[^0-9a-zA-Z_]", "", name) # Remove leading characters until we find a letter or an underscore - name = re.sub('^[^a-zA-Z_]+', '', name) + name = re.sub("^[^a-zA-Z_]+", "", name) return name @@ -699,7 +670,7 @@ def save_suite(suite, file_or_buffer): """ # Accept file-like objects or a handle if isinstance(file_or_buffer, str): - handle = open(file_or_buffer, 'w+') + handle = open(file_or_buffer, "w+") else: handle = file_or_buffer logger.debug("Saving TyphosSuite contents to %r", handle) @@ -708,7 +679,7 @@ def save_suite(suite, file_or_buffer): def load_suite(path, cfg=None): - """" + """ " Load a file saved via Typhos Parameters @@ -725,12 +696,11 @@ def load_suite(path, cfg=None): suite: TyphosSuite """ logger.info("Importing TyphosSuite from file %r ...", path) - module_name = pathlib.Path(path).name.replace('.py', '') - spec = importlib.util.spec_from_file_location(module_name, - path) + module_name = pathlib.Path(path).name.replace(".py", "") + spec = importlib.util.spec_from_file_location(module_name, path) suite_module = importlib.util.module_from_spec(spec) spec.loader.exec_module(suite_module) - if hasattr(suite_module, 'create_suite'): + if hasattr(suite_module, "create_suite"): logger.debug("Executing create_suite method from %r", suite_module) return suite_module.create_suite(cfg=cfg) else: @@ -753,10 +723,10 @@ def create_suite(cfg=None): @contextlib.contextmanager def no_device_lazy_load(): - ''' + """ Context manager which disables the ophyd.device.Device `lazy_wait_for_connection` behavior and later restore its value. - ''' + """ old_val = Device.lazy_wait_for_connection try: Device.lazy_wait_for_connection = False @@ -766,34 +736,34 @@ def no_device_lazy_load(): def pyqt_class_from_enum(enum): - ''' + """ Create an inheritable base class from a Python Enum, which can also be used for Q_ENUMS. - ''' + """ enum_dict = {item.name: item.value for item in list(enum)} - return type(enum.__name__, (object, ), enum_dict) + return type(enum.__name__, (object,), enum_dict) def _get_template_filenames_for_class(class_, view_type, *, include_mro=True): - ''' + """ Yields all possible template filenames that can be used for the class, in order of priority, including those in the class MRO. This does not include the file extension, to be appended by the caller. - ''' + """ for cls in class_.mro(): module = cls.__module__ name = cls.__name__ - yield f'{module}.{name}.{view_type}' - yield f'{name}.{view_type}' - yield f'{name}' + yield f"{module}.{name}.{view_type}" + yield f"{name}.{view_type}" + yield f"{name}" if not include_mro: break def remove_duplicate_items(list_): - 'Return a de-duplicated list/tuple of items in `list_`, retaining order' + "Return a de-duplicated list/tuple of items in `list_`, retaining order" cls = type(list_) return cls(sorted(set(list_), key=list_.index)) @@ -810,9 +780,8 @@ def is_standard_template(template): return common_path == ui_core_dir -def find_templates_for_class(cls, view_type, paths, *, extensions=None, - include_mro=True): - ''' +def find_templates_for_class(cls, view_type, paths, *, extensions=None, include_mro=True): + """ Given a class `cls` and a view type (such as 'detailed'), search `paths` for potential templates to show. @@ -833,22 +802,20 @@ def find_templates_for_class(cls, view_type, paths, *, extensions=None, ------ path : pathlib.Path A matching path, ordered from most-to-least specific. - ''' + """ if not inspect.isclass(cls): cls = type(cls) if not extensions: - extensions = ['.py', '.ui'] + extensions = [".py", ".ui"] elif isinstance(extensions, str): extensions = [extensions] from .cache import _CachedPath - paths = remove_duplicate_items( - [_CachedPath.from_path(p) for p in paths] - ) - for candidate_filename in _get_template_filenames_for_class( - cls, view_type, include_mro=include_mro): + paths = remove_duplicate_items([_CachedPath.from_path(p) for p in paths]) + + for candidate_filename in _get_template_filenames_for_class(cls, view_type, include_mro=include_mro): for extension in extensions: for path in paths: for match in path.glob(candidate_filename + extension): @@ -857,7 +824,7 @@ def find_templates_for_class(cls, view_type, paths, *, extensions=None, def find_file_in_paths(filename, *, paths=None): - ''' + """ Search for filename ``filename`` in the list of paths ``paths`` Parameters @@ -870,7 +837,7 @@ def find_file_in_paths(filename, *, paths=None): Yields ------ All filenames that match in the given paths - ''' + """ if paths is None: paths = DISPLAY_PATHS @@ -883,9 +850,8 @@ def find_file_in_paths(filename, *, paths=None): filename = filename.name from .cache import _CachedPath - paths = remove_duplicate_items( - [_CachedPath.from_path(p) for p in paths] - ) + + paths = remove_duplicate_items([_CachedPath.from_path(p) for p in paths]) for path in paths: for match in path.glob(filename): @@ -909,12 +875,12 @@ def get_device_from_fake_class(cls): """ bases = cls.__bases__ if not bases or len(bases) != 1: - raise ValueError('Not a fake class based on inheritance') + raise ValueError("Not a fake class based on inheritance") - actual_class, = bases + (actual_class,) = bases if actual_class not in ophyd.sim.fake_device_cache: - raise ValueError('Not a fake class (ophyd.sim does not know about it)') + raise ValueError("Not a fake class (ophyd.sim does not know about it)") return actual_class @@ -941,25 +907,23 @@ def code_from_device_repr(device): try: module = device.__module__ except AttributeError: - raise ValueError('Device class must be in a module') from None + raise ValueError("Device class must be in a module") from None class_name = device.__class__.__name__ - if module == '__main__': - raise ValueError('Device class must be in a module') + if module == "__main__": + raise ValueError("Device class must be in a module") cls = device.__class__ is_fake = is_fake_device_class(cls) - full_class_name = f'{module}.{class_name}' - kwargs = '\n '.join(f'{k}={v!r},' for k, v in device._repr_info()) - logger.debug('%r fully qualified Device class: %r', device.name, - full_class_name) + full_class_name = f"{module}.{class_name}" + kwargs = "\n ".join(f"{k}={v!r}," for k, v in device._repr_info()) + logger.debug("%r fully qualified Device class: %r", device.name, full_class_name) if is_fake: actual_class = get_device_from_fake_class(cls) - actual_name = f'{actual_class.__module__}.{actual_class.__name__}' - logger.debug('%r fully qualified Device class is fake, based on: %r', - device.name, actual_class) - return f'''\ + actual_name = f"{actual_class.__module__}.{actual_class.__name__}" + logger.debug("%r fully qualified Device class is fake, based on: %r", device.name, actual_class) + return f"""\ import ophyd.sim import pcdsutils @@ -969,16 +933,16 @@ def code_from_device_repr(device): {kwargs} ) ophyd.sim.clear_fake_device({device.name}) -''' +""" - return f'''\ + return f"""\ import pcdsutils {class_name} = pcdsutils.utils.import_helper({full_class_name!r}) {device.name} = {class_name}( {kwargs} ) -''' +""" def code_from_device(device): @@ -986,7 +950,7 @@ def code_from_device(device): Generate code required to load ``device`` in another process """ is_fake = is_fake_device_class(device.__class__) - if happi is None or not hasattr(device, 'md') or is_fake: + if happi is None or not hasattr(device, "md") or is_fake: return code_from_device_repr(device) happi_name = device.md.name @@ -1001,7 +965,7 @@ def code_from_device(device): @contextlib.contextmanager def subscription_context(*objects, callback, event_type=None, run=True): - ''' + """ [Context manager] Subscribe to a specific event from all objects Unsubscribes all signals before exiting @@ -1017,15 +981,14 @@ def subscription_context(*objects, callback, event_type=None, run=True): The event type to subscribe to run : bool, optional Run the previously cached subscription immediately - ''' + """ obj_to_cid = {} try: for obj in objects: try: - obj_to_cid[obj] = obj.subscribe(callback, - event_type=event_type, run=run) + obj_to_cid[obj] = obj.subscribe(callback, event_type=event_type, run=run) except Exception: - logger.exception('Failed to subscribe to object %s', obj.name) + logger.exception("Failed to subscribe to object %s", obj.name) yield dict(obj_to_cid) finally: for obj, cid in obj_to_cid.items(): @@ -1038,7 +1001,7 @@ def subscription_context(*objects, callback, event_type=None, run=True): def get_all_signals_from_device(device, include_lazy=False, filter_by=None): - ''' + """ Get all signals in a given device Parameters @@ -1049,17 +1012,14 @@ def get_all_signals_from_device(device, include_lazy=False, filter_by=None): Include lazy signals as well filter_by : callable, optional Filter signals, with signature ``callable(ophyd.Device.ComponentWalk)`` - ''' + """ if not filter_by: + def filter_by(walk): return True def _get_signals(): - return [ - walk.item - for walk in device.walk_signals(include_lazy=include_lazy) - if filter_by(walk) - ] + return [walk.item for walk in device.walk_signals(include_lazy=include_lazy) if filter_by(walk)] if not include_lazy: return _get_signals() @@ -1069,9 +1029,8 @@ def _get_signals(): @contextlib.contextmanager -def subscription_context_device(device, callback, event_type=None, run=True, *, - include_lazy=False, filter_by=None): - ''' +def subscription_context_device(device, callback, event_type=None, run=True, *, include_lazy=False, filter_by=None): + """ [Context manager] Subscribe to ``event_type`` from signals in ``device`` Unsubscribes all signals before exiting @@ -1091,10 +1050,9 @@ def subscription_context_device(device, callback, event_type=None, run=True, *, Include lazy signals as well filter_by : callable, optional Filter signals, with signature ``callable(ophyd.Device.ComponentWalk)`` - ''' + """ signals = get_all_signals_from_device(device, include_lazy=include_lazy) - with subscription_context(*signals, callback=callback, - event_type=event_type, run=run) as obj_to_cid: + with subscription_context(*signals, callback=callback, event_type=event_type, run=run) as obj_to_cid: yield obj_to_cid @@ -1112,40 +1070,39 @@ def clear(self): self.remove_object(obj) def _run_callback_hack_on_object(self, obj): - ''' + """ HACK: peek into ophyd objects to see if they're connected but have never run metadata callbacks This is part of an ongoing ophyd issue and may be removed in the future. - ''' + """ if obj not in self.objects: return - if obj.connected and obj._args_cache.get('meta') is None: + if obj.connected and obj._args_cache.get("meta") is None: md = dict(obj.metadata) - if 'connected' not in md: - md['connected'] = True + if "connected" not in md: + md["connected"] = True self._connection_callback(obj=obj, **md) def add_object(self, obj): - 'Add an additional object to be monitored' + "Add an additional object to be monitored" with self.lock: if obj in self.objects: return self.objects.add(obj) try: - self.obj_to_cid[obj] = obj.subscribe( - self._connection_callback, event_type='meta', run=True) + self.obj_to_cid[obj] = obj.subscribe(self._connection_callback, event_type="meta", run=True) except Exception: - logger.exception('Failed to subscribe to object: %s', obj.name) + logger.exception("Failed to subscribe to object: %s", obj.name) self.objects.remove(obj) else: self._run_callback_hack_on_object(obj) def remove_object(self, obj): - 'Remove an object from being monitored - no more callbacks' + "Remove an object from being monitored - no more callbacks" with self.lock: if obj in self.connected: self.connected.remove(obj) @@ -1172,20 +1129,16 @@ def _connection_callback(self, *, obj, connected, **kwargs): else: return - logger.debug('Connection update: %r (obj=%s connected=%s kwargs=%r)', - self, obj.name, connected, kwargs) + logger.debug("Connection update: %r (obj=%s connected=%s kwargs=%r)", self, obj.name, connected, kwargs) self.callback(obj=obj, connected=connected, **kwargs) def __repr__(self): - return ( - f'<{self.__class__.__name__} connected={len(self.connected)} ' - f'objects={len(self.objects)}>' - ) + return f"<{self.__class__.__name__} connected={len(self.connected)} objects={len(self.objects)}>" @contextlib.contextmanager def connection_status_monitor(*signals, callback): - ''' + """ [Context manager] Monitor connection status from a number of signals Filters out any other metadata updates, only calling once @@ -1199,13 +1152,13 @@ def connection_status_monitor(*signals, callback): Callback to run, with same signature as that of :meth:`ophyd.OphydObj.subscribe`. ``obj`` and ``connected`` are guaranteed kwargs. - ''' + """ status = _ConnectionStatus(callback) - with subscription_context(*signals, callback=status._connection_callback, - event_type='meta', run=True - ) as status.obj_to_cid: + with subscription_context( + *signals, callback=status._connection_callback, event_type="meta", run=True + ) as status.obj_to_cid: for sig in signals: status._run_callback_hack_on_object(sig) @@ -1213,7 +1166,7 @@ def connection_status_monitor(*signals, callback): class DeviceConnectionMonitorThread(QtCore.QThread): - ''' + """ Monitor connection status in a background thread Parameters @@ -1229,7 +1182,7 @@ class DeviceConnectionMonitorThread(QtCore.QThread): Connection update signal with signature:: (signal, connected, metadata_dict) - ''' + """ connection_update = QtCore.Signal(object, bool, dict) @@ -1263,8 +1216,7 @@ def callback(self, obj, connected, **kwargs): self.connection_update.emit(obj, connected, kwargs) def run(self): - signals = get_all_signals_from_device( - self.device, include_lazy=self.include_lazy) + signals = get_all_signals_from_device(self.device, include_lazy=self.include_lazy) with connection_status_monitor(*signals, callback=self.callback): while not self.isInterruptionRequested(): @@ -1273,7 +1225,7 @@ def run(self): class ObjectConnectionMonitorThread(QtCore.QThread): - ''' + """ Monitor connection status in a background thread Attributes @@ -1282,7 +1234,7 @@ class ObjectConnectionMonitorThread(QtCore.QThread): Connection update signal with signature:: (signal, connected, metadata_dict) - ''' + """ connection_update = QtCore.Signal(object, bool, dict) @@ -1341,9 +1293,7 @@ def callback(self, obj, connected, **kwargs): def run(self): self.lock.acquire() try: - with connection_status_monitor( - *self._init_objects, - callback=self.callback) as self.status: + with connection_status_monitor(*self._init_objects, callback=self.callback) as self.status: self._init_objects.clear() self.lock.release() while not self.isInterruptionRequested(): @@ -1355,7 +1305,7 @@ def run(self): class ThreadPoolWorker(QtCore.QRunnable): - ''' + """ Worker thread helper Parameters @@ -1366,7 +1316,7 @@ class ThreadPoolWorker(QtCore.QRunnable): Arguments for the function call **kwargs Keyword rarguments for the function call - ''' + """ def __init__(self, func, *args, **kwargs): super().__init__() @@ -1379,8 +1329,7 @@ def run(self): try: self.func(*self.args, **self.kwargs) except Exception: - logger.exception('Failed to run %s(*%s, **%r) in thread pool', - self.func, self.args, self.kwargs) + logger.exception("Failed to run %s(*%s, **%r) in thread pool", self.func, self.args, self.kwargs) def _get_top_level_components(device_cls): @@ -1444,29 +1393,29 @@ def dump_grid_layout(layout, rows=None, cols=None, *, cell_width=60): rows = rows or layout.rowCount() cols = cols or layout.columnCount() - separator = '-' * ((cell_width + 4) * cols) - cell = ' {:<%ds}' % cell_width + separator = "-" * ((cell_width + 4) * cols) + cell = " {:<%ds}" % cell_width def get_text(item): if not item: - return '' + return "" entry = item.widget() or item.layout() visible = entry is None or entry.isVisible() if isinstance(entry, QtWidgets.QLabel): - entry = f'' + entry = f"" if not visible: - entry = f'(invis) {entry}' + entry = f"(invis) {entry}" return entry with io.StringIO() as file: print(separator, file=file) for row in range(rows): - print('|', end='', file=file) + print("|", end="", file=file) for col in range(cols): item = get_text(layout.itemAtPosition(row, col)) - print(cell.format(str(item)), end=' |', file=file) + print(cell.format(str(item)), end=" |", file=file) print(file=file) @@ -1518,7 +1467,7 @@ def get_variety_metadata(cpt): if not isinstance(cpt, ophyd.Component): cpt = get_component(cpt) - return getattr(cpt, '_variety_metadata', {}) + return getattr(cpt, "_variety_metadata", {}) def widget_to_image(widget, fill_color=QtCore.Qt.transparent): @@ -1530,8 +1479,7 @@ def widget_to_image(widget, fill_color=QtCore.Qt.transparent): QtGui.QImage The display, as an image. """ - image = QtGui.QImage(widget.width(), widget.height(), - QtGui.QImage.Format_ARGB32_Premultiplied) + image = QtGui.QImage(widget.width(), widget.height(), QtGui.QImage.Format_ARGB32_Premultiplied) image.fill(fill_color) pixmap = QtGui.QPixmap(image) @@ -1567,7 +1515,7 @@ def connect_slots_patch(top_level_widget): "downgrading Python or upgrading pyqt5 to >= 5.13.1. " "For further discussion, see " "https://github.com/pcdshub/typhos/issues/354", - exc_info=ex + exc_info=ex, ) QtCore.QMetaObject.connectSlotsByName = connect_slots_patch @@ -1637,12 +1585,11 @@ def wrapped(self): link_signal_to_widget(signal, widget) except Exception: logger.exception( - 'device.%s => self.%s (signal: %s widget: %s)', - device_attr, widget_attr, signal, widget) + "device.%s => self.%s (signal: %s widget: %s)", device_attr, widget_attr, signal, widget + ) signal = None else: - logger.debug('device.%s => self.%s (signal=%s widget=%s)', - device_attr, widget_attr, signal, widget) + logger.debug("device.%s => self.%s (signal=%s widget=%s)", device_attr, widget_attr, signal, widget) if signal is None and hide_unavailable: widget.setVisible(False) @@ -1650,6 +1597,7 @@ def wrapped(self): return func(self, signal, widget) return wrapped + return wrapper @@ -1679,6 +1627,7 @@ class FrameOnEditFilter(QtCore.QObject): This will make the QLineEdit look like a QLabel when the user is not editing it. """ + def eventFilter(self, object: QtWidgets.QLineEdit, event: QtCore.QEvent) -> bool: # Even if we install only on line edits, this can be passed a generic # QWidget when we remove and clean up the line edit widget. @@ -1704,8 +1653,7 @@ def set_edit_style(object: QtWidgets.QLineEdit): object.setFrame(True) color = object.palette().color(QtGui.QPalette.ColorRole.Base) object.setStyleSheet( - f"QLineEdit {{ background: rgba({color.red()}," - f"{color.green()}, {color.blue()}, {color.alpha()})}}" + f"QLineEdit {{ background: rgba({color.red()},{color.green()}, {color.blue()}, {color.alpha()})}}" ) object.setReadOnly(False) @@ -1720,9 +1668,7 @@ def set_no_edit_style(object: QtWidgets.QLineEdit): """ if object.text(): object.setFrame(False) - object.setStyleSheet( - "QLineEdit { background: transparent }" - ) + object.setStyleSheet("QLineEdit { background: transparent }") object.setReadOnly(True) @@ -1738,11 +1684,7 @@ def take_widget_screenshot(widget: QtWidgets.QWidget) -> Optional[QtGui.QImage]: primary_screen: QtGui.QScreen = app.primaryScreen() logger.debug("Primary screen: %s", primary_screen) - screen = ( - widget.screen() - if hasattr(widget, "screen") - else primary_screen - ) + screen = widget.screen() if hasattr(widget, "screen") else primary_screen logger.info( "Screenshot: %s (%s, primary screen: %s widget screen: %s)", @@ -1760,10 +1702,9 @@ def take_widget_screenshot(widget: QtWidgets.QWidget) -> Optional[QtGui.QImage]: def take_top_level_widget_screenshots( - *, visible_only: bool = True, -) -> Generator[ - tuple[QtWidgets.QWidget, QtGui.QImage], None, None -]: + *, + visible_only: bool = True, +) -> Generator[tuple[QtWidgets.QWidget, QtGui.QImage], None, None]: """ Yield screenshots of all top-level widgets. diff --git a/typhos/variety.py b/typhos/variety.py index 2c4e5512..b67ed560 100644 --- a/typhos/variety.py +++ b/typhos/variety.py @@ -16,8 +16,7 @@ def _warn_unhandled(instance, metadata_key, value): return logger.debug( - '%s: Not yet implemented variety handling: key=%s value=%s', - instance.__class__.__name__, metadata_key, value + "%s: Not yet implemented variety handling: key=%s value=%s", instance.__class__.__name__, metadata_key, value ) @@ -38,7 +37,7 @@ def key_handler(key): def wrapper(method): assert callable(method) - if not hasattr(method, '_variety_handler'): + if not hasattr(method, "_variety_handler"): method._variety_handler_keys = set() method._variety_handler_keys.add(key) return method @@ -48,8 +47,8 @@ def wrapper(method): def _get_variety_handlers(members): handlers = {} - for attr, method in members: - for key in getattr(method, '_variety_handler_keys', []): + for _attr, method in members: + for key in getattr(method, "_variety_handler_keys", []): if key not in handlers: handlers[key] = [method] handlers[key].append(method) @@ -89,46 +88,46 @@ def use_for_variety(variety, *, read=True, write=True): """ known_varieties = { - 'array-histogram', - 'array-image', - 'array-nd', - 'array-tabular', - 'array-timeseries', - 'bitmask', - 'command', - 'command-enum', - 'command-proc', - 'command-setpoint-tracks-readback', - 'enum', - 'scalar', - 'scalar-range', - 'scalar-tweakable', - 'text', - 'text-enum', - 'text-multiline', + "array-histogram", + "array-image", + "array-nd", + "array-tabular", + "array-timeseries", + "bitmask", + "command", + "command-enum", + "command-proc", + "command-setpoint-tracks-readback", + "enum", + "scalar", + "scalar-range", + "scalar-tweakable", + "text", + "text-enum", + "text-multiline", } if variety not in known_varieties: # NOTE: not kept in sync with pcdsdevices; so this wrapper may need # updating. - raise ValueError(f'Not a known variety: {variety}') + raise ValueError(f"Not a known variety: {variety}") def wrapper(cls): if variety not in _variety_to_widget_class: _variety_to_widget_class[variety] = {} if read: - _variety_to_widget_class[variety]['read'] = cls + _variety_to_widget_class[variety]["read"] = cls if cls.__doc__ is not None: - cls.__doc__ += f'\n * Used for variety {variety} (readback)' + cls.__doc__ += f"\n * Used for variety {variety} (readback)" if write: - _variety_to_widget_class[variety]['write'] = cls + _variety_to_widget_class[variety]["write"] = cls if cls.__doc__ is not None: - cls.__doc__ += f'\n * Used for variety {variety} (setpoint)' + cls.__doc__ += f"\n * Used for variety {variety} (setpoint)" if not read and not write: - raise ValueError('`write` or `read` must be set.') + raise ValueError("`write` or `read` must be set.") return cls @@ -146,18 +145,16 @@ def use_for_variety_write(variety): def _get_widget_class_from_variety(desc, variety_md, read_only): - variety = variety_md['variety'] # a required key - read_key = 'read' if read_only else 'write' + variety = variety_md["variety"] # a required key + read_key = "read" if read_only else "write" try: widget_cls = _variety_to_widget_class[variety].get(read_key) except KeyError: - logger.error('Unsupported variety: %s (%s / %s)', - variety_md['variety'], desc, variety_md) + logger.error("Unsupported variety: %s (%s / %s)", variety_md["variety"], desc, variety_md) else: if widget_cls is None: # TODO: remove - logger.error('TODO no widget?: %s (%s / %s)', - variety_md['variety'], desc, variety_md) + logger.error("TODO no widget?: %s (%s / %s)", variety_md["variety"], desc, variety_md) return widget_cls @@ -173,17 +170,17 @@ def get_referenced_signal(widget, name_or_component): name_or_component : str or ophyd.Component The signal name or ophyd Component. """ - ophyd_signal = getattr(widget, 'ophyd_signal', None) + ophyd_signal = getattr(widget, "ophyd_signal", None) if ophyd_signal is None: - logger.error('Incorrectly configured widget (ophyd_signal unset?)') + logger.error("Incorrectly configured widget (ophyd_signal unset?)") return device = ophyd_signal.parent if device is None: - logger.debug('Cannot be used on isolated (non-Device) signal') + logger.debug("Cannot be used on isolated (non-Device) signal") return - if hasattr(name_or_component, 'attr'): + if hasattr(name_or_component, "attr"): name_or_component = name_or_component.attr return getattr(device, name_or_component) @@ -208,14 +205,15 @@ def fset(self, metadata): # Catch-all handler for variety metadata. try: - if hasattr(self, '_update_variety_metadata'): + if hasattr(self, "_update_variety_metadata"): self._update_variety_metadata(**self._variety_metadata) except Exception: - logger.exception('Failed to set variety metadata for class %s: %s', - type(self).__name__, self._variety_metadata) + logger.exception( + "Failed to set variety metadata for class %s: %s", type(self).__name__, self._variety_metadata + ) # Optionally, there may be 'handlers' for individual top-level keys. - handlers = getattr(self, '_variety_handlers', {}) + handlers = getattr(self, "_variety_handlers", {}) for key, handler_list in handlers.items(): for unbound in handler_list: handler = getattr(self, unbound.__name__) @@ -231,20 +229,21 @@ def fset(self, metadata): handler(info) except Exception: logger.exception( - 'Failed to set variety metadata for class %s.%s %r: ' - '%s', type(self).__name__, handler.__name__, key, info + "Failed to set variety metadata for class %s.%s %r: %s", + type(self).__name__, + handler.__name__, + key, + info, ) - return property(fget, fset, - doc='Additional component variety metadata.') + return property(fget, fset, doc="Additional component variety metadata.") def get_enum_strings(enum_strings, enum_dict): """Get enum strings from either `enum_strings` or `enum_dict`.""" if enum_dict: max_value = max(enum_dict) - return [enum_dict.get(idx, '') - for idx in range(max_value + 1)] + return [enum_dict.get(idx, "") for idx in range(max_value + 1)] return enum_strings @@ -252,5 +251,4 @@ def get_enum_strings(enum_strings, enum_dict): def get_display_format(value): """Get the display format enum value from the variety metadata value.""" if value is not None: - return getattr(DisplayFormat, value.capitalize(), - DisplayFormat.Default) + return getattr(DisplayFormat, value.capitalize(), DisplayFormat.Default) diff --git a/typhos/version.py b/typhos/version.py index da8a5d52..fa1e59be 100644 --- a/typhos/version.py +++ b/typhos/version.py @@ -22,6 +22,7 @@ class VersionProxy(UserString): 4. A fallback in case none of the above match - resulting in a version of 0.0.unknown """ + def __init__(self): self._version = None @@ -32,6 +33,7 @@ def _get_version(self) -> Optional[str]: try: # Git checkout from setuptools_scm import get_version + return get_version(root="..", relative_to=__file__) except (ImportError, LookupError): ... @@ -40,6 +42,7 @@ def _get_version(self) -> Optional[str]: # done a build at least once. try: from ._version import version # noqa: F401 + return version except ImportError: ... @@ -51,7 +54,7 @@ def data(self) -> str: # This is accessed by UserString to allow us to lazily fill in the # information if self._version is None: - self._version = self._get_version() or '0.0.unknown' + self._version = self._get_version() or "0.0.unknown" return self._version diff --git a/typhos/web.py b/typhos/web.py index 245338d4..abd05e96 100644 --- a/typhos/web.py +++ b/typhos/web.py @@ -11,10 +11,8 @@ # Skip if this fails, will not impact use of qt designer. try: # qtpy has some holes in its web engine support. Fall back to Qt5: - from PyQt5.QtWebEngineCore import (QWebEngineHttpRequest, - QWebEngineUrlRequestInterceptor) - from PyQt5.QtWebEngineWidgets import (QWebEnginePage, QWebEngineProfile, - QWebEngineView) + from PyQt5.QtWebEngineCore import QWebEngineHttpRequest, QWebEngineUrlRequestInterceptor + from PyQt5.QtWebEngineWidgets import QWebEnginePage, QWebEngineProfile, QWebEngineView except ImportError as ex: QWebEngineHttpRequest = None QWebEnginePage = None @@ -26,6 +24,7 @@ TyphosWebRequestInterceptor = None logger.warning("Unable to import %s; typhos web views disabled.", ex) else: + class TyphosWebRequestInterceptor(QWebEngineUrlRequestInterceptor): def interceptRequest(self, request_info): """ @@ -35,10 +34,7 @@ def interceptRequest(self, request_info): # request is QWebEngineUrlRequestInfo url = request_info.requestUrl().toString() add_headers = should_add_headers(url) - logger.debug( - "Help navigating to %s (add headers=%s)", - url, add_headers - ) + logger.debug("Help navigating to %s (add headers=%s)", url, add_headers) if add_headers: for header, value in utils.HELP_HEADERS.items(): request_info.setHttpHeader( @@ -47,9 +43,7 @@ def interceptRequest(self, request_info): ) class TyphosWebEnginePage(QWebEnginePage): - def javaScriptConsoleMessage( - self, level, message, line_number, source_id - ): + def javaScriptConsoleMessage(self, level, message, line_number, source_id): """ This hook redirects javascript console messages to Python logging. @@ -57,10 +51,7 @@ def javaScriptConsoleMessage( error, so with this hook we can at least optionally filter javascript log messages. """ - logger.debug( - "[WebEngine] level=%s %s:%s %s", - level, source_id, line_number, message - ) + logger.debug("[WebEngine] level=%s %s:%s %s", level, source_id, line_number, message) class TyphosWebEngineView(QWebEngineView): def __init__(self, parent=None): @@ -94,7 +85,4 @@ def should_add_headers(url): """ target_netloc = urllib.parse.urlparse(url).netloc configured_netloc = urllib.parse.urlparse(utils.HELP_URL).netloc - return ( - target_netloc == configured_netloc or - target_netloc in utils.HELP_HEADERS_HOSTS - ) + return target_netloc == configured_netloc or target_netloc in utils.HELP_HEADERS_HOSTS diff --git a/typhos/widgets.py b/typhos/widgets.py index 9ae27b07..ac313c5b 100644 --- a/typhos/widgets.py +++ b/typhos/widgets.py @@ -19,8 +19,7 @@ from pyqtgraph.parametertree import ParameterItem from qtpy import QtGui, QtWidgets from qtpy.QtCore import Property, QObject, QSize, Qt, Signal, Slot -from qtpy.QtWidgets import (QAction, QDialog, QDockWidget, QPushButton, - QToolBar, QVBoxLayout, QWidget) +from qtpy.QtWidgets import QAction, QDialog, QDockWidget, QPushButton, QToolBar, QVBoxLayout, QWidget from . import dynamic_font, plugins, utils, variety from .textedit import TyphosTextEdit # noqa: F401 @@ -29,14 +28,10 @@ logger = logging.getLogger(__name__) -EXPONENTIAL_UNITS = ['mtorr', 'torr', 'kpa', 'pa'] +EXPONENTIAL_UNITS = ["mtorr", "torr", "kpa", "pa"] -class SignalWidgetInfo( - collections.namedtuple( - 'SignalWidgetInfo', - 'read_cls read_kwargs write_cls write_kwargs' - )): +class SignalWidgetInfo(collections.namedtuple("SignalWidgetInfo", "read_cls read_kwargs write_cls write_kwargs")): """ Provides information on how to create signal widgets: class and kwargs. @@ -71,11 +66,9 @@ def from_signal(cls, obj, desc=None): if desc is None: desc = obj.describe() - read_cls, read_kwargs = widget_type_from_description( - obj, desc, read_only=True) + read_cls, read_kwargs = widget_type_from_description(obj, desc, read_only=True) - is_read_only = utils.is_signal_ro(obj) or ( - read_cls is not None and issubclass(read_cls, SignalDialogButton)) + is_read_only = utils.is_signal_ro(obj) or (read_cls is not None and issubclass(read_cls, SignalDialogButton)) if is_read_only: write_cls = None @@ -111,6 +104,7 @@ class TogglePanel(QWidget): contents : QWidget Widget whose visibility is controlled via the QPushButton """ + def __init__(self, title, parent=None): super().__init__(parent=parent) # Create Widget Infrastructure @@ -150,21 +144,19 @@ def show_contents(self, show): self.contents.hide() -@use_for_variety_write('enum') -@use_for_variety_write('text-enum') +@use_for_variety_write("enum") +@use_for_variety_write("text-enum") class TyphosComboBox(pydm.widgets.PyDMEnumComboBox): """ Notes ----- """ - def __init__(self, *args, variety_metadata=None, ophyd_signal=None, - **kwargs): + + def __init__(self, *args, variety_metadata=None, ophyd_signal=None, **kwargs): super().__init__(*args, **kwargs) self.ophyd_signal = ophyd_signal self._ophyd_enum_strings = None - self._md_sub = ophyd_signal.subscribe( - self._metadata_update, event_type="meta" - ) + self._md_sub = ophyd_signal.subscribe(self._metadata_update, event_type="meta") def __dtor__(self): """PyQt5 destructor hook.""" @@ -179,9 +171,7 @@ def _metadata_update(self, enum_strs=None, **kwargs): def enum_strings_changed(self, new_enum_strings): current_idx = self.currentIndex() - super().enum_strings_changed( - tuple(self._ophyd_enum_strings or new_enum_strings) - ) + super().enum_strings_changed(tuple(self._ophyd_enum_strings or new_enum_strings)) self.value_changed(current_idx) def wheelEvent(self, event: QtGui.QWheelEvent): @@ -192,12 +182,13 @@ class NoScrollComboBox(QtWidgets.QComboBox): """ A combobox disconnected from direct EPICS/ophyd with scrolling ignored. """ + def wheelEvent(self, event: QtGui.QWheelEvent): event.ignore() -@use_for_variety_write('scalar') -@use_for_variety_write('text') +@use_for_variety_write("scalar") +@use_for_variety_write("text") class TyphosLineEdit(pydm.widgets.PyDMLineEdit): """ Reimplementation of PyDMLineEdit to set some custom defaults @@ -205,11 +196,11 @@ class TyphosLineEdit(pydm.widgets.PyDMLineEdit): Notes ----- """ + def __init__(self, *args, display_format=None, **kwargs): self._channel = None self._setpoint_history_count = 5 - self._setpoint_history = collections.deque( - [], self._setpoint_history_count) + self._setpoint_history = collections.deque([], self._setpoint_history_count) super().__init__(*args, **kwargs) self.showUnits = True @@ -217,7 +208,11 @@ def __init__(self, *args, display_format=None, **kwargs): self.displayFormat = display_format def __dtor__(self): - menu = self.unitMenu + try: + menu = self.unitMenu + except AttributeError: + # It never got made for whatever reason, can leave + return if menu is not None: menu.deleteLater() self.unitMenu = None @@ -239,17 +234,14 @@ def setpointHistoryCount(self): @setpointHistoryCount.setter def setpointHistoryCount(self, value): self._setpoint_history_count = max((0, int(value))) - self._setpoint_history = collections.deque( - self._setpoint_history, self._setpoint_history_count) + self._setpoint_history = collections.deque(self._setpoint_history, self._setpoint_history_count) def _remove_history_item_by_value(self, remove_value): """ Remove an item from the history buffer by value """ - new_history = [(value, ts) for value, ts in self._setpoint_history - if value != remove_value] - self._setpoint_history = collections.deque( - new_history, self._setpoint_history_count) + new_history = [(value, ts) for value, ts in self._setpoint_history if value != remove_value] + self._setpoint_history = collections.deque(new_history, self._setpoint_history_count) def _add_history_item(self, value, *, timestamp=None): """ @@ -259,9 +251,7 @@ def _add_history_item(self, value, *, timestamp=None): # Push this value to the end of the list as most-recently used self._remove_history_item_by_value(value) - self._setpoint_history.append( - (value, timestamp or datetime.datetime.now()) - ) + self._setpoint_history.append((value, timestamp or datetime.datetime.now())) def send_value(self): """ @@ -280,17 +270,15 @@ def _create_history_menu(self): font = QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.FixedFont) history_menu.setFont(font) - max_len = max(len(value) - for value, timestamp in self._setpoint_history) + max_len = max(len(value) for value, timestamp in self._setpoint_history) # Pad values such that timestamp lines up: # (Value) @ (Timestamp) - action_format = '{value:<%d} @ {timestamp}' % (max_len + 1) + action_format = "{value:<%d} @ {timestamp}" % (max_len + 1) for value, timestamp in reversed(self._setpoint_history): - timestamp = timestamp.strftime('%m/%d %H:%M') - action = history_menu.addAction( - action_format.format(value=value, timestamp=timestamp)) + timestamp = timestamp.strftime("%m/%d %H:%M") + action = history_menu.addAction(action_format.format(value=value, timestamp=timestamp)) def history_selected(*, value=value): self.setText(str(value)) @@ -324,22 +312,22 @@ def unit_changed(self, new_unit): return super().unit_changed(new_unit) - default = (self.displayFormat == DisplayFormat.Default) + default = self.displayFormat == DisplayFormat.Default if new_unit.lower() in EXPONENTIAL_UNITS and default: self.displayFormat = DisplayFormat.Exponential -@use_for_variety_read('array-nd') -@use_for_variety_read('command-enum') -@use_for_variety_read('command-setpoint-tracks-readback') -@use_for_variety_read('enum') -@use_for_variety_read('scalar') -@use_for_variety_read('scalar-range') -@use_for_variety_read('scalar-tweakable') -@use_for_variety_read('text') -@use_for_variety_read('text-enum') -@use_for_variety_read('text-multiline') -@use_for_variety_write('array-nd') +@use_for_variety_read("array-nd") +@use_for_variety_read("command-enum") +@use_for_variety_read("command-setpoint-tracks-readback") +@use_for_variety_read("enum") +@use_for_variety_read("scalar") +@use_for_variety_read("scalar-range") +@use_for_variety_read("scalar-tweakable") +@use_for_variety_read("text") +@use_for_variety_read("text-enum") +@use_for_variety_read("text-multiline") +@use_for_variety_write("array-nd") class TyphosLabel(pydm.widgets.PyDMLabel): """ Reimplementation of PyDMLabel to set some custom defaults @@ -347,22 +335,18 @@ class TyphosLabel(pydm.widgets.PyDMLabel): Notes ----- """ - def __init__( - self, *args, display_format=None, ophyd_signal=None, **kwargs - ): + + def __init__(self, *args, display_format=None, ophyd_signal=None, **kwargs): super().__init__(*args, **kwargs) self.setAlignment(Qt.AlignCenter) - self.setSizePolicy(QtWidgets.QSizePolicy.Preferred, - QtWidgets.QSizePolicy.Maximum) + self.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Maximum) self.showUnits = True if display_format is not None: self.displayFormat = display_format self.ophyd_signal = ophyd_signal self._ophyd_enum_strings = None - self._md_sub = ophyd_signal.subscribe( - self._metadata_update, event_type="meta" - ) + self._md_sub = ophyd_signal.subscribe(self._metadata_update, event_type="meta") def __dtor__(self): """PyQt5 destructor hook.""" @@ -376,9 +360,7 @@ def _metadata_update(self, enum_strs=None, **kwargs): self.enum_strings_changed(enum_strs) def enum_strings_changed(self, new_enum_strings): - super().enum_strings_changed( - tuple(self._ophyd_enum_strings or new_enum_strings) - ) + super().enum_strings_changed(tuple(self._ophyd_enum_strings or new_enum_strings)) def unit_changed(self, new_unit): """ @@ -395,7 +377,7 @@ def unit_changed(self, new_unit): return super().unit_changed(new_unit) - default = (self.displayFormat == DisplayFormat.Default) + default = self.displayFormat == DisplayFormat.Default if new_unit.lower() in EXPONENTIAL_UNITS and default: self.displayFormat = DisplayFormat.Exponential @@ -419,6 +401,7 @@ class TyphosSidebarItem(ParameterItem): Notes ----- """ + def __init__(self, param, depth): super().__init__(param, depth) # Configure a QToolbar @@ -426,16 +409,13 @@ def __init__(self, param, depth): self.toolbar.setToolButtonStyle(Qt.ToolButtonIconOnly) self.toolbar.setIconSize(QSize(15, 15)) # Setup the action to open the widget - self.open_action = QAction( - qta.icon('fa5s.square', color='green'), 'Open', self.toolbar) + self.open_action = QAction(qta.icon("fa5s.square", color="green"), "Open", self.toolbar) self.open_action.triggered.connect(self.open_requested) # Setup the action to embed the widget - self.embed_action = QAction( - qta.icon('fa5s.th-large', color='yellow'), 'Embed', self.toolbar) + self.embed_action = QAction(qta.icon("fa5s.th-large", color="yellow"), "Embed", self.toolbar) self.embed_action.triggered.connect(self.embed_requested) # Setup the action to hide the widget - self.hide_action = QAction( - qta.icon('fa5s.times-circle', color='red'), 'Close', self.toolbar) + self.hide_action = QAction(qta.icon("fa5s.times-circle", color="red"), "Close", self.toolbar) self.hide_action.triggered.connect(self.hide_requested) self.hide_action.setEnabled(False) # Add actions to toolbars @@ -480,6 +460,7 @@ def treeWidgetChanged(self): class SubDisplay(QDockWidget): """QDockWidget modified to emit a signal when closed""" + closing = Signal() def closeEvent(self, evt): @@ -508,12 +489,11 @@ def __init__(self, *, tx_slot, **kwargs): def tx_slot(self, value): """Transmission Slot""" # Do not fire twice for the same device - if not self._last_md or self._last_md != value['md']: - self._last_md = value['md'] + if not self._last_md or self._last_md != value["md"]: + self._last_md = value["md"] self._tx_slot(value) else: - logger.debug("HappiChannel %r received same device. " - "Ignoring for now ...", self) + logger.debug("HappiChannel %r received same device. Ignoring for now ...", self) class TyphosDesignerMixin(pydm.widgets.base.PyDMWidget): @@ -547,25 +527,25 @@ def channel(self, value): if self._channels: self._channels.clear() for channel in self._channels: - if hasattr(channel, 'disconnect'): + if hasattr(channel, "disconnect"): channel.disconnect() # Load new channel self._channel = str(value) - channel = HappiChannel(address=self._channel, - tx_slot=self._tx) + channel = HappiChannel(address=self._channel, tx_slot=self._tx) self._channels = [channel] # Connect the channel to the HappiPlugin - if hasattr(channel, 'connect'): + if hasattr(channel, "connect"): channel.connect() @Slot(object) def _tx(self, value): """Receive information from happi channel""" - self.add_device(value['obj']) + self.add_device(value["obj"]) class SignalDialogButton(QPushButton): """QPushButton to launch a QDialog with a PyDMWidget""" + text = NotImplemented icon = NotImplemented parent_widget_class = QtWidgets.QWidget @@ -589,8 +569,7 @@ def show_dialog(self) -> QDialog: if not self.dialog: logger.debug("Creating QDialog for %r", self.channel) # Set up the QDialog - parent = utils.find_parent_with_class( - self, self.parent_widget_class) + parent = utils.find_parent_with_class(self, self.parent_widget_class) self.dialog = QDialog(parent) self.dialog.setWindowTitle(self.channel) self.dialog.setLayout(QVBoxLayout()) @@ -608,7 +587,7 @@ def show_dialog(self) -> QDialog: return self.dialog -@use_for_variety_read('array-image') +@use_for_variety_read("array-image") class ImageDialogButton(SignalDialogButton): """ QPushButton to show a 2-d array. @@ -616,18 +595,18 @@ class ImageDialogButton(SignalDialogButton): Notes ----- """ + text = "Show Image" icon = "fa5s.camera" parent_widget_class = QtWidgets.QMainWindow def widget(self): """Create PyDMImageView""" - return pydm.widgets.PyDMImageView( - parent=self, image_channel=self.channel) + return pydm.widgets.PyDMImageView(parent=self, image_channel=self.channel) -@use_for_variety_read('array-timeseries') -@use_for_variety_read('array-histogram') # TODO: histogram settings? +@use_for_variety_read("array-timeseries") +@use_for_variety_read("array-histogram") # TODO: histogram settings? class WaveformDialogButton(SignalDialogButton): """ QPushButton to show a 1-d array. @@ -635,20 +614,20 @@ class WaveformDialogButton(SignalDialogButton): Notes ----- """ - text = 'Show Waveform' + + text = "Show Waveform" icon = "fa5s.chart-line" parent_widget_class = QtWidgets.QMainWindow def widget(self): """Create PyDMWaveformPlot""" - return pydm.widgets.PyDMWaveformPlot( - init_y_channels=[self.channel], parent=self) + return pydm.widgets.PyDMWaveformPlot(init_y_channels=[self.channel], parent=self) # @variety.uses_key_handlers -@use_for_variety_write('command') -@use_for_variety_write('command-proc') -@use_for_variety_write('command-setpoint-tracks-readback') # TODO +@use_for_variety_write("command") +@use_for_variety_write("command-proc") +@use_for_variety_write("command-setpoint-tracks-readback") # TODO class TyphosCommandButton(pydm.widgets.PyDMPushButton): """ A pushbutton widget which executes a command by sending a specific value. @@ -661,10 +640,9 @@ class TyphosCommandButton(pydm.widgets.PyDMPushButton): ----- """ - default_label = 'Command' + default_label = "Command" - def __init__(self, *args, variety_metadata=None, ophyd_signal=None, - **kwargs): + def __init__(self, *args, variety_metadata=None, ophyd_signal=None, **kwargs): super().__init__(*args, **kwargs) self.ophyd_signal = ophyd_signal self.variety_metadata = variety_metadata @@ -673,11 +651,9 @@ def __init__(self, *args, variety_metadata=None, ophyd_signal=None, variety_metadata = variety.create_variety_property() def enum_strings_changed(self, new_enum_strings): - return super().enum_strings_changed( - self._forced_enum_strings or new_enum_strings) + return super().enum_strings_changed(self._forced_enum_strings or new_enum_strings) - def _update_variety_metadata(self, *, value, enum_strings=None, - enum_dict=None, tags=None, **kwargs): + def _update_variety_metadata(self, *, value, enum_strings=None, enum_dict=None, tags=None, **kwargs): self.pressValue = value enum_strings = variety.get_enum_strings(enum_strings, enum_dict) if enum_strings is not None: @@ -686,11 +662,11 @@ def _update_variety_metadata(self, *, value, enum_strings=None, tags = set(tags or {}) - if 'protected' in tags: + if "protected" in tags: self.passwordProtected = True - self.password = 'typhos' # ... yeah (TODO) + self.password = "typhos" # ... yeah (TODO) - if 'confirm' in tags: + if "confirm" in tags: self.showConfirmDialog = True variety._warn_unhandled_kwargs(self, kwargs) @@ -700,7 +676,7 @@ def _update_variety_metadata(self, *, value, enum_strings=None, @variety.uses_key_handlers -@use_for_variety_write('command-enum') +@use_for_variety_write("command-enum") class TyphosCommandEnumButton(pydm.widgets.enum_button.PyDMEnumButton): """ A group of buttons which represent several command options. @@ -716,8 +692,7 @@ class TyphosCommandEnumButton(pydm.widgets.enum_button.PyDMEnumButton): ----- """ - def __init__(self, *args, variety_metadata=None, ophyd_signal=None, - **kwargs): + def __init__(self, *args, variety_metadata=None, ophyd_signal=None, **kwargs): super().__init__(*args, **kwargs) self.ophyd_signal = ophyd_signal self.variety_metadata = variety_metadata @@ -726,11 +701,9 @@ def __init__(self, *args, variety_metadata=None, ophyd_signal=None, variety_metadata = variety.create_variety_property() def enum_strings_changed(self, new_enum_strings): - return super().enum_strings_changed( - self._forced_enum_strings or new_enum_strings) + return super().enum_strings_changed(self._forced_enum_strings or new_enum_strings) - def _update_variety_metadata(self, *, value, enum_strings=None, - enum_dict=None, tags=None, **kwargs): + def _update_variety_metadata(self, *, value, enum_strings=None, enum_dict=None, tags=None, **kwargs): enum_strings = variety.get_enum_strings(enum_strings, enum_dict) if enum_strings is not None: self._forced_enum_strings = tuple(enum_strings) @@ -739,7 +712,7 @@ def _update_variety_metadata(self, *, value, enum_strings=None, variety._warn_unhandled_kwargs(self, kwargs) -@use_for_variety_read('bitmask') +@use_for_variety_read("bitmask") @variety.uses_key_handlers class TyphosByteIndicator(pydm.widgets.PyDMByteIndicator): """ @@ -749,31 +722,28 @@ class TyphosByteIndicator(pydm.widgets.PyDMByteIndicator): ----- """ - def __init__(self, *args, variety_metadata=None, ophyd_signal=None, - **kwargs): + def __init__(self, *args, variety_metadata=None, ophyd_signal=None, **kwargs): super().__init__(*args, **kwargs) self.ophyd_signal = ophyd_signal self.variety_metadata = variety_metadata variety_metadata = variety.create_variety_property() - def _update_variety_metadata(self, *, bits, orientation, first_bit, style, - meaning=None, tags=None, **kwargs): + def _update_variety_metadata(self, *, bits, orientation, first_bit, style, meaning=None, tags=None, **kwargs): self.numBits = bits self.orientation = { - 'horizontal': Qt.Horizontal, - 'vertical': Qt.Vertical, + "horizontal": Qt.Horizontal, + "vertical": Qt.Vertical, }[orientation] - self.bigEndian = (first_bit == 'most-significant') + self.bigEndian = first_bit == "most-significant" # TODO: labels do not display properly # if meaning: # self.labels = meaning[:bits] # self.showLabels = True variety._warn_unhandled_kwargs(self, kwargs) - @variety.key_handler('style') - def _variety_key_handler_style(self, *, shape, on_color, off_color, - **kwargs): + @variety.key_handler("style") + def _variety_key_handler_style(self, *, shape, on_color, off_color, **kwargs): """Variety hook for the sub-dictionary "style".""" on_color = QtGui.QColor(on_color) if on_color is not None: @@ -783,13 +753,13 @@ def _variety_key_handler_style(self, *, shape, on_color, off_color, if off_color is not None: self.offColor = off_color - self.circles = (shape == 'circle') + self.circles = shape == "circle" variety._warn_unhandled_kwargs(self, kwargs) -@use_for_variety_read('command') -@use_for_variety_read('command-proc') +@use_for_variety_read("command") +@use_for_variety_read("command-proc") class TyphosCommandIndicator(pydm.widgets.PyDMByteIndicator): """Displays command status as a read-only bit indicator.""" @@ -803,6 +773,7 @@ def __init__(self, *args, ophyd_signal=None, **kwargs): class ClickableBitIndicator(pydm.widgets.byte.PyDMBitIndicator): """A bit indicator that emits `clicked` when clicked.""" + clicked = Signal() def mousePressEvent(self, event: QtGui.QMouseEvent): @@ -811,9 +782,8 @@ def mousePressEvent(self, event: QtGui.QMouseEvent): self.clicked.emit() -@use_for_variety_write('bitmask') -class TyphosByteSetpoint(TyphosByteIndicator, - pydm.widgets.base.PyDMWritableWidget): +@use_for_variety_write("bitmask") +class TyphosByteSetpoint(TyphosByteIndicator, pydm.widgets.base.PyDMWritableWidget): """ Displays an integer value as individual, toggleable bit indicators. @@ -821,11 +791,9 @@ class TyphosByteSetpoint(TyphosByteIndicator, ----- """ - def __init__(self, *args, variety_metadata=None, ophyd_signal=None, - **kwargs): + def __init__(self, *args, variety_metadata=None, ophyd_signal=None, **kwargs): # NOTE: need to have these in the signature explicitly - super().__init__(*args, variety_metadata=variety_metadata, - ophyd_signal=ophyd_signal, **kwargs) + super().__init__(*args, variety_metadata=variety_metadata, ophyd_signal=ophyd_signal, **kwargs) self._requests_pending = {} @@ -876,24 +844,21 @@ def numBits(self, num_bits): for indicator in self._indicators: indicator.deleteLater() - self._indicators = [ - ClickableBitIndicator(parent=self, circle=self.circles) - for bit in range(self._num_bits) - ] + self._indicators = [ClickableBitIndicator(parent=self, circle=self.circles) for bit in range(self._num_bits)] for bit, indicator in enumerate(self._indicators): + def indicator_clicked(*, bit=bit): self._bit_clicked(bit) indicator.clicked.connect(indicator_clicked) - new_labels = [f"Bit {bit}" - for bit in range(len(self.labels), self._num_bits)] + new_labels = [f"Bit {bit}" for bit in range(len(self.labels), self._num_bits)] self.labels = self.labels + new_labels @variety.uses_key_handlers -@use_for_variety_write('scalar-range') +@use_for_variety_write("scalar-range") class TyphosScalarRange(pydm.widgets.PyDMSlider): """ A slider widget which displays a scalar value with an explicit range. @@ -902,8 +867,7 @@ class TyphosScalarRange(pydm.widgets.PyDMSlider): ----- """ - def __init__(self, *args, variety_metadata=None, ophyd_signal=None, - **kwargs): + def __init__(self, *args, variety_metadata=None, ophyd_signal=None, **kwargs): super().__init__(*args, **kwargs) self.ophyd_signal = ophyd_signal self._delta_value = None @@ -922,10 +886,10 @@ def __dtor__(self): self._delta_signal_sub = None self.delta_signal = None - @variety.key_handler('range') + @variety.key_handler("range") def _variety_key_handler_range(self, value, source, **kwargs): """Variety hook for the sub-dictionary "range".""" - if source == 'value': + if source == "value": if value is not None: low, high = value self.userMinimum = low @@ -933,27 +897,26 @@ def _variety_key_handler_range(self, value, source, **kwargs): self.userDefinedLimits = True # elif source == 'use_limits': else: - variety._warn_unhandled(self, 'range.source', source) + variety._warn_unhandled(self, "range.source", source) variety._warn_unhandled_kwargs(self, kwargs) - @variety.key_handler('delta') - def _variety_key_handler_delta(self, source, value=None, signal=None, - **kwargs): + @variety.key_handler("delta") + def _variety_key_handler_delta(self, source, value=None, signal=None, **kwargs): """Variety hook for the sub-dictionary "delta".""" - if source == 'value': + if source == "value": if value is not None: self.delta_value = value - elif source == 'signal': + elif source == "signal": if signal is not None: self.delta_signal = variety.get_referenced_signal(self, signal) else: - variety._warn_unhandled(self, 'delta.source', source) + variety._warn_unhandled(self, "delta.source", source) # range_ = kwargs.pop('range') # unhandled variety._warn_unhandled_kwargs(self, kwargs) - @variety.key_handler('display_format') + @variety.key_handler("display_format") def _variety_key_handler_display_format(self, value): """Variety hook for the sub-dictionary "delta".""" self.displayFormat = variety.get_display_format(value) @@ -1002,9 +965,9 @@ def delta_value(self, value): try: self.num_steps = (self.maximum - self.minimum) / value except Exception: - logger.exception('Failed to set number of steps with ' - 'min=%s, max=%s, delta=%s', self.minimum, - self.maximum, value) + logger.exception( + "Failed to set number of steps with min=%s, max=%s, delta=%s", self.minimum, self.maximum, value + ) finally: self._mute_internal_slider_changes = False @@ -1016,7 +979,7 @@ def connection_changed(self, connected): @variety.uses_key_handlers -@use_for_variety_write('array-tabular') +@use_for_variety_write("array-tabular") class TyphosArrayTable(pydm.widgets.PyDMWaveformTable): """ A table widget which reshapes and displays a given waveform value. @@ -1025,8 +988,7 @@ class TyphosArrayTable(pydm.widgets.PyDMWaveformTable): ----- """ - def __init__(self, *args, variety_metadata=None, ophyd_signal=None, - **kwargs): + def __init__(self, *args, variety_metadata=None, ophyd_signal=None, **kwargs): super().__init__(*args, **kwargs) self.ophyd_signal = ophyd_signal self.variety_metadata = variety_metadata @@ -1037,7 +999,7 @@ def value_changed(self, value): try: len(value) except TypeError: - logger.debug('Non-waveform value? %r', value) + logger.debug("Non-waveform value? %r", value) return # shape = self.variety_metadata.get('shape') @@ -1066,8 +1028,8 @@ def _update_variety_metadata(self, *, shape=None, tags=None, **kwargs): rows = max(np.product(shape[1:]), 1) self.setRowCount(rows) self.setColumnCount(columns) - self.rowHeaderLabels = [f'{idx}' for idx in range(rows)] - self.columnHeaderLabels = [f'{idx}' for idx in range(columns)] + self.rowHeaderLabels = [f"{idx}" for idx in range(rows)] + self.columnHeaderLabels = [f"{idx}" for idx in range(columns)] if rows <= 5 and columns <= 5: full_size = self._calculate_size() @@ -1095,7 +1057,7 @@ def _get_scalar_widget_class(desc, variety_md, read_only): if read_only: return TyphosLabel - if 'enum_strs' in desc: + if "enum_strs" in desc: # Create a QCombobox if the widget has enum_strs return TyphosComboBox @@ -1124,10 +1086,7 @@ def _get_ndimensional_widget_class(dimensions, desc, variety_md, read_only): if dimensions == 0: return _get_scalar_widget_class(desc, variety_md, read_only) - return { - 1: WaveformDialogButton, - 2: ImageDialogButton - }.get(dimensions, TyphosLabel) + return {1: WaveformDialogButton, 2: ImageDialogButton}.get(dimensions, TyphosLabel) DIRECT_CONTROL_LAYERS = {"pyepics", "caproto"} @@ -1157,51 +1116,48 @@ def widget_type_from_description(signal, desc, read_only=False): """ use_pv_directly = ( # We can use PyDM's data source directly with EpicsSignalBase: - isinstance(signal, EpicsSignalBase) and + isinstance(signal, EpicsSignalBase) + and # So long as its underlying control layer is a supported ophyd-provided # one, at least. getattr(signal.cl, "name", "") in DIRECT_CONTROL_LAYERS ) if use_pv_directly: # Still re-route EpicsSignal through the ca:// plugin - pv = (signal._read_pv - if read_only else signal._write_pv) + pv = signal._read_pv if read_only else signal._write_pv init_channel = utils.channel_name(pv.pvname) else: # Register signal with plugin plugins.register_signal(signal) - init_channel = utils.channel_name(signal.name, protocol='sig') + init_channel = utils.channel_name(signal.name, protocol="sig") variety_metadata = utils.get_variety_metadata(signal) kwargs = { - 'init_channel': init_channel, + "init_channel": init_channel, } if variety_metadata: - widget_cls = variety._get_widget_class_from_variety( - desc, variety_metadata, read_only) + widget_cls = variety._get_widget_class_from_variety(desc, variety_metadata, read_only) else: try: - dimensions = len(desc.get('shape', [])) + dimensions = len(desc.get("shape", [])) except TypeError: dimensions = 0 - widget_cls = _get_ndimensional_widget_class( - dimensions, desc, variety_metadata, read_only) + widget_cls = _get_ndimensional_widget_class(dimensions, desc, variety_metadata, read_only) if widget_cls is None: return None, None - if desc.get('dtype') == 'string' and widget_cls in (TyphosLabel, - TyphosLineEdit): - kwargs['display_format'] = DisplayFormat.String + if desc.get("dtype") == "string" and widget_cls in (TyphosLabel, TyphosLineEdit): + kwargs["display_format"] = DisplayFormat.String class_signature = inspect.signature(widget_cls) - if 'variety_metadata' in class_signature.parameters: - kwargs['variety_metadata'] = variety_metadata + if "variety_metadata" in class_signature.parameters: + kwargs["variety_metadata"] = variety_metadata - if 'ophyd_signal' in class_signature.parameters: - kwargs['ophyd_signal'] = signal + if "ophyd_signal" in class_signature.parameters: + kwargs["ophyd_signal"] = signal return widget_cls, kwargs @@ -1228,8 +1184,7 @@ def determine_widget_type(signal, read_only=False): try: desc = signal.describe()[signal.name] except Exception: - logger.error("Unable to connect to %r during widget creation", - signal.name) + logger.error("Unable to connect to %r during widget creation", signal.name) desc = {} return widget_type_from_description(signal, desc, read_only=read_only) @@ -1264,7 +1219,7 @@ def create_signal_widget(signal, read_only=False, tooltip=None): logger.debug("Creating %s for %s", widget_cls, signal.name) widget = widget_cls(**kwargs) - widget.setObjectName(f'{signal.name}_{widget_cls.__name__}') + widget.setObjectName(f"{signal.name}_{widget_cls.__name__}") if tooltip is not None: widget.setToolTip(tooltip) diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000..90a51fb3 --- /dev/null +++ b/uv.lock @@ -0,0 +1,2182 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[options] +exclude-newer = "2026-04-22T17:16:05.905390161Z" +exclude-newer-span = "P7D" + +[[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 = "asttokens" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[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 = "bluesky" +version = "1.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cycler" }, + { name = "event-model" }, + { name = "historydict" }, + { name = "msgpack" }, + { name = "msgpack-numpy" }, + { name = "numpy" }, + { name = "opentelemetry-api" }, + { name = "toolz" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a4/51/8fca065db911517add90c408760e3f209762d656f4d1d114172287874619/bluesky-1.15.0.tar.gz", hash = "sha256:71d35f3514e616e7fed0430327cc64d2d40e4fcef9d6764be4db8f621057f572", size = 507145, upload-time = "2026-04-15T15:39:23.557Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/bf/86fa0ca9d3ecf02ff9941dfa74d1038ccc34c2081ac780f7fe10cdb6ab39/bluesky-1.15.0-py3-none-any.whl", hash = "sha256:3fa8bbe0d069decbabaafb522db60418a2de0fb7450ad460dd3d6bcc7aed6d0e", size = 365347, upload-time = "2026-04-15T15:39:21.926Z" }, +] + +[[package]] +name = "caproto" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/0a/6d6cb9d632b283e6215da216d5987763c043fe2a8d9c48a3ea7ab91b77b4/caproto-1.3.0.tar.gz", hash = "sha256:ea74f433297e895695c80a706a2389f745a94d756b5835c80af73af3df585cba", size = 452074, upload-time = "2025-09-03T21:02:00.662Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/bd/4d1f59c9287ec5f93f9d879db3ac06785ba7c4d04a7120678d894e0c53d0/caproto-1.3.0-py3-none-any.whl", hash = "sha256:fa623c4a7d7c3537fc41ce023b7c72922b8819ab88bf9abc527e3ac594634180", size = 407021, upload-time = "2025-09-03T21:01:59.169Z" }, +] + +[[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/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 = "click" +version = "8.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856, upload-time = "2026-04-03T19:14:45.118Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379, upload-time = "2026-04-03T19:14:43.505Z" }, +] + +[[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 = "coloredlogs" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "humanfriendly" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/c7/eed8f27100517e8c0e6b923d5f0845d0cb99763da6fdee00478f91db7325/coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0", size = 278520, upload-time = "2021-06-11T10:22:45.202Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", size = 46018, upload-time = "2021-06-11T10:22:42.561Z" }, +] + +[[package]] +name = "coverage" +version = "7.13.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554, upload-time = "2026-03-17T10:30:42.208Z" }, + { url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" }, + { url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419, upload-time = "2026-03-17T10:30:45.545Z" }, + { url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" }, + { url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376", size = 255270, upload-time = "2026-03-17T10:30:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538, upload-time = "2026-03-17T10:30:50.77Z" }, + { url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821, upload-time = "2026-03-17T10:30:52.5Z" }, + { url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191, upload-time = "2026-03-17T10:30:54.543Z" }, + { url = "https://files.pythonhosted.org/packages/70/ee/fe1621488e2e0a58d7e94c4800f0d96f79671553488d401a612bebae324b/coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09", size = 251337, upload-time = "2026-03-17T10:30:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404, upload-time = "2026-03-17T10:30:58.427Z" }, + { url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903, upload-time = "2026-03-17T10:31:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780, upload-time = "2026-03-17T10:31:01.916Z" }, + { url = "https://files.pythonhosted.org/packages/a4/d7/0ad9b15812d81272db94379fe4c6df8fd17781cc7671fdfa30c76ba5ff7b/coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf", size = 222093, upload-time = "2026-03-17T10:31:03.642Z" }, + { url = "https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810", size = 222900, upload-time = "2026-03-17T10:31:05.651Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/2238c2ad08e35cf4f020ea721f717e09ec3152aea75d191a7faf3ef009a8/coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de", size = 221515, upload-time = "2026-03-17T10:31:07.293Z" }, + { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" }, + { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" }, + { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" }, + { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" }, + { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" }, + { url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" }, + { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" }, + { url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" }, + { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" }, + { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" }, + { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" }, + { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" }, + { url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" }, + { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" }, + { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" }, + { url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" }, + { url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" }, + { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" }, + { url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" }, + { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" }, + { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" }, + { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" }, + { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" }, + { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" }, + { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" }, + { url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" }, + { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" }, + { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" }, + { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" }, + { url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" }, + { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" }, + { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" }, + { url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" }, + { url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + +[[package]] +name = "decorator" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, +] + +[[package]] +name = "docs-versions-menu" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pyparsing" }, + { name = "setuptools" }, + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/b7/33bb6d00593ce0a7df5d4b13871b798d1cc8fd798933f036b058568dbe2d/docs_versions_menu-0.5.2.tar.gz", hash = "sha256:8c6eae5836fb63e4f9700387385c5074dac8187609538422dab5fa39de110a73", size = 782903, upload-time = "2023-04-20T03:41:47.03Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/23/89993b2e9895add5e76a3f38ceb3c06dc2f1d0733937568085a36a56b21b/docs_versions_menu-0.5.2-py3-none-any.whl", hash = "sha256:8e331e2e9b2c9d3a7b2a7c8325a7e7ed7070b144cd3ef0f701172468af7e3cdf", size = 533159, upload-time = "2023-04-20T03:41:45.019Z" }, +] + +[[package]] +name = "docutils" +version = "0.22.4" +source = { registry = "https://pypi.org/simple" } +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 = "entrypoints" +version = "0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/8d/a7121ffe5f402dc015277d2d31eb82d2187334503a011c18f2e78ecbb9b2/entrypoints-0.4.tar.gz", hash = "sha256:b706eddaa9218a19ebcd67b56818f05bb27589b1ca9e8d797b74affad4ccacd4", size = 13974, upload-time = "2022-02-02T21:30:28.172Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/a8/365059bbcd4572cbc41de17fd5b682be5868b218c3c5479071865cab9078/entrypoints-0.4-py3-none-any.whl", hash = "sha256:f174b5ff827504fd3cd97cc3f8649f3693f51538c7e4bdf3ef002c8429d42f9f", size = 5294, upload-time = "2022-02-02T21:30:26.024Z" }, +] + +[[package]] +name = "epics-pypdb" +version = "0.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ply" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/d6/5088d0064d9098ff2a3ce3ccc2608d578ef3b4d3ef07b3178fc59cc4fbf8/epics-pypdb-0.1.5.tar.gz", hash = "sha256:3cad472b279eba47156318c985b4e6293287524dea4bd66e002fe89b53080ea7", size = 18433, upload-time = "2020-04-10T18:13:57.727Z" } + +[[package]] +name = "event-model" +version = "1.23.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-resources" }, + { name = "jsonschema" }, + { name = "numpy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f9/5a/de03f05fdbc4377db89fa6daf243e89370f9bcf2d21ad96b54d4549a74ed/event_model-1.23.1.tar.gz", hash = "sha256:5bb70fd8c7f345aa32afe561aff5a306b2c8a19cbdc3066b736643c8092ddaab", size = 185271, upload-time = "2025-08-28T13:26:38.647Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/32/e31e3363bf48ad2ba80b644b01ad9676ce154f1b755950de81eb4ed5b6bd/event_model-1.23.1-py3-none-any.whl", hash = "sha256:e0b951b829cebcf3879beff238bb370fd997d315856bc5d5ac2a66202b854958", size = 77057, upload-time = "2025-08-28T13:26:37.228Z" }, +] + +[[package]] +name = "executing" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, +] + +[[package]] +name = "flexcache" +version = "0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/b0/8a21e330561c65653d010ef112bf38f60890051d244ede197ddaa08e50c1/flexcache-0.3.tar.gz", hash = "sha256:18743bd5a0621bfe2cf8d519e4c3bfdf57a269c15d1ced3fb4b64e0ff4600656", size = 15816, upload-time = "2024-03-09T03:21:07.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/cd/c883e1a7c447479d6e13985565080e3fea88ab5a107c21684c813dba1875/flexcache-0.3-py3-none-any.whl", hash = "sha256:d43c9fea82336af6e0115e308d9d33a185390b8346a017564611f1466dcd2e32", size = 13263, upload-time = "2024-03-09T03:21:05.635Z" }, +] + +[[package]] +name = "flexparser" +version = "0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/99/b4de7e39e8eaf8207ba1a8fa2241dd98b2ba72ae6e16960d8351736d8702/flexparser-0.4.tar.gz", hash = "sha256:266d98905595be2ccc5da964fe0a2c3526fbbffdc45b65b3146d75db992ef6b2", size = 31799, upload-time = "2024-11-07T02:00:56.249Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/5e/3be305568fe5f34448807976dc82fc151d76c3e0e03958f34770286278c1/flexparser-0.4-py3-none-any.whl", hash = "sha256:3738b456192dcb3e15620f324c447721023c0293f6af9955b481e91d00179846", size = 27625, upload-time = "2024-11-07T02:00:54.523Z" }, +] + +[[package]] +name = "greenlet" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/94/a5935717b307d7c71fe877b52b884c6af707d2d2090db118a03fbd799369/greenlet-3.4.0.tar.gz", hash = "sha256:f50a96b64dafd6169e595a5c56c9146ef80333e67d4476a65a9c55f400fc22ff", size = 195913, upload-time = "2026-04-08T17:08:00.863Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/8b/3669ad3b3f247a791b2b4aceb3aa5a31f5f6817bf547e4e1ff712338145a/greenlet-3.4.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:1a54a921561dd9518d31d2d3db4d7f80e589083063ab4d3e2e950756ef809e1a", size = 286902, upload-time = "2026-04-08T15:52:12.138Z" }, + { url = "https://files.pythonhosted.org/packages/38/3e/3c0e19b82900873e2d8469b590a6c4b3dfd2b316d0591f1c26b38a4879a5/greenlet-3.4.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16dec271460a9a2b154e3b1c2fa1050ce6280878430320e85e08c166772e3f97", size = 606099, upload-time = "2026-04-08T16:24:38.408Z" }, + { url = "https://files.pythonhosted.org/packages/b5/33/99fef65e7754fc76a4ed14794074c38c9ed3394a5bd129d7f61b705f3168/greenlet-3.4.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:90036ce224ed6fe75508c1907a77e4540176dcf0744473627785dd519c6f9996", size = 618837, upload-time = "2026-04-08T16:30:58.298Z" }, + { url = "https://files.pythonhosted.org/packages/36/f7/229f3aed6948faa20e0616a0b8568da22e365ede6a54d7d369058b128afd/greenlet-3.4.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a1c4f6b453006efb8310affb2d132832e9bbb4fc01ce6df6b70d810d38f1f6dc", size = 615062, upload-time = "2026-04-08T15:56:33.766Z" }, + { url = "https://files.pythonhosted.org/packages/08/97/d988180011aa40135c46cd0d0cf01dd97f7162bae14139b4a3ef54889ba5/greenlet-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b2d9a138ffa0e306d0e2b72976d2fb10b97e690d40ab36a472acaab0838e2de", size = 1573511, upload-time = "2026-04-08T16:26:20.058Z" }, + { url = "https://files.pythonhosted.org/packages/d4/0f/a5a26fe152fb3d12e6a474181f6e9848283504d0afd095f353d85726374b/greenlet-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8424683caf46eb0eb6f626cb95e008e8cc30d0cb675bdfa48200925c79b38a08", size = 1640396, upload-time = "2026-04-08T15:57:30.88Z" }, + { url = "https://files.pythonhosted.org/packages/42/cf/bb2c32d9a100e36ee9f6e38fad6b1e082b8184010cb06259b49e1266ca01/greenlet-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0a53fb071531d003b075c444014ff8f8b1a9898d36bb88abd9ac7b3524648a2", size = 238892, upload-time = "2026-04-08T17:03:10.094Z" }, + { url = "https://files.pythonhosted.org/packages/b7/47/6c41314bac56e71436ce551c7fbe3cc830ed857e6aa9708dbb9c65142eb6/greenlet-3.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:f38b81880ba28f232f1f675893a39cf7b6db25b31cc0a09bb50787ecf957e85e", size = 235599, upload-time = "2026-04-08T15:52:54.3Z" }, + { url = "https://files.pythonhosted.org/packages/7a/75/7e9cd1126a1e1f0cd67b0eda02e5221b28488d352684704a78ed505bd719/greenlet-3.4.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:43748988b097f9c6f09364f260741aa73c80747f63389824435c7a50bfdfd5c1", size = 285856, upload-time = "2026-04-08T15:52:45.82Z" }, + { url = "https://files.pythonhosted.org/packages/9d/c4/3e2df392e5cb199527c4d9dbcaa75c14edcc394b45040f0189f649631e3c/greenlet-3.4.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5566e4e2cd7a880e8c27618e3eab20f3494452d12fd5129edef7b2f7aa9a36d1", size = 610208, upload-time = "2026-04-08T16:24:39.674Z" }, + { url = "https://files.pythonhosted.org/packages/da/af/750cdfda1d1bd30a6c28080245be8d0346e669a98fdbae7f4102aa95fff3/greenlet-3.4.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1054c5a3c78e2ab599d452f23f7adafef55062a783a8e241d24f3b633ba6ff82", size = 621269, upload-time = "2026-04-08T16:30:59.767Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/0cbc693622cd54ebe25207efbb3a0eb07c2639cb8594f6e3aaaa0bb077a8/greenlet-3.4.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f82cb6cddc27dd81c96b1506f4aa7def15070c3b2a67d4e46fd19016aacce6cf", size = 617549, upload-time = "2026-04-08T15:56:34.893Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c0/8966767de01343c1ff47e8b855dc78e7d1a8ed2b7b9c83576a57e289f81d/greenlet-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:227a46251ecba4ff46ae742bc5ce95c91d5aceb4b02f885487aff269c127a729", size = 1575310, upload-time = "2026-04-08T16:26:21.671Z" }, + { url = "https://files.pythonhosted.org/packages/b8/38/bcdc71ba05e9a5fda87f63ffc2abcd1f15693b659346df994a48c968003d/greenlet-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5b99e87be7eba788dd5b75ba1cde5639edffdec5f91fe0d734a249535ec3408c", size = 1640435, upload-time = "2026-04-08T15:57:32.572Z" }, + { url = "https://files.pythonhosted.org/packages/a1/c2/19b664b7173b9e4ef5f77e8cef9f14c20ec7fce7920dc1ccd7afd955d093/greenlet-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:849f8bc17acd6295fcb5de8e46d55cc0e52381c56eaf50a2afd258e97bc65940", size = 238760, upload-time = "2026-04-08T17:04:03.878Z" }, + { url = "https://files.pythonhosted.org/packages/9b/96/795619651d39c7fbd809a522f881aa6f0ead504cc8201c3a5b789dfaef99/greenlet-3.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:9390ad88b652b1903814eaabd629ca184db15e0eeb6fe8a390bbf8b9106ae15a", size = 235498, upload-time = "2026-04-08T17:05:00.584Z" }, + { url = "https://files.pythonhosted.org/packages/78/02/bde66806e8f169cf90b14d02c500c44cdbe02c8e224c9c67bafd1b8cadd1/greenlet-3.4.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:10a07aca6babdd18c16a3f4f8880acfffc2b88dfe431ad6aa5f5740759d7d75e", size = 286291, upload-time = "2026-04-08T17:09:34.307Z" }, + { url = "https://files.pythonhosted.org/packages/05/1f/39da1c336a87d47c58352fb8a78541ce63d63ae57c5b9dae1fe02801bbc2/greenlet-3.4.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:076e21040b3a917d3ce4ad68fb5c3c6b32f1405616c4a57aa83120979649bd3d", size = 656749, upload-time = "2026-04-08T16:24:41.721Z" }, + { url = "https://files.pythonhosted.org/packages/d3/6c/90ee29a4ee27af7aa2e2ec408799eeb69ee3fcc5abcecac6ddd07a5cd0f2/greenlet-3.4.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e82689eea4a237e530bb5cb41b180ef81fa2160e1f89422a67be7d90da67f615", size = 669084, upload-time = "2026-04-08T16:31:01.372Z" }, + { url = "https://files.pythonhosted.org/packages/07/49/d4cad6e5381a50947bb973d2f6cf6592621451b09368b8c20d9b8af49c5b/greenlet-3.4.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4df3b0b2289ec686d3c821a5fee44259c05cfe824dd5e6e12c8e5f5df23085cf", size = 665621, upload-time = "2026-04-08T15:56:35.995Z" }, + { url = "https://files.pythonhosted.org/packages/37/31/d1edd54f424761b5d47718822f506b435b6aab2f3f93b465441143ea5119/greenlet-3.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8bff29d586ea415688f4cec96a591fcc3bf762d046a796cdadc1fdb6e7f2d5bf", size = 1622259, upload-time = "2026-04-08T16:26:23.201Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c6/6d3f9cdcb21c4e12a79cb332579f1c6aa1af78eb68059c5a957c7812d95e/greenlet-3.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8a569c2fb840c53c13a2b8967c63621fafbd1a0e015b9c82f408c33d626a2fda", size = 1686916, upload-time = "2026-04-08T15:57:34.282Z" }, + { url = "https://files.pythonhosted.org/packages/63/45/c1ca4a1ad975de4727e52d3ffe641ae23e1d7a8ffaa8ff7a0477e1827b92/greenlet-3.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:207ba5b97ea8b0b60eb43ffcacf26969dd83726095161d676aac03ff913ee50d", size = 239821, upload-time = "2026-04-08T17:03:48.423Z" }, + { url = "https://files.pythonhosted.org/packages/71/c4/6f621023364d7e85a4769c014c8982f98053246d142420e0328980933ceb/greenlet-3.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:f8296d4e2b92af34ebde81085a01690f26a51eb9ac09a0fcadb331eb36dbc802", size = 236932, upload-time = "2026-04-08T17:04:33.551Z" }, + { url = "https://files.pythonhosted.org/packages/d4/8f/18d72b629783f5e8d045a76f5325c1e938e659a9e4da79c7dcd10169a48d/greenlet-3.4.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d70012e51df2dbbccfaf63a40aaf9b40c8bed37c3e3a38751c926301ce538ece", size = 294681, upload-time = "2026-04-08T15:52:35.778Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ad/5fa86ec46769c4153820d58a04062285b3b9e10ba3d461ee257b68dcbf53/greenlet-3.4.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a58bec0751f43068cd40cff31bb3ca02ad6000b3a51ca81367af4eb5abc480c8", size = 658899, upload-time = "2026-04-08T16:24:43.32Z" }, + { url = "https://files.pythonhosted.org/packages/43/f0/4e8174ca0e87ae748c409f055a1ba161038c43cc0a5a6f1433a26ac2e5bf/greenlet-3.4.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05fa0803561028f4b2e3b490ee41216a842eaee11aed004cc343a996d9523aa2", size = 665284, upload-time = "2026-04-08T16:31:02.833Z" }, + { url = "https://files.pythonhosted.org/packages/19/da/991cf7cd33662e2df92a1274b7eb4d61769294d38a1bba8a45f31364845e/greenlet-3.4.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e60d38719cb80b3ab5e85f9f1aed4960acfde09868af6762ccb27b260d68f4ed", size = 661861, upload-time = "2026-04-08T15:56:37.269Z" }, + { url = "https://files.pythonhosted.org/packages/36/c5/6c2c708e14db3d9caea4b459d8464f58c32047451142fe2cfd90e7458f41/greenlet-3.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f50c804733b43eded05ae694691c9aa68bca7d0a867d67d4a3f514742a2d53f", size = 1622182, upload-time = "2026-04-08T16:26:24.777Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4c/50c5fed19378e11a29fabab1f6be39ea95358f4a0a07e115a51ca93385d8/greenlet-3.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2d4f0635dc4aa638cda4b2f5a07ae9a2cff9280327b581a3fcb6f317b4fbc38a", size = 1685050, upload-time = "2026-04-08T15:57:36.453Z" }, + { url = "https://files.pythonhosted.org/packages/db/72/85ae954d734703ab48e622c59d4ce35d77ce840c265814af9c078cacc7aa/greenlet-3.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1a4a48f24681300c640f143ba7c404270e1ebbbcf34331d7104a4ff40f8ea705", size = 245554, upload-time = "2026-04-08T17:03:50.044Z" }, +] + +[[package]] +name = "happi" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "coloredlogs" }, + { name = "entrypoints" }, + { name = "jinja2" }, + { name = "platformdirs" }, + { name = "prettytable" }, + { name = "simplejson" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/0d/8a41c86d5c71084e554cd5b92df482722f4e53e7f100f4569694bc9f11e1/happi-3.0.1.tar.gz", hash = "sha256:972d9e8a57c3a2e5082316d909cd0dbb1663465a8deff2d27a37a214cee1c900", size = 112398, upload-time = "2025-11-24T21:29:05.103Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/27/83314cfd4800ada7542ba49efc772c7a8949d349c7bec177e4e7fa5257e0/happi-3.0.1-py3-none-any.whl", hash = "sha256:0bbf16da6935f2f892ddc78b3ebfecb7c84a6a432d1b2ed49530a6136645d7f5", size = 94099, upload-time = "2025-11-24T21:29:03.925Z" }, +] + +[[package]] +name = "historydict" +version = "1.2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/eb/91c2bd3684147ba8deef546ff2b6d091f32ece81ceb153831bab9f2ea6a5/historydict-1.2.6.tar.gz", hash = "sha256:a800ae05d28b618fe0c913ff0d64e4aebe05d76934fa610539f70ead37dc6fb5", size = 4011, upload-time = "2023-08-05T20:42:20.672Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/47/deb64c73aec25af7699247e021153a6bfe9a08452f7f7337dcee4aa07a2b/historydict-1.2.6-py3-none-any.whl", hash = "sha256:b4b00a170f05502aa682caba62435da5fe1f73037e884707581fe84f8d7b43f5", size = 4501, upload-time = "2023-08-05T20:42:19.244Z" }, +] + +[[package]] +name = "humanfriendly" +version = "10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyreadline3", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/3f/2c29224acb2e2df4d2046e4c73ee2662023c58ff5b113c4c1adac0886c43/humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc", size = 360702, upload-time = "2021-09-17T21:40:43.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", size = 86794, upload-time = "2021-09-17T21:40:39.897Z" }, +] + +[[package]] +name = "idna" +version = "3.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/12/2948fbe5513d062169bd91f7d7b1cd97bc8894f32946b71fa39f6e63ca0c/idna-3.12.tar.gz", hash = "sha256:724e9952cc9e2bd7550ea784adb098d837ab5267ef67a1ab9cf7846bdbdd8254", size = 194350, upload-time = "2026-04-21T13:32:48.916Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/b2/acc33950394b3becb2b664741a0c0889c7ef9f9ffbfa8d47eddb53a50abd/idna-3.12-py3-none-any.whl", hash = "sha256:60ffaa1858fac94c9c124728c24fcde8160f3fb4a7f79aa8cdd33a9d1af60a67", size = 68634, upload-time = "2026-04-21T13:32:47.403Z" }, +] + +[[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 = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + +[[package]] +name = "importlib-resources" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/06/b56dfa750b44e86157093bc8fca0ab81dccbf5260510de4eaf1cb69b5b99/importlib_resources-7.1.0.tar.gz", hash = "sha256:0722d4c6212489c530f2a145a34c0a7a3b4721bc96a15fada5930e2a0b760708", size = 44985, upload-time = "2026-04-12T16:36:09.232Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/db/55a262f3606bebcae07cc14095338471ad7c0bbcaa37707e6f0ee49725b7/importlib_resources-7.1.0-py3-none-any.whl", hash = "sha256:1bd7b48b4088eddb2cd16382150bb515af0bd2c70128194392725f82ad2c96a1", size = 37232, upload-time = "2026-04-12T16:36:08.219Z" }, +] + +[[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 = "ipython" +version = "9.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "decorator" }, + { name = "ipython-pygments-lexers" }, + { name = "jedi" }, + { name = "matplotlib-inline" }, + { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit" }, + { name = "pygments" }, + { name = "stack-data" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/73/7114f80a8f9cabdb13c27732dce24af945b2923dcab80723602f7c8bc2d8/ipython-9.12.0.tar.gz", hash = "sha256:01daa83f504b693ba523b5a407246cabde4eb4513285a3c6acaff11a66735ee4", size = 4428879, upload-time = "2026-03-27T09:42:45.312Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/22/906c8108974c673ebef6356c506cebb6870d48cedea3c41e949e2dd556bb/ipython-9.12.0-py3-none-any.whl", hash = "sha256:0f2701e8ee86e117e37f50563205d36feaa259d2e08d4a6bc6b6d74b18ce128d", size = 625661, upload-time = "2026-03-27T09:42:42.831Z" }, +] + +[[package]] +name = "ipython-pygments-lexers" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, +] + +[[package]] +name = "jedi" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parso" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, +] + +[[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 = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "lightpath" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coloredlogs" }, + { name = "happi" }, + { name = "networkx" }, + { name = "numpy" }, + { name = "ophyd" }, + { name = "prettytable" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/9b/192b444a68b7df0df9be5fe1d85cb68a213301e0425c7794f95bacc973da/lightpath-1.0.9.tar.gz", hash = "sha256:53f496842ab4645a21a786767ee16831ba21c4d372a87d04fd6945551c01d235", size = 190037, upload-time = "2026-02-20T17:54:25.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/6c/0391d59568bb1167be9edb9265dcc2631fbfc6d917b1c91739b2212d4b3f/lightpath-1.0.9-py3-none-any.whl", hash = "sha256:6f6148f7c44c0f091d426d0c920cb324dcae9586e6916f3411ae9e3ccc388f7b", size = 52669, upload-time = "2026-02-20T17:54:23.758Z" }, +] + +[[package]] +name = "line-profiler" +version = "5.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/03/b6/6d18ad201417a9c5168995541d0fd7981b5652b2b34f6e46a3a93c0f1beb/line_profiler-5.0.2.tar.gz", hash = "sha256:8d8a990c84c64bcde45af22af502d17bc0ae107be405ce41bba92af5c39c0000", size = 407075, upload-time = "2026-02-23T23:31:20.698Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/92/fb766e6355118d2a681c18525d4c005c146ec44b064ccfd70f4529d8d260/line_profiler-5.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:256c1d5e84a93254dbe656d0486322190cc68f6b517544edef17a9f00167e680", size = 646920, upload-time = "2026-02-23T23:30:14.692Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9d/3583c1cdc740206de9e4734bdcf377d649b89ea876bc36001d95b3dea67d/line_profiler-5.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:892f0cd9967b101ce7528be2d388616037c73cb27830effd7493fa021165c622", size = 505695, upload-time = "2026-02-23T23:30:16.375Z" }, + { url = "https://files.pythonhosted.org/packages/27/60/412476a1d09beac783d11d3bbf85fe6c1e3d50058e3c28967fee59c46649/line_profiler-5.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d2d02735843c14337dae3e80d95a732b4657ef759def75162ef97a1aa7466aac", size = 495859, upload-time = "2026-02-23T23:30:18.036Z" }, + { url = "https://files.pythonhosted.org/packages/37/75/65028bad08264fd8f9c3f0fd405c539ff552c2d1cf2a00965157ad148973/line_profiler-5.0.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0d2e166a86dc9c78c349ee18b592b98ebfb9dae615f63fc77cce5f5f751a6ad0", size = 1464882, upload-time = "2026-02-23T23:30:19.273Z" }, + { url = "https://files.pythonhosted.org/packages/0d/6c/2d0286f67e6bb2b00ae23f9af6df18bfc6bb1ac5d803a8f46bd3eb22a8f1/line_profiler-5.0.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a870b68af1539d718d030f4c4726d35cff4b14ab605147e65222933c5c0e10e", size = 1484331, upload-time = "2026-02-23T23:30:20.571Z" }, + { url = "https://files.pythonhosted.org/packages/4e/a4/b01359733214a1a85c5f86f3953b07deb61b267efa0328e8d436a1ad80ea/line_profiler-5.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:fe8cd787caa2a02ca7e138832fa4cab1f198377eaf6e5e8263e8b7506157c454", size = 2411802, upload-time = "2026-02-23T23:30:21.995Z" }, + { url = "https://files.pythonhosted.org/packages/d1/f4/1fa91206a6c50091cf614fdd5c9d349eb3a57d23f5eb8be8fffe7e0525b9/line_profiler-5.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:70ff915ade9e3ec38ff043ff093b590bbb3055e6fc8b311e0fe14cd78fb2a7f7", size = 2495790, upload-time = "2026-02-23T23:30:23.448Z" }, + { url = "https://files.pythonhosted.org/packages/87/18/d389c72dce6c8318c088a7c29ee8961a913c8a1c6469888b517e8f47ddaf/line_profiler-5.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:026779b9dfca0f367174f5d34bcccffce2755db40a4389f0d8a531a2e3ca7cfc", size = 478790, upload-time = "2026-02-23T23:30:24.848Z" }, + { url = "https://files.pythonhosted.org/packages/3f/54/d171600a4190c07215090a88846ef0093b5bf34a81f8059115592dbb1354/line_profiler-5.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:fe22b927f05a61a0149976bf0d22d8e56fa742ec89f3d72358db71a1f440c77b", size = 462269, upload-time = "2026-02-23T23:30:26.237Z" }, + { url = "https://files.pythonhosted.org/packages/a7/64/856b920e026fbd239df875ec05e63583f7bd7f250805215ab6e132da11d1/line_profiler-5.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:016effba91d34d15229d41984e921a27f66a7b634f1d7adf6c57c743f3d6a0eb", size = 642642, upload-time = "2026-02-23T23:30:27.63Z" }, + { url = "https://files.pythonhosted.org/packages/3b/08/0a56fab0a36818af6ffc8073700db2f402db5a62477b69d938c19871d631/line_profiler-5.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:506e800dd408a8aafadf39ff4e4a1375ae7794910d00098f191520a2f390cb99", size = 503787, upload-time = "2026-02-23T23:30:29.226Z" }, + { url = "https://files.pythonhosted.org/packages/ed/9a/0ab45cf92b2c13261b475c440e18bb18d9497cc2ad5dfaf38c231c72b02b/line_profiler-5.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e67f77bcb349a663cb22819f65621bcd2a39889524dd890d1d88f8736841b7b", size = 493631, upload-time = "2026-02-23T23:30:30.502Z" }, + { url = "https://files.pythonhosted.org/packages/fb/15/a5b603f0c7c795aa656a95e2a70d139dc499b5d153b6a3129bbba6b6f913/line_profiler-5.0.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6b9d08e85fd48d254ae253e76dc72598e94200ef7002eb1ae0bab4cc9c5e41a", size = 1464022, upload-time = "2026-02-23T23:30:31.793Z" }, + { url = "https://files.pythonhosted.org/packages/27/6f/0f399c72eecaf8f8c00e84238b5786afc34d0a4ef5ad10c63c712715ba86/line_profiler-5.0.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31290e06ac25cd87fee46ebe979541d4ec7c8d6f15c5cbe5874a932b1cee95bb", size = 1483425, upload-time = "2026-02-23T23:30:33.15Z" }, + { url = "https://files.pythonhosted.org/packages/65/18/f4c642a29719a84d17ea8b58cd6e60943573a28228c30c568565ed5512aa/line_profiler-5.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1d7fbcc2dbd8534fc6f7d2b440076749b2235cdc525eb177fefafeaf7550373f", size = 2410276, upload-time = "2026-02-23T23:30:34.943Z" }, + { url = "https://files.pythonhosted.org/packages/90/33/701203686e7d27a545e3bbc8e81fffc7d091c42ed33564be4e72376ef45b/line_profiler-5.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:55f04671f48afcd90858c18fbdb2509463c77d717ed5424664f096e902206b6b", size = 2495283, upload-time = "2026-02-23T23:30:36.616Z" }, + { url = "https://files.pythonhosted.org/packages/34/e1/59fe065f67ed1fb8f974a9e3434685af1fc1f6a154489f7ab0992eab1c73/line_profiler-5.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:d2262d4bbbcf72bd430fc5763073792a0f1cb20e64de0f7ecf6e8ae16627d876", size = 479287, upload-time = "2026-02-23T23:30:38.152Z" }, + { url = "https://files.pythonhosted.org/packages/e9/83/89f6ae52fa77960404ee88fc078ee680e504bf1ab8724ac01430cee0f5a5/line_profiler-5.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:abf755b020d91b639cbc563015eca381ca64e6bd27ee55ef9004a3a17b6d4dcf", size = 461960, upload-time = "2026-02-23T23:30:39.657Z" }, + { url = "https://files.pythonhosted.org/packages/c6/ae/43caf21edd10a7f5e138bdffcad01ade9a704462a923054402bbadbe5364/line_profiler-5.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:a1cc30f3f7877fec826d0f40f400ee6c99239dc6a2f587b8d90d06a42d29c8a5", size = 648335, upload-time = "2026-02-23T23:30:41.042Z" }, + { url = "https://files.pythonhosted.org/packages/34/90/8a1fb985dc582d140fc92608dec3037a484c5f8ab99ae05c24031aa68000/line_profiler-5.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:f90923e1cc4ff8eda1d18e525089fca7bfd6dfe8817ec530a913a2c7444ba0fd", size = 508823, upload-time = "2026-02-23T23:30:42.16Z" }, + { url = "https://files.pythonhosted.org/packages/a4/01/855c55e195ac0aadb8ca4e4c65311f945ed02a2491b436bc33cee318d841/line_profiler-5.0.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cc3d0ecccb14f014d05b32f687d22adcb98bf59fdcc721e7a4330f0372a56f92", size = 499868, upload-time = "2026-02-23T23:30:44.188Z" }, + { url = "https://files.pythonhosted.org/packages/fc/48/fe73d6192a37637534366306a7871ef0f7ff5973bd87da082e4bf5ec0764/line_profiler-5.0.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5341f36e532e7ed28e323f5502a29b397b66a6708c6427a77f965148a2e5ddec", size = 1460660, upload-time = "2026-02-23T23:30:45.601Z" }, + { url = "https://files.pythonhosted.org/packages/49/1c/e1236e0f3c7ec1e19e74d61ac15143a7826b5767296de87bcf3aa26548a1/line_profiler-5.0.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4cce501f9d996b317b599c0ae99e3eb1bd447874ef8fef1da330b27f3a23eb50", size = 1475222, upload-time = "2026-02-23T23:30:47.014Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ad/02302fd2a82949277036bc557ecebddb9bc6282b76a4da7660258fe82111/line_profiler-5.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b237d82fb792c3db7c80a8675d3c48993d4421b14d96ae602f7fe9ccf1f85903", size = 2413428, upload-time = "2026-02-23T23:30:48.828Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c7/b3efe646c8b9fdc6fe26720860276c8a2bb745ffe30f5bcbc9726b975673/line_profiler-5.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:74febeca89128a37a32e6500c99665943c0d11e6043f46ce95596d7d1e1732a7", size = 2494741, upload-time = "2026-02-23T23:30:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/0e/ad/ddadd39eb92900f063f27e8f6d748c03dc2638873f07ebf3cee75f29711f/line_profiler-5.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:d6ce98faff60d9552a30e233648a848682b5d664a7e09e9669163a8f01e28147", size = 485700, upload-time = "2026-02-23T23:30:52.373Z" }, + { url = "https://files.pythonhosted.org/packages/d0/45/a529f355eea8fb790fbdee0273d6c0049dba3232a36e82c30d849b00e996/line_profiler-5.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:8be7cc5f4ed9ad87352129d1a494cf5ba7f0fced0472201d83ac9fbfa20f798b", size = 469781, upload-time = "2026-02-23T23:30:53.747Z" }, +] + +[[package]] +name = "lxml" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/28/30/9abc9e34c657c33834eaf6cd02124c61bdf5944d802aa48e69be8da3585d/lxml-6.1.0.tar.gz", hash = "sha256:bfd57d8008c4965709a919c3e9a98f76c2c7cb319086b3d26858250620023b13", size = 4197006, upload-time = "2026-04-18T04:32:51.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/d4/9326838b59dc36dfae42eec9656b97520f9997eee1de47b8316aaeed169c/lxml-6.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d2f17a16cd8751e8eb233a7e41aecdf8e511712e00088bf9be455f604cd0d28d", size = 8570663, upload-time = "2026-04-18T04:27:48.253Z" }, + { url = "https://files.pythonhosted.org/packages/d8/a4/053745ce1f8303ccbb788b86c0db3a91b973675cefc42566a188637b7c40/lxml-6.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f0cea5b1d3e6e77d71bd2b9972eb2446221a69dc52bb0b9c3c6f6e5700592d93", size = 4624024, upload-time = "2026-04-18T04:27:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/90/97/a517944b20f8fd0932ad2109482bee4e29fe721416387a363306667941f6/lxml-6.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fc46da94826188ed45cb53bd8e3fc076ae22675aea2087843d4735627f867c6d", size = 4930895, upload-time = "2026-04-18T04:32:56.29Z" }, + { url = "https://files.pythonhosted.org/packages/94/7c/e08a970727d556caa040a44773c7b7e3ad0f0d73dedc863543e9a8b931f2/lxml-6.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9147d8e386ec3b82c3b15d88927f734f565b0aaadef7def562b853adca45784a", size = 5093820, upload-time = "2026-04-18T04:32:58.94Z" }, + { url = "https://files.pythonhosted.org/packages/88/ee/2a5c2aa2c32016a226ca25d3e1056a8102ea6e1fe308bf50213586635400/lxml-6.1.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5715e0e28736a070f3f34a7ccc09e2fdcba0e3060abbcf61a1a5718ff6d6b105", size = 5005790, upload-time = "2026-04-18T04:33:01.272Z" }, + { url = "https://files.pythonhosted.org/packages/e3/38/a0db9be8f38ad6043ab9429487c128dd1d30f07956ef43040402f8da49e8/lxml-6.1.0-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4937460dc5df0cdd2f06a86c285c28afda06aefa3af949f9477d3e8df430c485", size = 5630827, upload-time = "2026-04-18T04:33:04.036Z" }, + { url = "https://files.pythonhosted.org/packages/31/ba/3c13d3fc24b7cacf675f808a3a1baabf43a30d0cd24c98f94548e9aa58eb/lxml-6.1.0-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bc783ee3147e60a25aa0445ea82b3e8aabb83b240f2b95d32cb75587ff781814", size = 5240445, upload-time = "2026-04-18T04:33:06.87Z" }, + { url = "https://files.pythonhosted.org/packages/55/ba/eeef4ccba09b2212fe239f46c1692a98db1878e0872ae320756488878a94/lxml-6.1.0-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:40d9189f80075f2e1f88db21ef815a2b17b28adf8e50aaf5c789bfe737027f32", size = 5350121, upload-time = "2026-04-18T04:33:09.365Z" }, + { url = "https://files.pythonhosted.org/packages/7e/01/1da87c7b587c38d0cbe77a01aae3b9c1c49ed47d76918ef3db8fc151b1ca/lxml-6.1.0-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:05b9b8787e35bec69e68daf4952b2e6dfcfb0db7ecf1a06f8cdfbbac4eb71aad", size = 4694949, upload-time = "2026-04-18T04:33:11.628Z" }, + { url = "https://files.pythonhosted.org/packages/a1/88/7db0fe66d5aaf128443ee1623dec3db1576f3e4c17751ec0ef5866468590/lxml-6.1.0-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0f0f08beb0182e3e9a86fae124b3c47a7b41b7b69b225e1377db983802404e54", size = 5243901, upload-time = "2026-04-18T04:33:13.95Z" }, + { url = "https://files.pythonhosted.org/packages/00/a8/1346726af7d1f6fca1f11223ba34001462b0a3660416986d37641708d57c/lxml-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73becf6d8c81d4c76b1014dbd3584cb26d904492dcf73ca85dc8bff08dcd6d2d", size = 5048054, upload-time = "2026-04-18T04:33:16.965Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b7/85057012f035d1a0c87e02f8c723ca3c3e6e0728bcf4cb62080b21b1c1e3/lxml-6.1.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1ae225f66e5938f4fa29d37e009a3bb3b13032ac57eb4eb42afa44f6e4054e69", size = 4777324, upload-time = "2026-04-18T04:33:19.832Z" }, + { url = "https://files.pythonhosted.org/packages/75/6c/ad2f94a91073ef570f33718040e8e160d5fb93331cf1ab3ca1323f939e2d/lxml-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:690022c7fae793b0489aa68a658822cea83e0d5933781811cabbf5ea3bcfe73d", size = 5645702, upload-time = "2026-04-18T04:33:22.436Z" }, + { url = "https://files.pythonhosted.org/packages/3b/89/0bb6c0bd549c19004c60eea9dc554dd78fd647b72314ef25d460e0d208c6/lxml-6.1.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:63aeafc26aac0be8aff14af7871249e87ea1319be92090bfd632ec68e03b16a5", size = 5232901, upload-time = "2026-04-18T04:33:26.21Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d9/d609a11fb567da9399f525193e2b49847b5a409cdebe737f06a8b7126bdc/lxml-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:264c605ab9c0e4aa1a679636f4582c4d3313700009fac3ec9c3412ed0d8f3e1d", size = 5261333, upload-time = "2026-04-18T04:33:28.984Z" }, + { url = "https://files.pythonhosted.org/packages/a6/3a/ac3f99ec8ac93089e7dd556f279e0d14c24de0a74a507e143a2e4b496e7c/lxml-6.1.0-cp312-cp312-win32.whl", hash = "sha256:56971379bc5ee8037c5a0f09fa88f66cdb7d37c3e38af3e45cf539f41131ac1f", size = 3596289, upload-time = "2026-04-18T04:27:42.819Z" }, + { url = "https://files.pythonhosted.org/packages/f2/a7/0a915557538593cb1bbeedcd40e13c7a261822c26fecbbdb71dad0c2f540/lxml-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:bba078de0031c219e5dd06cf3e6bf8fb8e6e64a77819b358f53bb132e3e03366", size = 3997059, upload-time = "2026-04-18T04:27:46.764Z" }, + { url = "https://files.pythonhosted.org/packages/92/96/a5dc078cf0126fbfbc35611d77ecd5da80054b5893e28fb213a5613b9e1d/lxml-6.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:c3592631e652afa34999a088f98ba7dfc7d6aff0d535c410bea77a71743f3819", size = 3659552, upload-time = "2026-04-18T04:27:51.133Z" }, + { url = "https://files.pythonhosted.org/packages/08/03/69347590f1cf4a6d5a4944bb6099e6d37f334784f16062234e1f892fdb1d/lxml-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a0092f2b107b69601adf562a57c956fbb596e05e3e6651cabd3054113b007e45", size = 8559689, upload-time = "2026-04-18T04:31:57.785Z" }, + { url = "https://files.pythonhosted.org/packages/3f/58/25e00bb40b185c974cfe156c110474d9a8a8390d5f7c92a4e328189bb60e/lxml-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fc7140d7a7386e6b545d41b7358f4d02b656d4053f5fa6859f92f4b9c2572c4d", size = 4617892, upload-time = "2026-04-18T04:32:01.78Z" }, + { url = "https://files.pythonhosted.org/packages/f5/54/92ad98a94ac318dc4f97aaac22ff8d1b94212b2ae8af5b6e9b354bf825f7/lxml-6.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:419c58fc92cc3a2c3fa5f78c63dbf5da70c1fa9c1b25f25727ecee89a96c7de2", size = 4923489, upload-time = "2026-04-18T04:33:31.401Z" }, + { url = "https://files.pythonhosted.org/packages/15/3b/a20aecfab42bdf4f9b390590d345857ad3ffd7c51988d1c89c53a0c73faf/lxml-6.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:37fabd1452852636cf38ecdcc9dd5ca4bba7a35d6c53fa09725deeb894a87491", size = 5082162, upload-time = "2026-04-18T04:33:34.262Z" }, + { url = "https://files.pythonhosted.org/packages/45/26/2cdb3d281ac1bd175603e290cbe4bad6eff127c0f8de90bafd6f8548f0fd/lxml-6.1.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2853c8b2170cc6cd54a6b4d50d2c1a8a7aeca201f23804b4898525c7a152cfc", size = 4993247, upload-time = "2026-04-18T04:33:36.674Z" }, + { url = "https://files.pythonhosted.org/packages/f6/05/d735aef963740022a08185c84821f689fc903acb3d50326e6b1e9886cc22/lxml-6.1.0-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8e369cbd690e788c8d15e56222d91a09c6a417f49cbc543040cba0fe2e25a79e", size = 5613042, upload-time = "2026-04-18T04:33:39.205Z" }, + { url = "https://files.pythonhosted.org/packages/ee/b8/ead7c10efff731738c72e59ed6eb5791854879fbed7ae98781a12006263a/lxml-6.1.0-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e69aa6805905807186eb00e66c6d97a935c928275182eb02ee40ba00da9623b2", size = 5228304, upload-time = "2026-04-18T04:33:41.647Z" }, + { url = "https://files.pythonhosted.org/packages/6b/10/e9842d2ec322ea65f0a7270aa0315a53abed06058b88ef1b027f620e7a5f/lxml-6.1.0-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:4bd1bdb8a9e0e2dd229de19b5f8aebac80e916921b4b2c6ef8a52bc131d0c1f9", size = 5341578, upload-time = "2026-04-18T04:33:44.596Z" }, + { url = "https://files.pythonhosted.org/packages/89/54/40d9403d7c2775fa7301d3ddd3464689bfe9ba71acc17dfff777071b4fdc/lxml-6.1.0-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:cbd7b79cdcb4986ad78a2662625882747f09db5e4cd7b2ae178a88c9c51b3dfe", size = 4700209, upload-time = "2026-04-18T04:33:47.552Z" }, + { url = "https://files.pythonhosted.org/packages/85/b2/bbdcc2cf45dfc7dfffef4fd97e5c47b15919b6a365247d95d6f684ef5e82/lxml-6.1.0-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:43e4d297f11080ec9d64a4b1ad7ac02b4484c9f0e2179d9c4ef78e886e747b88", size = 5232365, upload-time = "2026-04-18T04:33:50.249Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/b06875665e53aaba7127611a7bed3b7b9658e20b22bc2dd217a0b7ab0091/lxml-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cc16682cc987a3da00aa56a3aa3075b08edb10d9b1e476938cfdbee8f3b67181", size = 5043654, upload-time = "2026-04-18T04:33:52.71Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9c/e71a069d09641c1a7abeb30e693f828c7c90a41cbe3d650b2d734d876f85/lxml-6.1.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:d6d8efe71429635f0559579092bb5e60560d7b9115ee38c4adbea35632e7fa24", size = 4769326, upload-time = "2026-04-18T04:33:55.244Z" }, + { url = "https://files.pythonhosted.org/packages/cc/06/7a9cd84b3d4ed79adf35f874750abb697dec0b4a81a836037b36e47c091a/lxml-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7e39ab3a28af7784e206d8606ec0e4bcad0190f63a492bca95e94e5a4aef7f6e", size = 5635879, upload-time = "2026-04-18T04:33:58.509Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f0/9d57916befc1e54c451712c7ee48e9e74e80ae4d03bdce49914e0aee42cd/lxml-6.1.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9eb667bf50856c4a58145f8ca2d5e5be160191e79eb9e30855a476191b3c3495", size = 5224048, upload-time = "2026-04-18T04:34:00.943Z" }, + { url = "https://files.pythonhosted.org/packages/99/75/90c4eefda0c08c92221fe0753db2d6699a4c628f76ff4465ec20dea84cc1/lxml-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7f4a77d6f7edf9230cee3e1f7f6764722a41604ee5681844f18db9a81ea0ec33", size = 5250241, upload-time = "2026-04-18T04:34:03.365Z" }, + { url = "https://files.pythonhosted.org/packages/5e/73/16596f7e4e38fa33084b9ccbccc22a15f82a290a055126f2c1541236d2ff/lxml-6.1.0-cp313-cp313-win32.whl", hash = "sha256:28902146ffbe5222df411c5d19e5352490122e14447e98cd118907ee3fd6ee62", size = 3596938, upload-time = "2026-04-18T04:31:56.206Z" }, + { url = "https://files.pythonhosted.org/packages/8e/63/981401c5680c1eb30893f00a19641ac80db5d1e7086c62cb4b13ed813038/lxml-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:4a1503c56e4e2b38dc76f2f2da7bae69670c0f1933e27cfa34b2fa5876410b16", size = 3995728, upload-time = "2026-04-18T04:31:58.763Z" }, + { url = "https://files.pythonhosted.org/packages/e7/e8/c358a38ac3e541d16a1b527e4e9cb78c0419b0506a070ace11777e5e8404/lxml-6.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:e0af85773850417d994d019741239b901b22c6680206f46a34766926e466141d", size = 3658372, upload-time = "2026-04-18T04:32:03.629Z" }, + { url = "https://files.pythonhosted.org/packages/eb/45/cee4cf203ef0bab5c52afc118da61d6b460c928f2893d40023cfa27e0b80/lxml-6.1.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:ab863fd37458fed6456525f297d21239d987800c46e67da5ef04fc6b3dd93ac8", size = 8576713, upload-time = "2026-04-18T04:32:06.831Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a7/eda05babeb7e046839204eaf254cd4d7c9130ce2bbf0d9e90ea41af5654d/lxml-6.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6fd8b1df8254ff4fd93fd31da1fc15770bde23ac045be9bb1f87425702f61cc9", size = 4623874, upload-time = "2026-04-18T04:32:10.755Z" }, + { url = "https://files.pythonhosted.org/packages/e7/e9/db5846de9b436b91890a62f29d80cd849ea17948a49bf532d5278ee69a9e/lxml-6.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:47024feaae386a92a146af0d2aeed65229bf6fff738e6a11dda6b0015fb8fd03", size = 4949535, upload-time = "2026-04-18T04:34:06.657Z" }, + { url = "https://files.pythonhosted.org/packages/5a/ba/0d3593373dcae1d68f40dc3c41a5a92f2544e68115eb2f62319a4c2a6500/lxml-6.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3f00972f84450204cd5d93a5395965e348956aaceaadec693a22ec743f8ae3eb", size = 5086881, upload-time = "2026-04-18T04:34:09.556Z" }, + { url = "https://files.pythonhosted.org/packages/43/76/759a7484539ad1af0d125a9afe9c3fb5f82a8779fd1f5f56319d9e4ea2fd/lxml-6.1.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97faa0860e13b05b15a51fb4986421ef7a30f0b3334061c416e0981e9450ca4c", size = 5031305, upload-time = "2026-04-18T04:34:12.336Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b9/c1f0daf981a11e47636126901fd4ab82429e18c57aeb0fc3ad2940b42d8b/lxml-6.1.0-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:972a6451204798675407beaad97b868d0c733d9a74dafefc63120b81b8c2de28", size = 5647522, upload-time = "2026-04-18T04:34:14.89Z" }, + { url = "https://files.pythonhosted.org/packages/31/e6/1f533dcd205275363d9ba3511bcec52fa2df86abf8abe6a5f2c599f0dc31/lxml-6.1.0-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fe022f20bc4569ec66b63b3fb275a3d628d9d32da6326b2982584104db6d3086", size = 5239310, upload-time = "2026-04-18T04:34:17.652Z" }, + { url = "https://files.pythonhosted.org/packages/c3/8c/4175fb709c78a6e315ed814ed33be3defd8b8721067e70419a6cf6f971da/lxml-6.1.0-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:75c4c7c619a744f972f4451bf5adf6d0fb00992a1ffc9fd78e13b0bc817cc99f", size = 5350799, upload-time = "2026-04-18T04:34:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/fd/77/6ffdebc5994975f0dde4acb59761902bd9d9bb84422b9a0bd239a7da9ca8/lxml-6.1.0-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:3648f20d25102a22b6061c688beb3a805099ea4beb0a01ce62975d926944d292", size = 4697693, upload-time = "2026-04-18T04:34:23.541Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f1/565f36bd5c73294602d48e04d23f81ff4c8736be6ba5e1d1ec670ac9be80/lxml-6.1.0-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:77b9f99b17cbf14026d1e618035077060fc7195dd940d025149f3e2e830fbfcb", size = 5250708, upload-time = "2026-04-18T04:34:26.001Z" }, + { url = "https://files.pythonhosted.org/packages/5a/11/a68ab9dd18c5c499404deb4005f4bc4e0e88e5b72cd755ad96efec81d18d/lxml-6.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32662519149fd7a9db354175aa5e417d83485a8039b8aaa62f873ceee7ea4cad", size = 5084737, upload-time = "2026-04-18T04:34:28.32Z" }, + { url = "https://files.pythonhosted.org/packages/ab/78/e8f41e2c74f4af564e6a0348aea69fb6daaefa64bc071ef469823d22cc18/lxml-6.1.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:73d658216fc173cf2c939e90e07b941c5e12736b0bf6a99e7af95459cfe8eabb", size = 4737817, upload-time = "2026-04-18T04:34:30.784Z" }, + { url = "https://files.pythonhosted.org/packages/06/2d/aa4e117aa2ce2f3b35d9ff246be74a2f8e853baba5d2a92c64744474603a/lxml-6.1.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ac4db068889f8772a4a698c5980ec302771bb545e10c4b095d4c8be26749616f", size = 5670753, upload-time = "2026-04-18T04:34:33.675Z" }, + { url = "https://files.pythonhosted.org/packages/08/f5/dd745d50c0409031dbfcc4881740542a01e54d6f0110bd420fa7782110b8/lxml-6.1.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:45e9dfbd1b661eb64ba0d4dbe762bd210c42d86dd1e5bd2bdf89d634231beb43", size = 5238071, upload-time = "2026-04-18T04:34:36.12Z" }, + { url = "https://files.pythonhosted.org/packages/3e/74/ad424f36d0340a904665867dab310a3f1f4c96ff4039698de83b77f44c1f/lxml-6.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:89e8d73d09ac696a5ba42ec69787913d53284f12092f651506779314f10ba585", size = 5264319, upload-time = "2026-04-18T04:34:39.035Z" }, + { url = "https://files.pythonhosted.org/packages/53/36/a15d8b3514ec889bfd6aa3609107fcb6c9189f8dc347f1c0b81eded8d87c/lxml-6.1.0-cp314-cp314-win32.whl", hash = "sha256:ebe33f4ec1b2de38ceb225a1749a2965855bffeef435ba93cd2d5d540783bf2f", size = 3657139, upload-time = "2026-04-18T04:32:20.006Z" }, + { url = "https://files.pythonhosted.org/packages/1a/a4/263ebb0710851a3c6c937180a9a86df1206fdfe53cc43005aa2237fd7736/lxml-6.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:398443df51c538bd578529aa7e5f7afc6c292644174b47961f3bf87fe5741120", size = 4064195, upload-time = "2026-04-18T04:32:23.876Z" }, + { url = "https://files.pythonhosted.org/packages/80/68/2000f29d323b6c286de077ad20b429fc52272e44eae6d295467043e56012/lxml-6.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:8c8984e1d8c4b3949e419158fda14d921ff703a9ed8a47236c6eb7a2b6cb4946", size = 3741870, upload-time = "2026-04-18T04:32:27.922Z" }, + { url = "https://files.pythonhosted.org/packages/30/e9/21383c7c8d43799f0da90224c0d7c921870d476ec9b3e01e1b2c0b8237c5/lxml-6.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1081dd10bc6fa437db2500e13993abf7cc30716d0a2f40e65abb935f02ec559c", size = 8827548, upload-time = "2026-04-18T04:32:15.094Z" }, + { url = "https://files.pythonhosted.org/packages/a5/01/c6bc11cd587030dd4f719f65c5657960649fe3e19196c844c75bf32cd0d6/lxml-6.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:dabecc48db5f42ba348d1f5d5afdc54c6c4cc758e676926c7cd327045749517d", size = 4735866, upload-time = "2026-04-18T04:32:18.924Z" }, + { url = "https://files.pythonhosted.org/packages/f3/01/757132fff5f4acf25463b5298f1a46099f3a94480b806547b29ce5e385de/lxml-6.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e3dd5fe19c9e0ac818a9c7f132a5e43c1339ec1cbbfecb1a938bd3a47875b7c9", size = 4969476, upload-time = "2026-04-18T04:34:41.889Z" }, + { url = "https://files.pythonhosted.org/packages/fd/fb/1bc8b9d27ed64be7c8903db6c89e74dc8c2cd9ec630a7462e4654316dc5b/lxml-6.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9e7b0a4ca6dcc007a4cef00a761bba2dea959de4bd2df98f926b33c92ca5dfb9", size = 5103719, upload-time = "2026-04-18T04:34:44.797Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e7/5bf82fa28133536a54601aae633b14988e89ed61d4c1eb6b899b023233aa/lxml-6.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d27bbe326c6b539c64b42638b18bc6003a8d88f76213a97ac9ed4f885efeab7", size = 5027890, upload-time = "2026-04-18T04:34:47.634Z" }, + { url = "https://files.pythonhosted.org/packages/2d/20/e048db5d4b4ea0366648aa595f26bb764b2670903fc585b87436d0a5032c/lxml-6.1.0-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4e425db0c5445ef0ad56b0eec54f89b88b2d884656e536a90b2f52aecb4ca86", size = 5596008, upload-time = "2026-04-18T04:34:51.503Z" }, + { url = "https://files.pythonhosted.org/packages/9a/c2/d10807bc8da4824b39e5bd01b5d05c077b6fd01bd91584167edf6b269d22/lxml-6.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b89b098105b8599dc57adac95d1813409ac476d3c948a498775d3d0c6124bfb", size = 5224451, upload-time = "2026-04-18T04:34:54.263Z" }, + { url = "https://files.pythonhosted.org/packages/3c/15/2ebea45bea427e7f0057e9ce7b2d62c5aba20c6b001cca89ed0aadb3ad41/lxml-6.1.0-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:c4a699432846df86cc3de502ee85f445ebad748a1c6021d445f3e514d2cd4b1c", size = 5312135, upload-time = "2026-04-18T04:34:56.818Z" }, + { url = "https://files.pythonhosted.org/packages/31/e2/87eeae151b0be2a308d49a7ec444ff3eb192b14251e62addb29d0bf3778f/lxml-6.1.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:30e7b2ed63b6c8e97cca8af048589a788ab5c9c905f36d9cf1c2bb549f450d2f", size = 4639126, upload-time = "2026-04-18T04:34:59.704Z" }, + { url = "https://files.pythonhosted.org/packages/a3/51/8a3f6a20902ad604dd746ec7b4000311b240d389dac5e9d95adefd349e0c/lxml-6.1.0-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:022981127642fe19866d2907d76241bb07ed21749601f727d5d5dd1ce5d1b773", size = 5232579, upload-time = "2026-04-18T04:35:02.658Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d2/650d619bdbe048d2c3f2c31edb00e35670a5e2d65b4fe3b61bce37b19121/lxml-6.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:23cad0cc86046d4222f7f418910e46b89971c5a45d3c8abfad0f64b7b05e4a9b", size = 5084206, upload-time = "2026-04-18T04:35:05.175Z" }, + { url = "https://files.pythonhosted.org/packages/dd/8a/672ca1a3cbeabd1f511ca275a916c0514b747f4b85bdaae103b8fa92f307/lxml-6.1.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:21c3302068f50d1e8728c67c87ba92aa87043abee517aa2576cca1855326b405", size = 4758906, upload-time = "2026-04-18T04:35:08.098Z" }, + { url = "https://files.pythonhosted.org/packages/be/f1/ef4b691da85c916cb2feb1eec7414f678162798ac85e042fa164419ac05c/lxml-6.1.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:be10838781cb3be19251e276910cd508fe127e27c3242e50521521a0f3781690", size = 5620553, upload-time = "2026-04-18T04:35:11.23Z" }, + { url = "https://files.pythonhosted.org/packages/59/17/94e81def74107809755ac2782fdad4404420f1c92ca83433d117a6d5acf0/lxml-6.1.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2173a7bffe97667bbf0767f8a99e587740a8c56fdf3befac4b09cb29a80276fd", size = 5229458, upload-time = "2026-04-18T04:35:14.254Z" }, + { url = "https://files.pythonhosted.org/packages/21/55/c4be91b0f830a871fc1b0d730943d56013b683d4671d5198260e2eae722b/lxml-6.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c6854e9cf99c84beb004eecd7d3a3868ef1109bf2b1df92d7bc11e96a36c2180", size = 5247861, upload-time = "2026-04-18T04:35:17.006Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ca/77123e4d77df3cb1e968ade7b1f808f5d3a5c1c96b18a33895397de292c1/lxml-6.1.0-cp314-cp314t-win32.whl", hash = "sha256:00750d63ef0031a05331b9223463b1c7c02b9004cef2346a5b2877f0f9494dd2", size = 3897377, upload-time = "2026-04-18T04:32:07.656Z" }, + { url = "https://files.pythonhosted.org/packages/64/ce/3554833989d074267c063209bae8b09815e5656456a2d332b947806b05ff/lxml-6.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:80410c3a7e3c617af04de17caa9f9f20adaa817093293d69eae7d7d0522836f5", size = 4392701, upload-time = "2026-04-18T04:32:12.113Z" }, + { url = "https://files.pythonhosted.org/packages/2b/a0/9b916c68c0e57752c07f8f64b30138d9d4059dbeb27b90274dedbea128ff/lxml-6.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:26dd9f57ee3bd41e7d35b4c98a2ffd89ed11591649f421f0ec19f67d50ec67ac", size = 3817120, upload-time = "2026-04-18T04:32:15.803Z" }, +] + +[[package]] +name = "lxml-stubs" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/99/da/1a3a3e5d159b249fc2970d73437496b908de8e4716a089c69591b4ffa6fd/lxml-stubs-0.5.1.tar.gz", hash = "sha256:e0ec2aa1ce92d91278b719091ce4515c12adc1d564359dfaf81efa7d4feab79d", size = 14778, upload-time = "2024-01-10T09:37:46.521Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/c9/e0f8e4e6e8a69e5959b06499582dca6349db6769cc7fdfb8a02a7c75a9ae/lxml_stubs-0.5.1-py3-none-any.whl", hash = "sha256:1f689e5dbc4b9247cb09ae820c7d34daeb1fdbd1db06123814b856dae7787272", size = 13584, upload-time = "2024-01-10T09:37:44.931Z" }, +] + +[[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/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 = "matplotlib-inline" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/74/97e72a36efd4ae2bccb3463284300f8953f199b5ffbc04cbbb0ec78f74b1/matplotlib_inline-0.2.1.tar.gz", hash = "sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe", size = 8110, upload-time = "2025-10-23T09:00:22.126Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "msgpack" +version = "1.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581, upload-time = "2025-10-08T09:15:56.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/bd/8b0d01c756203fbab65d265859749860682ccd2a59594609aeec3a144efa/msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa", size = 81939, upload-time = "2025-10-08T09:15:01.472Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb", size = 85064, upload-time = "2025-10-08T09:15:03.764Z" }, + { url = "https://files.pythonhosted.org/packages/f2/60/a064b0345fc36c4c3d2c743c82d9100c40388d77f0b48b2f04d6041dbec1/msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f", size = 417131, upload-time = "2025-10-08T09:15:05.136Z" }, + { url = "https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42", size = 427556, upload-time = "2025-10-08T09:15:06.837Z" }, + { url = "https://files.pythonhosted.org/packages/f5/87/ffe21d1bf7d9991354ad93949286f643b2bb6ddbeab66373922b44c3b8cc/msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9", size = 404920, upload-time = "2025-10-08T09:15:08.179Z" }, + { url = "https://files.pythonhosted.org/packages/ff/41/8543ed2b8604f7c0d89ce066f42007faac1eaa7d79a81555f206a5cdb889/msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620", size = 415013, upload-time = "2025-10-08T09:15:09.83Z" }, + { url = "https://files.pythonhosted.org/packages/41/0d/2ddfaa8b7e1cee6c490d46cb0a39742b19e2481600a7a0e96537e9c22f43/msgpack-1.1.2-cp312-cp312-win32.whl", hash = "sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029", size = 65096, upload-time = "2025-10-08T09:15:11.11Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b", size = 72708, upload-time = "2025-10-08T09:15:12.554Z" }, + { url = "https://files.pythonhosted.org/packages/c5/31/5b1a1f70eb0e87d1678e9624908f86317787b536060641d6798e3cf70ace/msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69", size = 64119, upload-time = "2025-10-08T09:15:13.589Z" }, + { url = "https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf", size = 81212, upload-time = "2025-10-08T09:15:14.552Z" }, + { url = "https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7", size = 84315, upload-time = "2025-10-08T09:15:15.543Z" }, + { url = "https://files.pythonhosted.org/packages/d3/68/93180dce57f684a61a88a45ed13047558ded2be46f03acb8dec6d7c513af/msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999", size = 412721, upload-time = "2025-10-08T09:15:16.567Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e", size = 424657, upload-time = "2025-10-08T09:15:17.825Z" }, + { url = "https://files.pythonhosted.org/packages/38/f8/4398c46863b093252fe67368b44edc6c13b17f4e6b0e4929dbf0bdb13f23/msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162", size = 402668, upload-time = "2025-10-08T09:15:19.003Z" }, + { url = "https://files.pythonhosted.org/packages/28/ce/698c1eff75626e4124b4d78e21cca0b4cc90043afb80a507626ea354ab52/msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794", size = 419040, upload-time = "2025-10-08T09:15:20.183Z" }, + { url = "https://files.pythonhosted.org/packages/67/32/f3cd1667028424fa7001d82e10ee35386eea1408b93d399b09fb0aa7875f/msgpack-1.1.2-cp313-cp313-win32.whl", hash = "sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c", size = 65037, upload-time = "2025-10-08T09:15:21.416Z" }, + { url = "https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9", size = 72631, upload-time = "2025-10-08T09:15:22.431Z" }, + { url = "https://files.pythonhosted.org/packages/e5/db/0314e4e2db56ebcf450f277904ffd84a7988b9e5da8d0d61ab2d057df2b6/msgpack-1.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84", size = 64118, upload-time = "2025-10-08T09:15:23.402Z" }, + { url = "https://files.pythonhosted.org/packages/22/71/201105712d0a2ff07b7873ed3c220292fb2ea5120603c00c4b634bcdafb3/msgpack-1.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00", size = 81127, upload-time = "2025-10-08T09:15:24.408Z" }, + { url = "https://files.pythonhosted.org/packages/1b/9f/38ff9e57a2eade7bf9dfee5eae17f39fc0e998658050279cbb14d97d36d9/msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939", size = 84981, upload-time = "2025-10-08T09:15:25.812Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a9/3536e385167b88c2cc8f4424c49e28d49a6fc35206d4a8060f136e71f94c/msgpack-1.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e", size = 411885, upload-time = "2025-10-08T09:15:27.22Z" }, + { url = "https://files.pythonhosted.org/packages/2f/40/dc34d1a8d5f1e51fc64640b62b191684da52ca469da9cd74e84936ffa4a6/msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931", size = 419658, upload-time = "2025-10-08T09:15:28.4Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ef/2b92e286366500a09a67e03496ee8b8ba00562797a52f3c117aa2b29514b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014", size = 403290, upload-time = "2025-10-08T09:15:29.764Z" }, + { url = "https://files.pythonhosted.org/packages/78/90/e0ea7990abea5764e4655b8177aa7c63cdfa89945b6e7641055800f6c16b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2", size = 415234, upload-time = "2025-10-08T09:15:31.022Z" }, + { url = "https://files.pythonhosted.org/packages/72/4e/9390aed5db983a2310818cd7d3ec0aecad45e1f7007e0cda79c79507bb0d/msgpack-1.1.2-cp314-cp314-win32.whl", hash = "sha256:80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717", size = 66391, upload-time = "2025-10-08T09:15:32.265Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f1/abd09c2ae91228c5f3998dbd7f41353def9eac64253de3c8105efa2082f7/msgpack-1.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b", size = 73787, upload-time = "2025-10-08T09:15:33.219Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b0/9d9f667ab48b16ad4115c1935d94023b82b3198064cb84a123e97f7466c1/msgpack-1.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af", size = 66453, upload-time = "2025-10-08T09:15:34.225Z" }, + { url = "https://files.pythonhosted.org/packages/16/67/93f80545eb1792b61a217fa7f06d5e5cb9e0055bed867f43e2b8e012e137/msgpack-1.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a", size = 85264, upload-time = "2025-10-08T09:15:35.61Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/33c8a24959cf193966ef11a6f6a2995a65eb066bd681fd085afd519a57ce/msgpack-1.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b", size = 89076, upload-time = "2025-10-08T09:15:36.619Z" }, + { url = "https://files.pythonhosted.org/packages/fc/6b/62e85ff7193663fbea5c0254ef32f0c77134b4059f8da89b958beb7696f3/msgpack-1.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245", size = 435242, upload-time = "2025-10-08T09:15:37.647Z" }, + { url = "https://files.pythonhosted.org/packages/c1/47/5c74ecb4cc277cf09f64e913947871682ffa82b3b93c8dad68083112f412/msgpack-1.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90", size = 432509, upload-time = "2025-10-08T09:15:38.794Z" }, + { url = "https://files.pythonhosted.org/packages/24/a4/e98ccdb56dc4e98c929a3f150de1799831c0a800583cde9fa022fa90602d/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20", size = 415957, upload-time = "2025-10-08T09:15:40.238Z" }, + { url = "https://files.pythonhosted.org/packages/da/28/6951f7fb67bc0a4e184a6b38ab71a92d9ba58080b27a77d3e2fb0be5998f/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27", size = 422910, upload-time = "2025-10-08T09:15:41.505Z" }, + { url = "https://files.pythonhosted.org/packages/f0/03/42106dcded51f0a0b5284d3ce30a671e7bd3f7318d122b2ead66ad289fed/msgpack-1.1.2-cp314-cp314t-win32.whl", hash = "sha256:1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b", size = 75197, upload-time = "2025-10-08T09:15:42.954Z" }, + { url = "https://files.pythonhosted.org/packages/15/86/d0071e94987f8db59d4eeb386ddc64d0bb9b10820a8d82bcd3e53eeb2da6/msgpack-1.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff", size = 85772, upload-time = "2025-10-08T09:15:43.954Z" }, + { url = "https://files.pythonhosted.org/packages/81/f2/08ace4142eb281c12701fc3b93a10795e4d4dc7f753911d836675050f886/msgpack-1.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46", size = 70868, upload-time = "2025-10-08T09:15:44.959Z" }, +] + +[[package]] +name = "msgpack-numpy" +version = "0.4.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "msgpack" }, + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/94/61e8aee142733ebfdc400a05bdac6e1763c4514bba3b42743d223f388450/msgpack-numpy-0.4.8.tar.gz", hash = "sha256:c667d3180513422f9c7545be5eec5d296dcbb357e06f72ed39cc683797556e69", size = 10923, upload-time = "2022-06-09T03:43:08.739Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/5d/f25ac7d4fb77cbd53ddc6d05d833c6bf52b12770a44fa9a447eed470ca9a/msgpack_numpy-0.4.8-py2.py3-none-any.whl", hash = "sha256:773c19d4dfbae1b3c7b791083e2caf66983bb19b40901646f61d8731554ae3da", size = 6919, upload-time = "2022-06-09T03:43:06.82Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/05/32396bec30fb2263770ee910142f49c1476d08e8ad41abf8403806b520ce/numpy-2.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b", size = 16689272, upload-time = "2026-03-29T13:18:49.223Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f3/a983d28637bfcd763a9c7aafdb6d5c0ebf3d487d1e1459ffdb57e2f01117/numpy-2.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e", size = 14699573, upload-time = "2026-03-29T13:18:52.629Z" }, + { url = "https://files.pythonhosted.org/packages/9b/fd/e5ecca1e78c05106d98028114f5c00d3eddb41207686b2b7de3e477b0e22/numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842", size = 5204782, upload-time = "2026-03-29T13:18:55.579Z" }, + { url = "https://files.pythonhosted.org/packages/de/2f/702a4594413c1a8632092beae8aba00f1d67947389369b3777aed783fdca/numpy-2.4.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8", size = 6552038, upload-time = "2026-03-29T13:18:57.769Z" }, + { url = "https://files.pythonhosted.org/packages/7f/37/eed308a8f56cba4d1fdf467a4fc67ef4ff4bf1c888f5fc980481890104b1/numpy-2.4.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121", size = 15670666, upload-time = "2026-03-29T13:19:00.341Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0d/0e3ecece05b7a7e87ab9fb587855548da437a061326fff64a223b6dcb78a/numpy-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e", size = 16645480, upload-time = "2026-03-29T13:19:03.63Z" }, + { url = "https://files.pythonhosted.org/packages/34/49/f2312c154b82a286758ee2f1743336d50651f8b5195db18cdb63675ff649/numpy-2.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44", size = 17020036, upload-time = "2026-03-29T13:19:07.428Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e9/736d17bd77f1b0ec4f9901aaec129c00d59f5d84d5e79bba540ef12c2330/numpy-2.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d", size = 18368643, upload-time = "2026-03-29T13:19:10.775Z" }, + { url = "https://files.pythonhosted.org/packages/63/f6/d417977c5f519b17c8a5c3bc9e8304b0908b0e21136fe43bf628a1343914/numpy-2.4.4-cp312-cp312-win32.whl", hash = "sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827", size = 5961117, upload-time = "2026-03-29T13:19:13.464Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5b/e1deebf88ff431b01b7406ca3583ab2bbb90972bbe1c568732e49c844f7e/numpy-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a", size = 12320584, upload-time = "2026-03-29T13:19:16.155Z" }, + { url = "https://files.pythonhosted.org/packages/58/89/e4e856ac82a68c3ed64486a544977d0e7bdd18b8da75b78a577ca31c4395/numpy-2.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec", size = 10221450, upload-time = "2026-03-29T13:19:18.994Z" }, + { url = "https://files.pythonhosted.org/packages/14/1d/d0a583ce4fefcc3308806a749a536c201ed6b5ad6e1322e227ee4848979d/numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50", size = 16684933, upload-time = "2026-03-29T13:19:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/c1/62/2b7a48fbb745d344742c0277f01286dead15f3f68e4f359fbfcf7b48f70f/numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115", size = 14694532, upload-time = "2026-03-29T13:19:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af", size = 5199661, upload-time = "2026-03-29T13:19:28.31Z" }, + { url = "https://files.pythonhosted.org/packages/cd/da/464d551604320d1491bc345efed99b4b7034143a85787aab78d5691d5a0e/numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c", size = 6547539, upload-time = "2026-03-29T13:19:30.97Z" }, + { url = "https://files.pythonhosted.org/packages/7d/90/8d23e3b0dafd024bf31bdec225b3bb5c2dbfa6912f8a53b8659f21216cbf/numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103", size = 15668806, upload-time = "2026-03-29T13:19:33.887Z" }, + { url = "https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83", size = 16632682, upload-time = "2026-03-29T13:19:37.336Z" }, + { url = "https://files.pythonhosted.org/packages/34/fb/14570d65c3bde4e202a031210475ae9cde9b7686a2e7dc97ee67d2833b35/numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed", size = 17019810, upload-time = "2026-03-29T13:19:40.963Z" }, + { url = "https://files.pythonhosted.org/packages/8a/77/2ba9d87081fd41f6d640c83f26fb7351e536b7ce6dd9061b6af5904e8e46/numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959", size = 18357394, upload-time = "2026-03-29T13:19:44.859Z" }, + { url = "https://files.pythonhosted.org/packages/a2/23/52666c9a41708b0853fa3b1a12c90da38c507a3074883823126d4e9d5b30/numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed", size = 5959556, upload-time = "2026-03-29T13:19:47.661Z" }, + { url = "https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf", size = 12317311, upload-time = "2026-03-29T13:19:50.67Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d8/11490cddd564eb4de97b4579ef6bfe6a736cc07e94c1598590ae25415e01/numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d", size = 10222060, upload-time = "2026-03-29T13:19:54.229Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/dab4339177a905aad3e2221c915b35202f1ec30d750dd2e5e9d9a72b804b/numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5", size = 14822302, upload-time = "2026-03-29T13:19:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e4/0564a65e7d3d97562ed6f9b0fd0fb0a6f559ee444092f105938b50043876/numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7", size = 5327407, upload-time = "2026-03-29T13:20:00.601Z" }, + { url = "https://files.pythonhosted.org/packages/29/8d/35a3a6ce5ad371afa58b4700f1c820f8f279948cca32524e0a695b0ded83/numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93", size = 6647631, upload-time = "2026-03-29T13:20:02.855Z" }, + { url = "https://files.pythonhosted.org/packages/f4/da/477731acbd5a58a946c736edfdabb2ac5b34c3d08d1ba1a7b437fa0884df/numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e", size = 15727691, upload-time = "2026-03-29T13:20:06.004Z" }, + { url = "https://files.pythonhosted.org/packages/e6/db/338535d9b152beabeb511579598418ba0212ce77cf9718edd70262cc4370/numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40", size = 16681241, upload-time = "2026-03-29T13:20:09.417Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a9/ad248e8f58beb7a0219b413c9c7d8151c5d285f7f946c3e26695bdbbe2df/numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e", size = 17085767, upload-time = "2026-03-29T13:20:13.126Z" }, + { url = "https://files.pythonhosted.org/packages/b5/1a/3b88ccd3694681356f70da841630e4725a7264d6a885c8d442a697e1146b/numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392", size = 18403169, upload-time = "2026-03-29T13:20:17.096Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c9/fcfd5d0639222c6eac7f304829b04892ef51c96a75d479214d77e3ce6e33/numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008", size = 6083477, upload-time = "2026-03-29T13:20:20.195Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e3/3938a61d1c538aaec8ed6fd6323f57b0c2d2d2219512434c5c878db76553/numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8", size = 12457487, upload-time = "2026-03-29T13:20:22.946Z" }, + { url = "https://files.pythonhosted.org/packages/97/6a/7e345032cc60501721ef94e0e30b60f6b0bd601f9174ebd36389a2b86d40/numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233", size = 10292002, upload-time = "2026-03-29T13:20:25.909Z" }, + { url = "https://files.pythonhosted.org/packages/6e/06/c54062f85f673dd5c04cbe2f14c3acb8c8b95e3384869bb8cc9bff8cb9df/numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0", size = 16684353, upload-time = "2026-03-29T13:20:29.504Z" }, + { url = "https://files.pythonhosted.org/packages/4c/39/8a320264a84404c74cc7e79715de85d6130fa07a0898f67fb5cd5bd79908/numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a", size = 14704914, upload-time = "2026-03-29T13:20:33.547Z" }, + { url = "https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a", size = 5210005, upload-time = "2026-03-29T13:20:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/63/eb/fcc338595309910de6ecabfcef2419a9ce24399680bfb149421fa2df1280/numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b", size = 6544974, upload-time = "2026-03-29T13:20:39.014Z" }, + { url = "https://files.pythonhosted.org/packages/44/5d/e7e9044032a716cdfaa3fba27a8e874bf1c5f1912a1ddd4ed071bf8a14a6/numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a", size = 15684591, upload-time = "2026-03-29T13:20:42.146Z" }, + { url = "https://files.pythonhosted.org/packages/98/7c/21252050676612625449b4807d6b695b9ce8a7c9e1c197ee6216c8a65c7c/numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d", size = 16637700, upload-time = "2026-03-29T13:20:46.204Z" }, + { url = "https://files.pythonhosted.org/packages/b1/29/56d2bbef9465db24ef25393383d761a1af4f446a1df9b8cded4fe3a5a5d7/numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252", size = 17035781, upload-time = "2026-03-29T13:20:50.242Z" }, + { url = "https://files.pythonhosted.org/packages/e3/2b/a35a6d7589d21f44cea7d0a98de5ddcbb3d421b2622a5c96b1edf18707c3/numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f", size = 18362959, upload-time = "2026-03-29T13:20:54.019Z" }, + { url = "https://files.pythonhosted.org/packages/64/c9/d52ec581f2390e0f5f85cbfd80fb83d965fc15e9f0e1aec2195faa142cde/numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc", size = 6008768, upload-time = "2026-03-29T13:20:56.912Z" }, + { url = "https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74", size = 12449181, upload-time = "2026-03-29T13:20:59.548Z" }, + { url = "https://files.pythonhosted.org/packages/70/2e/14cda6f4d8e396c612d1bf97f22958e92148801d7e4f110cabebdc0eef4b/numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb", size = 10496035, upload-time = "2026-03-29T13:21:02.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e8/8fed8c8d848d7ecea092dc3469643f9d10bc3a134a815a3b033da1d2039b/numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e", size = 14824958, upload-time = "2026-03-29T13:21:05.671Z" }, + { url = "https://files.pythonhosted.org/packages/05/1a/d8007a5138c179c2bf33ef44503e83d70434d2642877ee8fbb230e7c0548/numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113", size = 5330020, upload-time = "2026-03-29T13:21:08.635Z" }, + { url = "https://files.pythonhosted.org/packages/99/64/ffb99ac6ae93faf117bcbd5c7ba48a7f45364a33e8e458545d3633615dda/numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d", size = 6650758, upload-time = "2026-03-29T13:21:10.949Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6e/795cc078b78a384052e73b2f6281ff7a700e9bf53bcce2ee579d4f6dd879/numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d", size = 15729948, upload-time = "2026-03-29T13:21:14.047Z" }, + { url = "https://files.pythonhosted.org/packages/5f/86/2acbda8cc2af5f3d7bfc791192863b9e3e19674da7b5e533fded124d1299/numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f", size = 16679325, upload-time = "2026-03-29T13:21:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/bc/59/cafd83018f4aa55e0ac6fa92aa066c0a1877b77a615ceff1711c260ffae8/numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0", size = 17084883, upload-time = "2026-03-29T13:21:21.106Z" }, + { url = "https://files.pythonhosted.org/packages/f0/85/a42548db84e65ece46ab2caea3d3f78b416a47af387fcbb47ec28e660dc2/numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150", size = 18403474, upload-time = "2026-03-29T13:21:24.828Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ad/483d9e262f4b831000062e5d8a45e342166ec8aaa1195264982bca267e62/numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", size = 6155500, upload-time = "2026-03-29T13:21:28.205Z" }, + { url = "https://files.pythonhosted.org/packages/c7/03/2fc4e14c7bd4ff2964b74ba90ecb8552540b6315f201df70f137faa5c589/numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", size = 12637755, upload-time = "2026-03-29T13:21:31.107Z" }, + { url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" }, +] + +[[package]] +name = "numpydoc" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/3c/dfccc9e7dee357fb2aa13c3890d952a370dd0ed071e0f7ed62ed0df567c1/numpydoc-1.10.0.tar.gz", hash = "sha256:3f7970f6eee30912260a6b31ac72bba2432830cd6722569ec17ee8d3ef5ffa01", size = 94027, upload-time = "2025-12-02T16:39:12.937Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/5e/3a6a3e90f35cea3853c45e5d5fb9b7192ce4384616f932cf7591298ab6e1/numpydoc-1.10.0-py3-none-any.whl", hash = "sha256:3149da9874af890bcc2a82ef7aae5484e5aa81cb2778f08e3c307ba6d963721b", size = 69255, upload-time = "2025-12-02T16:39:11.561Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.41.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/8e/3778a7e87801d994869a9396b9fc2a289e5f9be91ff54a27d41eace494b0/opentelemetry_api-1.41.0.tar.gz", hash = "sha256:9421d911326ec12dee8bc933f7839090cad7a3f13fcfb0f9e82f8174dc003c09", size = 71416, upload-time = "2026-04-09T14:38:34.544Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/ee/99ab786653b3bda9c37ade7e24a7b607a1b1f696063172768417539d876d/opentelemetry_api-1.41.0-py3-none-any.whl", hash = "sha256:0e77c806e6a89c9e4f8d372034622f3e1418a11bdbe1c80a50b3d3397ad0fa4f", size = 69007, upload-time = "2026-04-09T14:38:11.833Z" }, +] + +[[package]] +name = "ophyd" +version = "1.11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "networkx" }, + { name = "numpy" }, + { name = "opentelemetry-api" }, + { name = "packaging" }, + { name = "pint" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/4a/d41c587299c62dcee80af425e29fddb96ba6ee8d8cd83d05acc959316554/ophyd-1.11.1.tar.gz", hash = "sha256:c813fa90f7d070b8da2a875ace9f50193a29d4c4555838c9c766947d971bbb9c", size = 313339, upload-time = "2026-03-31T19:03:33.948Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/fc/a4bf334181e291ced9c9b09178394e6cd42ec6e8e0ef2e890c5b6c8e4ab5/ophyd-1.11.1-py3-none-any.whl", hash = "sha256:af3032fe4cc74a2f219d0dfd0c65454faff397b14b194ca3fc7a3b5ec89c1ceb", size = 280052, upload-time = "2026-03-31T19:03:32.394Z" }, +] + +[[package]] +name = "packaging" +version = "26.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/de/0d2b39fb4af88a0258f3bac87dfcbb48e73fbdea4a2ed0e2213f9a4c2f9a/packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de", size = 215519, upload-time = "2026-04-14T21:12:49.362Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831, upload-time = "2026-04-14T21:12:47.56Z" }, +] + +[[package]] +name = "parso" +version = "0.8.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/76/a1e769043c0c0c9fe391b702539d594731a4362334cdf4dc25d0c09761e7/parso-0.8.6.tar.gz", hash = "sha256:2b9a0332696df97d454fa67b81618fd69c35a7b90327cbe6ba5c92d2c68a7bfd", size = 401621, upload-time = "2026-02-09T15:45:24.425Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.py3-none-any.whl", hash = "sha256:2c549f800b70a5c4952197248825584cb00f033b29c692671d3bf08bf380baff", size = 106894, upload-time = "2026-02-09T15:45:21.391Z" }, +] + +[[package]] +name = "pcdscalc" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "ophyd" }, + { name = "periodictable" }, + { name = "scipy" }, + { name = "xraydb" }, + { name = "xraylib" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/fb/1c38e1c5a684d5f98b27aa9105e7f7cefee95fe6a6282d09e3e49e22752a/pcdscalc-0.6.1.tar.gz", hash = "sha256:abadb41c4ceedea269a2b0d250981c7f4b894edfb02cd0088ec53603a7e01d4d", size = 41986, upload-time = "2025-03-15T21:35:36.784Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/54/8e02ad3ae19944df33202bed047c89cc36797400079d37aef50891e4e9d4/pcdscalc-0.6.1-py3-none-any.whl", hash = "sha256:19034db598d9f86b4d602e7a3ec28ba5ac7738b1333bb751659618ba9b03d39b", size = 35371, upload-time = "2025-03-15T21:35:35.831Z" }, +] + +[[package]] +name = "pcdsdevices" +version = "10.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bluesky" }, + { name = "happi" }, + { name = "jsonschema" }, + { name = "lightpath" }, + { name = "numpy" }, + { name = "ophyd" }, + { name = "pcdscalc" }, + { name = "pcdsutils" }, + { name = "prettytable" }, + { name = "pyepics" }, + { name = "pytmc" }, + { name = "pyyaml" }, + { name = "schema" }, + { name = "scipy" }, + { name = "sympy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/36/ab2e5f8027f04c107c832a34dfc3a6b218a491b5ab4c384dd4f45cf656ad/pcdsdevices-10.3.0.tar.gz", hash = "sha256:a3b8fabe5d9404a8022c4c1e2ed7870494240864a27520d7ecca40774a85a198", size = 550817, upload-time = "2026-02-17T19:18:53.306Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/a7/474553377ff69327e46547b2c27c201e78a4efbd6e3be0eed91e4ffefd71/pcdsdevices-10.3.0-py3-none-any.whl", hash = "sha256:9a6fcc76948e8096317bb98abb25bff2cb2e645e8942af7d5d43fd3626f69244", size = 612308, upload-time = "2026-02-17T19:18:51.331Z" }, +] + +[[package]] +name = "pcdsutils" +version = "0.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "prettytable" }, + { name = "pypandoc" }, + { name = "pyyaml" }, + { name = "qtpy" }, + { name = "qtpyinheritance" }, + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/0b/01b717b89317058551d5061015cefc8fea41d419327c48b234b878f8c28b/pcdsutils-0.14.2.tar.gz", hash = "sha256:10572df8ebf26608a0d27fbc0db2839990f243051295665d3cf324625aaa0406", size = 62668, upload-time = "2025-08-21T17:53:58.245Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/13/dfd93864e3e16c991b82a75737ba5439bb1d49b674b303ff1f371be13590/pcdsutils-0.14.2-py3-none-any.whl", hash = "sha256:997b699c0399de48a6d8a6d2ac36c95f5162dca0db1ffd26cd96e320daaaf916", size = 55739, upload-time = "2025-08-21T17:53:57.318Z" }, +] + +[[package]] +name = "periodictable" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pyparsing" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/0d/f494a2ec62ab56ffd44da4f236db374421fe0cd1e39d9ebc9785751eb432/periodictable-2.1.0-py3-none-any.whl", hash = "sha256:e9155d2bf5ac10050abeff2f99096d4f04312c0c8a6bb432e28744367c5064b3", size = 830749, upload-time = "2026-02-28T00:08:27.135Z" }, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, +] + +[[package]] +name = "pint" +version = "0.25.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flexcache" }, + { name = "flexparser" }, + { name = "platformdirs" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/9d/b1379cdbd33a49d17d627bc24e2b63cca06a1c5343b38072d2889499e82e/pint-0.25.3.tar.gz", hash = "sha256:f8f5df6cf65314d74da1ade1bf96f8e3e4d0c41b51577ac53c49e7d44ca5acee", size = 255106, upload-time = "2026-03-19T21:57:08.72Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/dd/a9fe6a0a09512da23951c68bf36466aeecd89def3183dc095edbc807ddc5/pint-0.25.3-py3-none-any.whl", hash = "sha256:27eb25143bd5de9fcc4d5a4b484f16faf6b4615aa93ece6b3373a8c1a3c1b97d", size = 307488, upload-time = "2026-03-19T21:57:07.022Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, +] + +[[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 = "ply" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/69/882ee5c9d017149285cab114ebeab373308ef0f874fcdac9beb90e0ac4da/ply-3.11.tar.gz", hash = "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3", size = 159130, upload-time = "2018-02-15T19:01:31.097Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl", hash = "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce", size = 49567, upload-time = "2018-02-15T19:01:27.172Z" }, +] + +[[package]] +name = "prettytable" +version = "3.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/45/b0847d88d6cfeb4413566738c8bbf1e1995fad3d42515327ff32cc1eb578/prettytable-3.17.0.tar.gz", hash = "sha256:59f2590776527f3c9e8cf9fe7b66dd215837cca96a9c39567414cbc632e8ddb0", size = 67892, upload-time = "2025-11-14T17:33:20.212Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/8c/83087ebc47ab0396ce092363001fa37c17153119ee282700c0713a195853/prettytable-3.17.0-py3-none-any.whl", hash = "sha256:aad69b294ddbe3e1f95ef8886a060ed1666a0b83018bbf56295f6f226c43d287", size = 34433, upload-time = "2025-11-14T17:33:19.093Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, +] + +[[package]] +name = "pure-eval" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, +] + +[[package]] +name = "py-cpuinfo" +version = "9.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/a8/d832f7293ebb21690860d2e01d8115e5ff6f2ae8bbdc953f0eb0fa4bd2c7/py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690", size = 104716, upload-time = "2022-10-25T20:38:06.303Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5", size = 22335, upload-time = "2022-10-25T20:38:27.636Z" }, +] + +[[package]] +name = "pydm" +version = "1.28.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "entrypoints" }, + { name = "numpy" }, + { name = "pyepics" }, + { name = "pyqtgraph" }, + { name = "qtpy" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/b6/413d601572700869381a9c9b00237499b7e8da949fa5bb45f1d6498af1a4/pydm-1.28.3.tar.gz", hash = "sha256:168b1a27894daa35300b4009ee088c8ace1c637bb74301be01e48a52ef85f9f0", size = 18209399, upload-time = "2026-03-17T18:16:20.961Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/0d/fe64dcf3c95ad22624b85787cbdae1cd91b32bbf7ba03561d7d1effc6cb5/pydm-1.28.3-py3-none-any.whl", hash = "sha256:d0693580c47ac8a973cda2b20bf517080b1bbb48323e83bff90c230dec30080c", size = 770425, upload-time = "2026-03-17T18:16:18.292Z" }, +] + +[[package]] +name = "pyepics" +version = "3.5.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pyparsing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/56/b7edf871ec2d81ecc600a7687cf9c536759f31ea482e8aec453c6dd12d21/pyepics-3.5.9.tar.gz", hash = "sha256:78222c1a8aff55bc7a93bdcb6eea9cb544fa8b9122daed1e7ea5b5e87269d45c", size = 6149589, upload-time = "2025-12-17T17:16:33.913Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/83/7dafb09fbc3efe9d00c4667d22b32b53d08e8a676fa164c6dd8f5debe85e/pyepics-3.5.9-py3-none-any.whl", hash = "sha256:b9863cc55a58542f0a28ad04621d4471f649e9cacfa4ccf346a58d6ba158640c", size = 5332286, upload-time = "2025-12-17T17:16:31.93Z" }, +] + +[[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 = "pypandoc" +version = "1.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/d6/410615fc433e5d1eacc00db2044ae2a9c82302df0d35366fe2bd15de024d/pypandoc-1.17.tar.gz", hash = "sha256:51179abfd6e582a25ed03477541b48836b5bba5a4c3b282a547630793934d799", size = 69071, upload-time = "2026-03-14T22:39:07.21Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/86/e2ffa604eacfbec3f430b1d850e7e04c4101eca1a5828f9ae54bf51dfba4/pypandoc-1.17-py3-none-any.whl", hash = "sha256:01fdbffa61edb9f8e82e8faad6954efcb7b6f8f0634aead4d89e322a00225a67", size = 23554, upload-time = "2026-03-14T22:38:46.007Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + +[[package]] +name = "pyqt5" +version = "5.15.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyqt5-qt5" }, + { name = "pyqt5-sip" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/07/c9ed0bd428df6f87183fca565a79fee19fa7c88c7f00a7f011ab4379e77a/PyQt5-5.15.11.tar.gz", hash = "sha256:fda45743ebb4a27b4b1a51c6d8ef455c4c1b5d610c90d2934c7802b5c1557c52", size = 3216775, upload-time = "2024-07-19T08:39:57.756Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/64/42ec1b0bd72d87f87bde6ceb6869f444d91a2d601f2e67cd05febc0346a1/PyQt5-5.15.11-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:c8b03dd9380bb13c804f0bdb0f4956067f281785b5e12303d529f0462f9afdc2", size = 6579776, upload-time = "2024-07-19T08:39:19.775Z" }, + { url = "https://files.pythonhosted.org/packages/49/f5/3fb696f4683ea45d68b7e77302eff173493ac81e43d63adb60fa760b9f91/PyQt5-5.15.11-cp38-abi3-macosx_11_0_x86_64.whl", hash = "sha256:6cd75628f6e732b1ffcfe709ab833a0716c0445d7aec8046a48d5843352becb6", size = 7016415, upload-time = "2024-07-19T08:39:32.977Z" }, + { url = "https://files.pythonhosted.org/packages/b4/8c/4065950f9d013c4b2e588fe33cf04e564c2322842d84dbcbce5ba1dc28b0/PyQt5-5.15.11-cp38-abi3-manylinux_2_17_x86_64.whl", hash = "sha256:cd672a6738d1ae33ef7d9efa8e6cb0a1525ecf53ec86da80a9e1b6ec38c8d0f1", size = 8188103, upload-time = "2024-07-19T08:39:40.561Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f0/ae5a5b4f9b826b29ea4be841b2f2d951bcf5ae1d802f3732b145b57c5355/PyQt5-5.15.11-cp38-abi3-win32.whl", hash = "sha256:76be0322ceda5deecd1708a8d628e698089a1cea80d1a49d242a6d579a40babd", size = 5433308, upload-time = "2024-07-19T08:39:46.932Z" }, + { url = "https://files.pythonhosted.org/packages/56/d5/68eb9f3d19ce65df01b6c7b7a577ad3bbc9ab3a5dd3491a4756e71838ec9/PyQt5-5.15.11-cp38-abi3-win_amd64.whl", hash = "sha256:bdde598a3bb95022131a5c9ea62e0a96bd6fb28932cc1619fd7ba211531b7517", size = 6865864, upload-time = "2024-07-19T08:39:53.572Z" }, +] + +[[package]] +name = "pyqt5-qt5" +version = "5.15.18" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/90/bf01ac2132400997a3474051dd680a583381ebf98b2f5d64d4e54138dc42/pyqt5_qt5-5.15.18-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:8bb997eb903afa9da3221a0c9e6eaa00413bbeb4394d5706118ad05375684767", size = 39715743, upload-time = "2025-11-09T12:56:42.936Z" }, + { url = "https://files.pythonhosted.org/packages/24/8e/76366484d9f9dbe28e3bdfc688183433a7b82e314216e9b14c89e5fab690/pyqt5_qt5-5.15.18-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c656af9c1e6aaa7f59bf3d8995f2fa09adbf6762b470ed284c31dca80d686a26", size = 36798484, upload-time = "2025-11-09T12:56:59.998Z" }, + { url = "https://files.pythonhosted.org/packages/9a/46/ffe177f99f897a59dc237a20059020427bd2d3853d713992b8081933ddfe/pyqt5_qt5-5.15.18-py3-none-manylinux2014_x86_64.whl", hash = "sha256:bf2457e6371969736b4f660a0c153258fa03dbc6a181348218e6f05421682af7", size = 60864590, upload-time = "2025-11-09T12:57:26.724Z" }, +] + +[[package]] +name = "pyqt5-sip" +version = "12.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/31/5ef342de9faee0f3801088946ae103db9b9eaeba3d6a64fefd5ce74df244/pyqt5_sip-12.18.0.tar.gz", hash = "sha256:71c37db75a0664325de149f43e2a712ec5fa1f90429a21dafbca005cb6767f94", size = 104143, upload-time = "2026-01-13T15:53:19.576Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/61/6d78d702016ac23d2b97634a3b6a831c3f7735f0552a1c8b058db96005d1/pyqt5_sip-12.18.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b29e4cda24748e59e5bd1bdad4812091a86b4b5b08c38b7f781eb55a5166f2b7", size = 124614, upload-time = "2026-01-13T15:52:57.59Z" }, + { url = "https://files.pythonhosted.org/packages/19/bf/8f3efa10ddd3e76c1253865340ab7c2960ef96681d732b1f666c77430612/pyqt5_sip-12.18.0-cp312-cp312-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:163c2bba5e637c2222ec17d82a2c5aa158184a191923eb7d137cf4cfa0399529", size = 339412, upload-time = "2026-01-13T15:53:00.563Z" }, + { url = "https://files.pythonhosted.org/packages/72/48/f1bcf6729d01bae6729cd790b22fd579dbe34014e8be031e6f10c5b9b2aa/pyqt5_sip-12.18.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ead5e0a64ad852ac60797989d8444a6a5bd834768536b04a07b40b2937d922f6", size = 282376, upload-time = "2026-01-13T15:52:59.172Z" }, + { url = "https://files.pythonhosted.org/packages/dd/b7/d84c764ac9f1366be561255ec9bd88ee224fefdbdb349aee250f3003f0ca/pyqt5_sip-12.18.0-cp312-cp312-win32.whl", hash = "sha256:993fe3ed9a62a92e770f32d5344e3df56c2cacf1471f01b7feaf04818a2df1c4", size = 49523, upload-time = "2026-01-13T15:53:03.068Z" }, + { url = "https://files.pythonhosted.org/packages/ab/e7/ef87178d5afa5f63be38556dc0df8af89f9bf74f2555f4dab6824c0fd150/pyqt5_sip-12.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:9b689e02e400abd1ce0a30cd6eae8eceabcf1bbba0395cb5c86e64ba74351d68", size = 58001, upload-time = "2026-01-13T15:53:02.15Z" }, + { url = "https://files.pythonhosted.org/packages/79/67/8d43d0fea10ff48ddecc8534aead8b855dc80df80653b8b1bf9e1f993063/pyqt5_sip-12.18.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9254e5dd7676b76503ba20edcc919e7ac4a97b6c70a6fb2f9dba9e13b4c60509", size = 124605, upload-time = "2026-01-13T15:53:04.991Z" }, + { url = "https://files.pythonhosted.org/packages/48/2a/b08bc8efeb49c50c6cdac11417dc2c8eaefcac2f0a6382eae7b26dc0f232/pyqt5_sip-12.18.0-cp313-cp313-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c969631ada7293a81e1012b2264a62d69a91995b517586489dfe24421b87b9af", size = 339918, upload-time = "2026-01-13T15:53:08.502Z" }, + { url = "https://files.pythonhosted.org/packages/b6/99/24f82437b2f073cf39296b7c731b6a8bc0f5207911fdd93841a0ea9abe42/pyqt5_sip-12.18.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d84ac384a63285132e67762c87681191c25e28a1df7560287ec3889d9eb223b5", size = 282088, upload-time = "2026-01-13T15:53:06.632Z" }, + { url = "https://files.pythonhosted.org/packages/3e/27/20d3924943df34361fae9c6a0489ae89d0b07571693245c61678d185e4a4/pyqt5_sip-12.18.0-cp313-cp313-win32.whl", hash = "sha256:95bba4670ecf5cba73958b85aa2087c17838a402ed251c38e68060c7665c998b", size = 49501, upload-time = "2026-01-13T15:53:11.159Z" }, + { url = "https://files.pythonhosted.org/packages/3f/36/e251623c12968730730512a9e5150430e36246afbe64894610190b896f61/pyqt5_sip-12.18.0-cp313-cp313-win_amd64.whl", hash = "sha256:aac4adc37df2f2ac1dc259409be1900f07332d140a12c9db7c84112cef64ff59", size = 58076, upload-time = "2026-01-13T15:53:09.928Z" }, + { url = "https://files.pythonhosted.org/packages/37/3a/b46a0116b1aacbb6156b2957eb5cb928c94b49f4626eb2540ca8d16ee757/pyqt5_sip-12.18.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8372ec8704bfd5e09942d0d055a1657eb4f702f4b30847a5e59df0496f99d67f", size = 124594, upload-time = "2026-01-13T15:53:13.159Z" }, + { url = "https://files.pythonhosted.org/packages/58/63/df3037f11391c25c5b0ab233d22e58b8f056cb1ce16d7ecadb844421ce75/pyqt5_sip-12.18.0-cp314-cp314-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fdb45c7cd2af7eccd7370b994d432bfc7965079f845392760724f26771bb59dc", size = 339056, upload-time = "2026-01-13T15:53:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e7/4f96b84520b8f8b7502682fd43f68f63ca6572b5858f56e5f61c76a54fe2/pyqt5_sip-12.18.0-cp314-cp314-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:92abe984becbde768954d6d0951f56d80a9868d2fd9e738e61fc944f0ff83dd6", size = 282439, upload-time = "2026-01-13T15:53:14.856Z" }, + { url = "https://files.pythonhosted.org/packages/79/8e/ccdf20d373ceba83e1d1b7f818505c375208ffde4a96376dc7dbe592406c/pyqt5_sip-12.18.0-cp314-cp314-win32.whl", hash = "sha256:bd9e3c6f81346f1b08d6db02305cdee20c009b43aa083d44ee2de47a7da0e123", size = 50713, upload-time = "2026-01-13T15:53:18.634Z" }, + { url = "https://files.pythonhosted.org/packages/7f/21/8486ed45977be615ec5371b24b47298b1cb0e1a455b419eddd0215078dba/pyqt5_sip-12.18.0-cp314-cp314-win_amd64.whl", hash = "sha256:6d948f1be619c645cd3bda54952bfdc1aef7c79242dccea6a6858748e61114b9", size = 59622, upload-time = "2026-01-13T15:53:17.714Z" }, +] + +[[package]] +name = "pyqtgraph" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, + { name = "numpy" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/36/4c242f81fdcbfa4fb62a5645f6af79191f4097a0577bd5460c24f19cc4ef/pyqtgraph-0.14.0-py3-none-any.whl", hash = "sha256:7abb7c3e17362add64f8711b474dffac5e7b0e9245abdf992e9a44119b7aa4f5", size = 1924755, upload-time = "2025-11-16T19:43:22.251Z" }, +] + +[[package]] +name = "pyreadline3" +version = "3.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/49/4cea918a08f02817aabae639e3d0ac046fef9f9180518a3ad394e22da148/pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7", size = 99839, upload-time = "2024-09-19T02:40:10.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/dc/491b7661614ab97483abf2056be1deee4dc2490ecbf7bff9ab5cdbac86e1/pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6", size = 83178, upload-time = "2024-09-19T02:40:08.598Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + +[[package]] +name = "pytest-benchmark" +version = "5.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "py-cpuinfo" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/24/34/9f732b76456d64faffbef6232f1f9dbec7a7c4999ff46282fa418bd1af66/pytest_benchmark-5.2.3.tar.gz", hash = "sha256:deb7317998a23c650fd4ff76e1230066a76cb45dcece0aca5607143c619e7779", size = 341340, upload-time = "2025-11-09T18:48:43.215Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/29/e756e715a48959f1c0045342088d7ca9762a2f509b945f362a316e9412b7/pytest_benchmark-5.2.3-py3-none-any.whl", hash = "sha256:bc839726ad20e99aaa0d11a127445457b4219bdb9e80a1afc4b51da7f96b0803", size = 45255, upload-time = "2025-11-09T18:48:39.765Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] + +[[package]] +name = "pytest-qt" +version = "4.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pluggy" }, + { name = "pytest" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/61/8bdec02663c18bf5016709b909411dce04a868710477dc9b9844ffcf8dd2/pytest_qt-4.5.0.tar.gz", hash = "sha256:51620e01c488f065d2036425cbc1cbcf8a6972295105fd285321eb47e66a319f", size = 128702, upload-time = "2025-07-01T17:24:39.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/d0/8339b888ad64a3d4e508fed8245a402b503846e1972c10ad60955883dcbb/pytest_qt-4.5.0-py3-none-any.whl", hash = "sha256:ed21ea9b861247f7d18090a26bfbda8fb51d7a8a7b6f776157426ff2ccf26eff", size = 37214, upload-time = "2025-07-01T17:24:38.226Z" }, +] + +[[package]] +name = "pytest-timeout" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973, upload-time = "2025-05-05T19:44:34.99Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" }, +] + +[[package]] +name = "pytmc" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "epics-pypdb" }, + { name = "jinja2" }, + { name = "lxml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/b7/20fc0be93937e1249fcd7f658e1c8275e544cc61437d09754f2e74e1c62c/pytmc-2.20.0.tar.gz", hash = "sha256:e1a001cff2413e9dd930085db240178f7f698aa9c464d3394143ee938b224e10", size = 468055, upload-time = "2026-03-26T16:49:24.264Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/ed/393de32d1d073afef85bb29aa04fca10203eb1e956ad9c04181846efdf50/pytmc-2.20.0-py3-none-any.whl", hash = "sha256:d97607bfd87ca74b2744ca792d6a2cb780b6a2706b283c50817673a46309b669", size = 477112, upload-time = "2026-03-26T16:49:23.103Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "qdarkstyle" +version = "3.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "qtpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/1e/5bf72a61a7636058e25eaa7503c050dae9445de75fad6f71ba08f2174e49/QDarkStyle-3.2.3.tar.gz", hash = "sha256:0c0b7f74a6e92121008992b369bab60468157db1c02cd30d64a5e9a3b402f1ae", size = 700957, upload-time = "2023-11-28T19:54:51.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/7d/c3c10498430dadcea4def5faddf71cd199e577d20a125e7ef1e9d7bdbbfa/QDarkStyle-3.2.3-py2.py3-none-any.whl", hash = "sha256:ea980ee426d594909cf1058306832af71ff6cbad6f69237b036d1550635aefbc", size = 871762, upload-time = "2023-11-28T19:54:48.123Z" }, +] + +[[package]] +name = "qtawesome" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "qtpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/bc/475c8df94de4f358bbf7703747c13b16ae3c0c4cda50d5243b99cb45e2a8/qtawesome-1.4.2.tar.gz", hash = "sha256:b2bf9351beb335095006892796f072ffd9755a2d7e5113dc71918dcd9ba4ef4a", size = 2614207, upload-time = "2026-04-10T18:49:46.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/b1/da1d826ccc9258674b26dd0abcbbb1c55cab06e7fbf2697518b67edc79fc/qtawesome-1.4.2-py3-none-any.whl", hash = "sha256:dbf08524428fa2df73918ce362153254cd44f089380576d84bfaad8f40eece45", size = 2593557, upload-time = "2026-04-10T18:49:44.219Z" }, +] + +[[package]] +name = "qtpy" +version = "2.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/01/392eba83c8e47b946b929d7c46e0f04b35e9671f8bb6fc36b6f7945b4de8/qtpy-2.4.3.tar.gz", hash = "sha256:db744f7832e6d3da90568ba6ccbca3ee2b3b4a890c3d6fbbc63142f6e4cdf5bb", size = 66982, upload-time = "2025-02-11T15:09:25.759Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/76/37c0ccd5ab968a6a438f9c623aeecc84c202ab2fabc6a8fd927580c15b5a/QtPy-2.4.3-py3-none-any.whl", hash = "sha256:72095afe13673e017946cc258b8d5da43314197b741ed2890e563cf384b51aa1", size = 95045, upload-time = "2025-02-11T15:09:24.162Z" }, +] + +[[package]] +name = "qtpyinheritance" +version = "0.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "qtpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5d/5e/88386f48580e805a3bd115d926d14fa6321c381e41412e5648085378298c/qtpyinheritance-0.0.3.tar.gz", hash = "sha256:86d4e2f4908971a74a1502e5ebd5fb4b319d11c75d64e72aa05ad7248d93b1da", size = 16296, upload-time = "2024-05-10T20:03:47.489Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/d9/c4abea7473ef4946f20564b267b193df157b1a689877ce52ce6d76d51c81/qtpyinheritance-0.0.3-py3-none-any.whl", hash = "sha256:5acafe3a7337d08d7d4f38c0a74618e56f3ee71fd88955e6d4482b20cc765f36", size = 9641, upload-time = "2024-05-10T20:03:45.903Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[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 = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/8d/192f3d7103816158dfd5ea50d098ef2aec19194e6cbccd4b3485bdb2eb2d/ruff-0.15.11.tar.gz", hash = "sha256:f092b21708bf0e7437ce9ada249dfe688ff9a0954fc94abab05dcea7dcd29c33", size = 4637264, upload-time = "2026-04-16T18:46:26.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/1e/6aca3427f751295ab011828e15e9bf452200ac74484f1db4be0197b8170b/ruff-0.15.11-py3-none-linux_armv6l.whl", hash = "sha256:e927cfff503135c558eb581a0c9792264aae9507904eb27809cdcff2f2c847b7", size = 10607943, upload-time = "2026-04-16T18:46:05.967Z" }, + { url = "https://files.pythonhosted.org/packages/e7/26/1341c262e74f36d4e84f3d6f4df0ac68cd53331a66bfc5080daa17c84c0b/ruff-0.15.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7a1b5b2938d8f890b76084d4fa843604d787a912541eae85fd7e233398bbb73e", size = 10988592, upload-time = "2026-04-16T18:46:00.742Z" }, + { url = "https://files.pythonhosted.org/packages/03/71/850b1d6ffa9564fbb6740429bad53df1094082fe515c8c1e74b6d8d05f18/ruff-0.15.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d4176f3d194afbdaee6e41b9ccb1a2c287dba8700047df474abfbe773825d1cb", size = 10338501, upload-time = "2026-04-16T18:46:03.723Z" }, + { url = "https://files.pythonhosted.org/packages/f2/11/cc1284d3e298c45a817a6aadb6c3e1d70b45c9b36d8d9cce3387b495a03a/ruff-0.15.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b17c886fb88203ced3afe7f14e8d5ae96e9d2f4ccc0ee66aa19f2c2675a27e4", size = 10670693, upload-time = "2026-04-16T18:46:41.941Z" }, + { url = "https://files.pythonhosted.org/packages/ce/9e/f8288b034ab72b371513c13f9a41d9ba3effac54e24bfb467b007daee2ca/ruff-0.15.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:49fafa220220afe7758a487b048de4c8f9f767f37dfefad46b9dd06759d003eb", size = 10416177, upload-time = "2026-04-16T18:46:21.717Z" }, + { url = "https://files.pythonhosted.org/packages/85/71/504d79abfd3d92532ba6bbe3d1c19fada03e494332a59e37c7c2dabae427/ruff-0.15.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2ab8427e74a00d93b8bda1307b1e60970d40f304af38bccb218e056c220120d", size = 11221886, upload-time = "2026-04-16T18:46:15.086Z" }, + { url = "https://files.pythonhosted.org/packages/43/5a/947e6ab7a5ad603d65b474be15a4cbc6d29832db5d762cd142e4e3a74164/ruff-0.15.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:195072c0c8e1fc8f940652073df082e37a5d9cb43b4ab1e4d0566ab8977a13b7", size = 12075183, upload-time = "2026-04-16T18:46:07.944Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a1/0b7bb6268775fdd3a0818aee8efd8f5b4e231d24dd4d528ced2534023182/ruff-0.15.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a0996d486af3920dec930a2e7daed4847dfc12649b537a9335585ada163e9e", size = 11516575, upload-time = "2026-04-16T18:46:31.687Z" }, + { url = "https://files.pythonhosted.org/packages/30/c3/bb5168fc4d233cc06e95f482770d0f3c87945a0cd9f614b90ea8dc2f2833/ruff-0.15.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bef2cb556d509259f1fe440bb9cd33c756222cf0a7afe90d15edf0866702431", size = 11306537, upload-time = "2026-04-16T18:46:36.988Z" }, + { url = "https://files.pythonhosted.org/packages/e4/92/4cfae6441f3967317946f3b788136eecf093729b94d6561f963ed810c82e/ruff-0.15.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:030d921a836d7d4a12cf6e8d984a88b66094ccb0e0f17ddd55067c331191bf19", size = 11296813, upload-time = "2026-04-16T18:46:24.182Z" }, + { url = "https://files.pythonhosted.org/packages/43/26/972784c5dde8313acde8ac71ba8ac65475b85db4a2352a76c9934361f9bc/ruff-0.15.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0e783b599b4577788dbbb66b9addcef87e9a8832f4ce0c19e34bf55543a2f890", size = 10633136, upload-time = "2026-04-16T18:46:39.802Z" }, + { url = "https://files.pythonhosted.org/packages/5b/53/3985a4f185020c2f367f2e08a103032e12564829742a1b417980ce1514a0/ruff-0.15.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ae90592246625ba4a34349d68ec28d4400d75182b71baa196ddb9f82db025ef5", size = 10424701, upload-time = "2026-04-16T18:46:10.381Z" }, + { url = "https://files.pythonhosted.org/packages/d3/57/bf0dfb32241b56c83bb663a826133da4bf17f682ba8c096973065f6e6a68/ruff-0.15.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1f111d62e3c983ed20e0ca2e800f8d77433a5b1161947df99a5c2a3fb60514f0", size = 10873887, upload-time = "2026-04-16T18:46:29.157Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/e48076b2a57dc33ee8c7a957296f97c744ca891a8ffb4ffb1aaa3b3f517d/ruff-0.15.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:06f483d6646f59eaffba9ae30956370d3a886625f511a3108994000480621d1c", size = 11404316, upload-time = "2026-04-16T18:46:19.462Z" }, + { url = "https://files.pythonhosted.org/packages/88/27/0195d15fe7a897cbcba0904792c4b7c9fdd958456c3a17d2ea6093716a9a/ruff-0.15.11-py3-none-win32.whl", hash = "sha256:476a2aa56b7da0b73a3ee80b6b2f0e19cce544245479adde7baa65466664d5f3", size = 10655535, upload-time = "2026-04-16T18:46:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5e/c927b325bd4c1d3620211a4b96f47864633199feed60fa936025ab27e090/ruff-0.15.11-py3-none-win_amd64.whl", hash = "sha256:8b6756d88d7e234fb0c98c91511aae3cd519d5e3ed271cae31b20f39cb2a12a3", size = 11779692, upload-time = "2026-04-16T18:46:17.268Z" }, + { url = "https://files.pythonhosted.org/packages/63/b6/aeadee5443e49baa2facd51131159fd6301cc4ccfc1541e4df7b021c37dd/ruff-0.15.11-py3-none-win_arm64.whl", hash = "sha256:063fed18cc1bbe0ee7393957284a6fe8b588c6a406a285af3ee3f46da2391ee4", size = 11032614, upload-time = "2026-04-16T18:46:34.487Z" }, +] + +[[package]] +name = "schema" +version = "0.7.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/2e/8da627b65577a8f130fe9dfa88ce94fcb24b1f8b59e0fc763ee61abef8b8/schema-0.7.8.tar.gz", hash = "sha256:e86cc08edd6fe6e2522648f4e47e3a31920a76e82cce8937535422e310862ab5", size = 45540, upload-time = "2025-10-11T13:15:40.281Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/75/aad85817266ac5285c93391711d231ca63e9ae7d42cd3ca37549e24ebe52/schema-0.7.8-py2.py3-none-any.whl", hash = "sha256:00bd977fadc7d9521bf289850cd8a8aa5f4948f575476b8daaa5c1b57af2dce1", size = 19108, upload-time = "2025-10-11T17:13:07.323Z" }, +] + +[[package]] +name = "scipy" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954, upload-time = "2026-02-23T00:17:49.855Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662, upload-time = "2026-02-23T00:18:01.64Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366, upload-time = "2026-02-23T00:18:12.015Z" }, + { url = "https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017, upload-time = "2026-02-23T00:18:21.502Z" }, + { url = "https://files.pythonhosted.org/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842, upload-time = "2026-02-23T00:18:35.367Z" }, + { url = "https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890, upload-time = "2026-02-23T00:18:49.188Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557, upload-time = "2026-02-23T00:18:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856, upload-time = "2026-02-23T00:19:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682, upload-time = "2026-02-23T00:19:07.67Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340, upload-time = "2026-02-23T00:19:12.024Z" }, + { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" }, + { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" }, + { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" }, + { url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" }, + { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" }, + { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" }, + { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" }, + { url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" }, + { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" }, + { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" }, + { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" }, + { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" }, + { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" }, + { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" }, + { url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" }, + { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" }, + { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" }, + { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" }, + { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" }, + { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" }, + { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" }, + { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, +] + +[[package]] +name = "setuptools" +version = "82.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4f/db/cfac1baf10650ab4d1c111714410d2fbb77ac5a616db26775db562c8fab2/setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9", size = 1152316, upload-time = "2026-03-09T12:47:17.221Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", size = 1006223, upload-time = "2026-03-09T12:47:15.026Z" }, +] + +[[package]] +name = "simplejson" +version = "4.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/14/91/b0e7a38d63706dde006d1213f9c394ad7702df841c019fb4cf0e3295c58c/simplejson-4.0.1.tar.gz", hash = "sha256:bc13170567a5c856a0e6c16620c0b0388722f7d6382acd8007857624c3dedf3e", size = 115959, upload-time = "2026-04-18T22:46:17.544Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/21/03ec9b2cceff7966637bd7c96aa4f4df7d59edf4f481fcd76ada5d883f20/simplejson-4.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5be5c2b2ea9e81528b3622f8fd486da04e80e983829f2e69a7acb94302792ef2", size = 110113, upload-time = "2026-04-18T22:44:40.74Z" }, + { url = "https://files.pythonhosted.org/packages/a5/41/77b6f4301566206998ba1911ec334cbe3c574fa5095f846ea99e50e81242/simplejson-4.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aef5610bbbdffc24820daba4f4c7dd1e52bf0f4e016e2860484b562bb964dde5", size = 89804, upload-time = "2026-04-18T22:44:42.289Z" }, + { url = "https://files.pythonhosted.org/packages/8f/48/859d3d12a5f2d6acd1ef1ce75bdd8d25758d63d47223e66eccf3b02461e6/simplejson-4.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82b34e268f1c7acb9d1bed942b376cd01c418cee9bf05a207ac962c6ce28d29a", size = 90103, upload-time = "2026-04-18T22:44:43.446Z" }, + { url = "https://files.pythonhosted.org/packages/12/17/5135ac3005d2db660182117053d63b97d4ae841efbf77c02ad3aa8b36e3d/simplejson-4.0.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:905d03e5336b20c6d8ac2a648cb178259807836fb5c8a1ee091ca4601962ec47", size = 183967, upload-time = "2026-04-18T22:44:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/e7/e4/3f61fc83ca56256b581c2c55331ba4a51fc50e28d0153f40baebfeb9b90c/simplejson-4.0.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:95c6a6e1f66b6fda46fb9f44809911a90715f05f6df1c6d6db941a8ad2872c35", size = 181209, upload-time = "2026-04-18T22:44:45.899Z" }, + { url = "https://files.pythonhosted.org/packages/3d/26/809a1ed84241a96868fd3915aad173c7e983acb0f7896b7a895635ab1577/simplejson-4.0.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:99f12a0d959d7103403e4774189b562f8b1e0529a41561248e2a3c25b6c80797", size = 189815, upload-time = "2026-04-18T22:44:47.175Z" }, + { url = "https://files.pythonhosted.org/packages/08/29/c157b85210df6e286f0f52f8683040d49e654ed51d30126f9eea0f669725/simplejson-4.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5deb4b6c77a02c2e2267135c5afd0d4a9acd3c83c3f73674816d2377b475630", size = 177993, upload-time = "2026-04-18T22:44:48.771Z" }, + { url = "https://files.pythonhosted.org/packages/7e/f5/b8259ecfad5b717199a9ed1c01c54dff0d858934e226dbb6013bfe4a4995/simplejson-4.0.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c95f7c5c53c3e62bde8c452f41d6795f541e5dc951926275771ff7f2bbae0511", size = 186796, upload-time = "2026-04-18T22:44:50.181Z" }, + { url = "https://files.pythonhosted.org/packages/78/ee/d3dcfaf22d9eb620f8840390a655c22c858e9c2c75073883d7090e9ccf78/simplejson-4.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0f057dd469501aff95bb438fc9593d0c2f353dba83f00c0a3db6447ad79f5728", size = 181521, upload-time = "2026-04-18T22:44:51.413Z" }, + { url = "https://files.pythonhosted.org/packages/24/ed/6f13edaff9106e657aab61320e1adceccbd30f50afd877e99d0de292bc48/simplejson-4.0.1-cp312-cp312-win32.whl", hash = "sha256:cc962d5029509f2170ef40e5e7db4df87f71208a931aca6e8bdc0c76e1ff2b3a", size = 88003, upload-time = "2026-04-18T22:44:53.045Z" }, + { url = "https://files.pythonhosted.org/packages/d1/7d/1eebe8ef0682da987d637606aa2eaae0e211010a6fce21abd2c680cf65a6/simplejson-4.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:d2031d87911379c7a778e5f902eeffc161355c9137c695dd12dc219456e976e9", size = 90055, upload-time = "2026-04-18T22:44:54.491Z" }, + { url = "https://files.pythonhosted.org/packages/ed/94/afe6285b3b39208473ab9056039cf20cac393d1a7942f644ecb7d424463b/simplejson-4.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f934a918ef7a50698a481e18aa713d3075f94d1402a6d293f755d00118f8a975", size = 110975, upload-time = "2026-04-18T22:44:55.995Z" }, + { url = "https://files.pythonhosted.org/packages/9a/45/2c74fdb851af00c18ae11af978e7191972071e26ffe2f5aaccbd0a96e961/simplejson-4.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a2b0211da4be9fcbabe11ff9b65f7e06dd8205197b078fe7f1e9cc268cd0e369", size = 90384, upload-time = "2026-04-18T22:44:57.194Z" }, + { url = "https://files.pythonhosted.org/packages/92/af/f457958ef90935e99e1bdf51c16b2a0448726a4db2881d472d22f0259290/simplejson-4.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:58c125ee57f2a35081c46ca66d76ee3c109394f4aee0d981cb049c5ee5688e62", size = 90387, upload-time = "2026-04-18T22:44:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/43/58/da3da211a3b91ff0f8b9a3926e3b97b9224cbb94e4a67b998394c0e984a3/simplejson-4.0.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:24ca278f0ba7b3aff90ed1f873f5f4192ae1da97de72196b4be021f3b1d407a0", size = 186193, upload-time = "2026-04-18T22:45:00.108Z" }, + { url = "https://files.pythonhosted.org/packages/69/ff/5757f7eddea36d55593423c5da72cdb1ffd2ae7755d804da08f598cf277a/simplejson-4.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a6b3d15ab9c8247f7e0a280b2823eff08bd74e7605701c01d7cd54e80a86ed", size = 183647, upload-time = "2026-04-18T22:45:01.484Z" }, + { url = "https://files.pythonhosted.org/packages/1e/27/eb59ff58a78e57db4120e10bc6b7d6409f9c25180b912593c2cfe5637b5c/simplejson-4.0.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:534a7d90b86a97cf15ebe42d732c41223612d981d08f226025499177692a977d", size = 191880, upload-time = "2026-04-18T22:45:03.067Z" }, + { url = "https://files.pythonhosted.org/packages/1b/e6/0650672c0b43b9add2c2e5f13f5b2d3770b23bc10688dfe4ea637ef1a4b4/simplejson-4.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e2b10899f200b675a8ab0e2abd4c4846d2631d90d226fc3079e8b16073aba31d", size = 180312, upload-time = "2026-04-18T22:45:04.51Z" }, + { url = "https://files.pythonhosted.org/packages/00/b9/4a4042c8f24bb53ca03a2d5d794aeefa78965df1213239cf9fe895b40342/simplejson-4.0.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dd525b897132493aa65eecd3c5ddc3602e25c625caafbcc958fc87a74a24a1c1", size = 188278, upload-time = "2026-04-18T22:45:06.019Z" }, + { url = "https://files.pythonhosted.org/packages/17/b4/594be43d55ea5100334bc60013c3b310ecde4135b8288dd8183d21881966/simplejson-4.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:072a481be83b8c3f9167d0463105e0ddfcf2e00a6289c2ad650c3b35920ce0c8", size = 183928, upload-time = "2026-04-18T22:45:07.298Z" }, + { url = "https://files.pythonhosted.org/packages/0d/26/8c09ba0fcf0b8c284a4f0f57605890c443b59cec070c6918baa7600976d9/simplejson-4.0.1-cp313-cp313-win32.whl", hash = "sha256:91eb4b42ab0a2de89919ed3bfa960fca0b13af0816447c89123c6658c36fb0a1", size = 88305, upload-time = "2026-04-18T22:45:08.603Z" }, + { url = "https://files.pythonhosted.org/packages/4a/18/996ef6a0bd5b02bb9880901df01621b6c6de599360454a2ee2d06985076f/simplejson-4.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d2dc202a5e9d07e893c0bc1aa12b88bff98332c52b3ae184defe054d0e2ff35", size = 90273, upload-time = "2026-04-18T22:45:09.774Z" }, + { url = "https://files.pythonhosted.org/packages/c8/84/103fbe8f7d1221737fc26b8932804a4f9393ab8e83633ed244a3b75c2617/simplejson-4.0.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:77af5d5f5fecd749786a38e6bd0e4bdd97f37e321ee6008a130dfe0aef7ec4e4", size = 108191, upload-time = "2026-04-18T22:45:11.007Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ca/123e47cabb2307ad8959f3b3b6b181e96e83e261dc6033fb5b068cfea78f/simplejson-4.0.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:bb48e3e3f7a97780971d1649989b63c576f86ce91a59cc3400d80cd7964f1044", size = 88863, upload-time = "2026-04-18T22:45:12.407Z" }, + { url = "https://files.pythonhosted.org/packages/42/a2/9d56e5a028ff4d666f25609f42f2c88cbd155d5fb30b124fcb7cf6081b88/simplejson-4.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5738da4af10c92631e89421d4665f0e92119b63b62085d8eb2d51304bc4273ee", size = 89106, upload-time = "2026-04-18T22:45:13.933Z" }, + { url = "https://files.pythonhosted.org/packages/56/1b/9eb1901514f48cf8944fe7af8432f522aa1c80dd3a00eddfedefa62066bc/simplejson-4.0.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4d02c0fb43f2f0f20de94043235fa433e1cae63c39e2f4af0f9d21882a8479be", size = 178099, upload-time = "2026-04-18T22:45:15.156Z" }, + { url = "https://files.pythonhosted.org/packages/25/19/5437e74f888f76a0c765e61057396a0932bfab104974d81752e63a5c6dd2/simplejson-4.0.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:851cfde1b505289d8c60cc6057770fe0f0dd675065fdc0b8e916463dca3a9d63", size = 175091, upload-time = "2026-04-18T22:45:16.43Z" }, + { url = "https://files.pythonhosted.org/packages/02/ab/451bae4c4baf663349437ba5790f489a3956dd0b3d8ddcf25b087ea743f1/simplejson-4.0.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8953fee0520010baae040cf8a5316f5b75794d6bd79364be3deb365821ff2cb0", size = 183411, upload-time = "2026-04-18T22:45:17.746Z" }, + { url = "https://files.pythonhosted.org/packages/c4/38/90c48b57694bc14dab99a77508b037597c44929323d7dc4764b2889b41a6/simplejson-4.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aa3595f335868a3a421e089146fe53b0df19d9f7198748ce698d8d409c3c0786", size = 172859, upload-time = "2026-04-18T22:45:19.064Z" }, + { url = "https://files.pythonhosted.org/packages/98/ff/a7ba77ba7375aef15617c41383e6a301c3c8e3f789f0356037c149ffd9e7/simplejson-4.0.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c7962187dcb27c9abe3a5e04969b005f963d6f9bc05f77641126e4ee99aa6a5f", size = 180548, upload-time = "2026-04-18T22:45:20.75Z" }, + { url = "https://files.pythonhosted.org/packages/36/5d/748bed0e129846f727d37ac5739c562e1aa8aef4ba864e3bdf59eba530d0/simplejson-4.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f64c2bffa2a4438c9b45aef53743271fcac4836dd6d15646b6865fd3c49dd2b0", size = 176480, upload-time = "2026-04-18T22:45:22.4Z" }, + { url = "https://files.pythonhosted.org/packages/63/98/783ed8ddd0962e6e36c45dbc331fd35f42aa904829ba3757c1a3eee15b78/simplejson-4.0.1-cp314-cp314-win32.whl", hash = "sha256:cad8fe5e92def6080a31315b74947d956220cc6e7726878de797d233e1884bf6", size = 88023, upload-time = "2026-04-18T22:45:24.025Z" }, + { url = "https://files.pythonhosted.org/packages/9b/90/678107cf70a5709066f0a3859e2d56018da9eef1cf97ae478c9a7435294b/simplejson-4.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:9ae4bc39e64d24642c6e2f97a0d7f3c3f2262876e4d45fef439e0910b0b7433a", size = 90156, upload-time = "2026-04-18T22:45:25.239Z" }, + { url = "https://files.pythonhosted.org/packages/cb/76/709e8f60385b2c4cb26ad291302594d4711562c8005eb8e36e74080e47be/simplejson-4.0.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4dab04afe06dc2e83dce044cf53374d37019a5be14f815cb583456b02bcba46b", size = 111994, upload-time = "2026-04-18T22:45:26.479Z" }, + { url = "https://files.pythonhosted.org/packages/3c/18/87698fd8c4e286b1f7154d7a5076f5e97fdc7582484ffcc199afb5ea62b1/simplejson-4.0.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b6e744eacf669056ab19992f694e48149cccae9538e9014a4778749feae9978b", size = 90761, upload-time = "2026-04-18T22:45:28.099Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a4/e14d784ddddefc039d651f932a1a43efe4814726de652a3f6cd569fbf0e2/simplejson-4.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4166f23ace9b0cf28be7ed0f147c900baf4e579a57fe926ba54078b244eb2e7f", size = 91070, upload-time = "2026-04-18T22:45:29.953Z" }, + { url = "https://files.pythonhosted.org/packages/9b/93/ba9ab9bd5aad9df588b39163e6e381d6a5e4d51b6f70320d81a84316690a/simplejson-4.0.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5425cdfc980972688f08f332ed1d480874b5114855e49a9a6607e220c494c1e6", size = 208225, upload-time = "2026-04-18T22:45:31.183Z" }, + { url = "https://files.pythonhosted.org/packages/72/91/abb6e9caf7f08d392f5c50e14f3a411b3e85597b59851bb8ca09829a7466/simplejson-4.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7584d53ed708ad17984a591a542861e0712da285577cfbbcd82c956eb6b12233", size = 207827, upload-time = "2026-04-18T22:45:32.828Z" }, + { url = "https://files.pythonhosted.org/packages/a6/c6/bfdb1be8ef2d7db768237f97a7e420fe34146692ebfbc2ffd719c528ffb8/simplejson-4.0.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0d98b8fdd7df1cec61de1f63ae7f111d761e2a3208049b5d24358aa29dc3044f", size = 213386, upload-time = "2026-04-18T22:45:34.178Z" }, + { url = "https://files.pythonhosted.org/packages/92/87/0b81b072d1fe185c4723ff1c76096e73b6e0019791804aad5f4946724620/simplejson-4.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9f8d949c5d34afb3bf81d71879a288526f85cd31277f4a321e4b643f0c4f367d", size = 204013, upload-time = "2026-04-18T22:45:35.674Z" }, + { url = "https://files.pythonhosted.org/packages/72/b2/b396ee3ded3fe2a0609b89b922b09ba9134c8da539c267bb2b356f3471a8/simplejson-4.0.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5ecc0ad5fdd14109f563510e448a2f937c1d8816e91b33ab5549408e72dda884", size = 209274, upload-time = "2026-04-18T22:45:37.301Z" }, + { url = "https://files.pythonhosted.org/packages/a5/27/22ef6035399b7c09e07cc7373d3c54a5261fb55554b7efbd0bc9aa360f29/simplejson-4.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:54342df0f6a273436e6869ac372a077161877c826ddc66b861f31a59fabe2b98", size = 204866, upload-time = "2026-04-18T22:45:38.671Z" }, + { url = "https://files.pythonhosted.org/packages/0b/a0/fc26e8d67c2cf9bf4742f1d81c2d15067731be2f60972ad0b780d70e7460/simplejson-4.0.1-cp314-cp314t-win32.whl", hash = "sha256:3e49d709a121d47acaf6932803407b6feee5d5fac5c7f2a370d7ba5b6fedaea3", size = 90571, upload-time = "2026-04-18T22:45:40.401Z" }, + { url = "https://files.pythonhosted.org/packages/3d/6b/1858670f2b22fcdb26efe9255d9354377d3259e08ed26d0652c7a4bd5aa3/simplejson-4.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:d8c702e19fd99fd75f3371de70b5489dc9f659cd69ada0c72a2f6dce76bd624c", size = 93474, upload-time = "2026-04-18T22:45:41.552Z" }, + { url = "https://files.pythonhosted.org/packages/a9/2d/93a5b862ac29f182a658eb3fc2c98fe28acbb5c05a9076402572c7eb6966/simplejson-4.0.1-py3-none-any.whl", hash = "sha256:dfa6e9923c0ec2880738d09e5ce045741eb6cd4551e261dcd6c3625d26666075", size = 69242, upload-time = "2026-04-18T22:46:16.183Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[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 = "9.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils" }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "roman-numerals" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, +] +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" }, + { name = "sphinx" }, + { 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" }, +] +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 = "sqlalchemy" +version = "2.0.49" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/45/461788f35e0364a8da7bda51a1fe1b09762d0c32f12f63727998d85a873b/sqlalchemy-2.0.49.tar.gz", hash = "sha256:d15950a57a210e36dd4cec1aac22787e2a4d57ba9318233e2ef8b2daf9ff2d5f", size = 9898221, upload-time = "2026-04-03T16:38:11.704Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/b3/2de412451330756aaaa72d27131db6dde23995efe62c941184e15242a5fa/sqlalchemy-2.0.49-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4bbccb45260e4ff1b7db0be80a9025bb1e6698bdb808b83fff0000f7a90b2c0b", size = 2157681, upload-time = "2026-04-03T16:53:07.132Z" }, + { url = "https://files.pythonhosted.org/packages/50/84/b2a56e2105bd11ebf9f0b93abddd748e1a78d592819099359aa98134a8bf/sqlalchemy-2.0.49-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb37f15714ec2652d574f021d479e78cd4eb9d04396dca36568fdfffb3487982", size = 3338976, upload-time = "2026-04-03T17:07:40Z" }, + { url = "https://files.pythonhosted.org/packages/2c/fa/65fcae2ed62f84ab72cf89536c7c3217a156e71a2c111b1305ab6f0690e2/sqlalchemy-2.0.49-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bb9ec6436a820a4c006aad1ac351f12de2f2dbdaad171692ee457a02429b672", size = 3351937, upload-time = "2026-04-03T17:12:23.374Z" }, + { url = "https://files.pythonhosted.org/packages/f8/2f/6fd118563572a7fe475925742eb6b3443b2250e346a0cc27d8d408e73773/sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8d6efc136f44a7e8bc8088507eaabbb8c2b55b3dbb63fe102c690da0ddebe55e", size = 3281646, upload-time = "2026-04-03T17:07:41.949Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d7/410f4a007c65275b9cf82354adb4bb8ba587b176d0a6ee99caa16fe638f8/sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e06e617e3d4fd9e51d385dfe45b077a41e9d1b033a7702551e3278ac597dc750", size = 3316695, upload-time = "2026-04-03T17:12:25.642Z" }, + { url = "https://files.pythonhosted.org/packages/d9/95/81f594aa60ded13273a844539041ccf1e66c5a7bed0a8e27810a3b52d522/sqlalchemy-2.0.49-cp312-cp312-win32.whl", hash = "sha256:83101a6930332b87653886c01d1ee7e294b1fe46a07dd9a2d2b4f91bcc88eec0", size = 2117483, upload-time = "2026-04-03T17:05:40.896Z" }, + { url = "https://files.pythonhosted.org/packages/47/9e/fd90114059175cac64e4fafa9bf3ac20584384d66de40793ae2e2f26f3bb/sqlalchemy-2.0.49-cp312-cp312-win_amd64.whl", hash = "sha256:618a308215b6cececb6240b9abde545e3acdabac7ae3e1d4e666896bf5ba44b4", size = 2144494, upload-time = "2026-04-03T17:05:42.282Z" }, + { url = "https://files.pythonhosted.org/packages/ae/81/81755f50eb2478eaf2049728491d4ea4f416c1eb013338682173259efa09/sqlalchemy-2.0.49-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df2d441bacf97022e81ad047e1597552eb3f83ca8a8f1a1fdd43cd7fe3898120", size = 2154547, upload-time = "2026-04-03T16:53:08.64Z" }, + { url = "https://files.pythonhosted.org/packages/a2/bc/3494270da80811d08bcfa247404292428c4fe16294932bce5593f215cad9/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8e20e511dc15265fb433571391ba313e10dd8ea7e509d51686a51313b4ac01a2", size = 3280782, upload-time = "2026-04-03T17:07:43.508Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f5/038741f5e747a5f6ea3e72487211579d8cbea5eb9827a9cbd61d0108c4bd/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47604cb2159f8bbd5a1ab48a714557156320f20871ee64d550d8bf2683d980d3", size = 3297156, upload-time = "2026-04-03T17:12:27.697Z" }, + { url = "https://files.pythonhosted.org/packages/88/50/a6af0ff9dc954b43a65ca9b5367334e45d99684c90a3d3413fc19a02d43c/sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:22d8798819f86720bc646ab015baff5ea4c971d68121cb36e2ebc2ee43ead2b7", size = 3228832, upload-time = "2026-04-03T17:07:45.38Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d1/5f6bdad8de0bf546fc74370939621396515e0cdb9067402d6ba1b8afbe9a/sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9b1c058c171b739e7c330760044803099c7fff11511e3ab3573e5327116a9c33", size = 3267000, upload-time = "2026-04-03T17:12:29.657Z" }, + { url = "https://files.pythonhosted.org/packages/f7/30/ad62227b4a9819a5e1c6abff77c0f614fa7c9326e5a3bdbee90f7139382b/sqlalchemy-2.0.49-cp313-cp313-win32.whl", hash = "sha256:a143af2ea6672f2af3f44ed8f9cd020e9cc34c56f0e8db12019d5d9ecf41cb3b", size = 2115641, upload-time = "2026-04-03T17:05:43.989Z" }, + { url = "https://files.pythonhosted.org/packages/17/3a/7215b1b7d6d49dc9a87211be44562077f5f04f9bb5a59552c1c8e2d98173/sqlalchemy-2.0.49-cp313-cp313-win_amd64.whl", hash = "sha256:12b04d1db2663b421fe072d638a138460a51d5a862403295671c4f3987fb9148", size = 2141498, upload-time = "2026-04-03T17:05:45.7Z" }, + { url = "https://files.pythonhosted.org/packages/28/4b/52a0cb2687a9cd1648252bb257be5a1ba2c2ded20ba695c65756a55a15a4/sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24bd94bb301ec672d8f0623eba9226cc90d775d25a0c92b5f8e4965d7f3a1518", size = 3560807, upload-time = "2026-04-03T16:58:31.666Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d8/fda95459204877eed0458550d6c7c64c98cc50c2d8d618026737de9ed41a/sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a51d3db74ba489266ef55c7a4534eb0b8db9a326553df481c11e5d7660c8364d", size = 3527481, upload-time = "2026-04-03T17:06:00.155Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0a/2aac8b78ac6487240cf7afef8f203ca783e8796002dc0cf65c4ee99ff8bb/sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:55250fe61d6ebfd6934a272ee16ef1244e0f16b7af6cd18ab5b1fc9f08631db0", size = 3468565, upload-time = "2026-04-03T16:58:33.414Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/ce71cfa82c50a373fd2148b3c870be05027155ce791dc9a5dcf439790b8b/sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:46796877b47034b559a593d7e4b549aba151dae73f9e78212a3478161c12ab08", size = 3477769, upload-time = "2026-04-03T17:06:02.787Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e8/0a9f5c1f7c6f9ca480319bf57c2d7423f08d31445974167a27d14483c948/sqlalchemy-2.0.49-cp313-cp313t-win32.whl", hash = "sha256:9c4969a86e41454f2858256c39bdfb966a20961e9b58bf8749b65abf447e9a8d", size = 2143319, upload-time = "2026-04-03T17:02:04.328Z" }, + { url = "https://files.pythonhosted.org/packages/0e/51/fb5240729fbec73006e137c4f7a7918ffd583ab08921e6ff81a999d6517a/sqlalchemy-2.0.49-cp313-cp313t-win_amd64.whl", hash = "sha256:b9870d15ef00e4d0559ae10ee5bc71b654d1f20076dbe8bc7ed19b4c0625ceba", size = 2175104, upload-time = "2026-04-03T17:02:05.989Z" }, + { url = "https://files.pythonhosted.org/packages/55/33/bf28f618c0a9597d14e0b9ee7d1e0622faff738d44fe986ee287cdf1b8d0/sqlalchemy-2.0.49-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:233088b4b99ebcbc5258c755a097aa52fbf90727a03a5a80781c4b9c54347a2e", size = 2156356, upload-time = "2026-04-03T16:53:09.914Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a7/5f476227576cb8644650eff68cc35fa837d3802b997465c96b8340ced1e2/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57ca426a48eb2c682dae8204cd89ea8ab7031e2675120a47924fabc7caacbc2a", size = 3276486, upload-time = "2026-04-03T17:07:46.9Z" }, + { url = "https://files.pythonhosted.org/packages/2e/84/efc7c0bf3a1c5eef81d397f6fddac855becdbb11cb38ff957888603014a7/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:685e93e9c8f399b0c96a624799820176312f5ceef958c0f88215af4013d29066", size = 3281479, upload-time = "2026-04-03T17:12:32.226Z" }, + { url = "https://files.pythonhosted.org/packages/91/68/bb406fa4257099c67bd75f3f2261b129c63204b9155de0d450b37f004698/sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e0400fa22f79acc334d9a6b185dc00a44a8e6578aa7e12d0ddcd8434152b187", size = 3226269, upload-time = "2026-04-03T17:07:48.678Z" }, + { url = "https://files.pythonhosted.org/packages/67/84/acb56c00cca9f251f437cb49e718e14f7687505749ea9255d7bd8158a6df/sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a05977bffe9bffd2229f477fa75eabe3192b1b05f408961d1bebff8d1cd4d401", size = 3248260, upload-time = "2026-04-03T17:12:34.381Z" }, + { url = "https://files.pythonhosted.org/packages/56/19/6a20ea25606d1efd7bd1862149bb2a22d1451c3f851d23d887969201633f/sqlalchemy-2.0.49-cp314-cp314-win32.whl", hash = "sha256:0f2fa354ba106eafff2c14b0cc51f22801d1e8b2e4149342023bd6f0955de5f5", size = 2118463, upload-time = "2026-04-03T17:05:47.093Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4f/8297e4ed88e80baa1f5aa3c484a0ee29ef3c69c7582f206c916973b75057/sqlalchemy-2.0.49-cp314-cp314-win_amd64.whl", hash = "sha256:77641d299179c37b89cf2343ca9972c88bb6eef0d5fc504a2f86afd15cd5adf5", size = 2144204, upload-time = "2026-04-03T17:05:48.694Z" }, + { url = "https://files.pythonhosted.org/packages/1f/33/95e7216df810c706e0cd3655a778604bbd319ed4f43333127d465a46862d/sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c1dc3368794d522f43914e03312202523cc89692f5389c32bea0233924f8d977", size = 3565474, upload-time = "2026-04-03T16:58:35.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/a4/ed7b18d8ccf7f954a83af6bb73866f5bc6f5636f44c7731fbb741f72cc4f/sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c821c47ecfe05cc32140dcf8dc6fd5d21971c86dbd56eabfe5ba07a64910c01", size = 3530567, upload-time = "2026-04-03T17:06:04.587Z" }, + { url = "https://files.pythonhosted.org/packages/73/a3/20faa869c7e21a827c4a2a42b41353a54b0f9f5e96df5087629c306df71e/sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9c04bff9a5335eb95c6ecf1c117576a0aa560def274876fd156cfe5510fccc61", size = 3474282, upload-time = "2026-04-03T16:58:37.131Z" }, + { url = "https://files.pythonhosted.org/packages/b7/50/276b9a007aa0764304ad467eceb70b04822dc32092492ee5f322d559a4dc/sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7f605a456948c35260e7b2a39f8952a26f077fd25653c37740ed186b90aaa68a", size = 3480406, upload-time = "2026-04-03T17:06:07.176Z" }, + { url = "https://files.pythonhosted.org/packages/e5/c3/c80fcdb41905a2df650c2a3e0337198b6848876e63d66fe9188ef9003d24/sqlalchemy-2.0.49-cp314-cp314t-win32.whl", hash = "sha256:6270d717b11c5476b0cbb21eedc8d4dbb7d1a956fd6c15a23e96f197a6193158", size = 2149151, upload-time = "2026-04-03T17:02:07.281Z" }, + { url = "https://files.pythonhosted.org/packages/05/52/9f1a62feab6ed368aff068524ff414f26a6daebc7361861035ae00b05530/sqlalchemy-2.0.49-cp314-cp314t-win_amd64.whl", hash = "sha256:275424295f4256fd301744b8f335cff367825d270f155d522b30c7bf49903ee7", size = 2184178, upload-time = "2026-04-03T17:02:08.623Z" }, + { url = "https://files.pythonhosted.org/packages/e5/30/8519fdde58a7bdf155b714359791ad1dc018b47d60269d5d160d311fdc36/sqlalchemy-2.0.49-py3-none-any.whl", hash = "sha256:ec44cfa7ef1a728e88ad41674de50f6db8cfdb3e2af84af86e0041aaf02d43d0", size = 1942158, upload-time = "2026-04-03T16:53:44.135Z" }, +] + +[[package]] +name = "stack-data" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asttokens" }, + { name = "executing" }, + { name = "pure-eval" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, +] + +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + +[[package]] +name = "timechart" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pydm" }, + { name = "pyqtgraph" }, + { name = "qtpy" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9c/b5/9c71f5404c1e762f99e53663cfe41d720afdc05bd03d8c0b3b0379eeef90/timechart-1.5.3.tar.gz", hash = "sha256:95a2b0614577ccb0af92c0dc33a9466864509bcc31a1f95b52e4befc52004a48", size = 1508904, upload-time = "2024-05-21T22:18:01.395Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/42/5c5ce8f56daa635497a064db498e7c73b007761544269915e86b63bbe790/timechart-1.5.3-py3-none-any.whl", hash = "sha256:a2f6aa1fd4d9f3ea6dcb7b05f1a691bacc2647b2c80f1250e061893f542f0c18", size = 61751, upload-time = "2024-05-21T22:17:59.053Z" }, +] + +[[package]] +name = "toolz" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/d6/114b492226588d6ff54579d95847662fc69196bdeec318eb45393b24c192/toolz-1.1.0.tar.gz", hash = "sha256:27a5c770d068c110d9ed9323f24f1543e83b2f300a687b7891c1a6d56b697b5b", size = 52613, upload-time = "2025-10-17T04:03:21.661Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl", hash = "sha256:15ccc861ac51c53696de0a5d6d4607f99c210739caf987b5d2054f3efed429d8", size = 58093, upload-time = "2025-10-17T04:03:20.435Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + +[[package]] +name = "traitlets" +version = "5.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, +] + +[[package]] +name = "typhos" +source = { editable = "." } +dependencies = [ + { name = "coloredlogs" }, + { name = "entrypoints" }, + { name = "lxml" }, + { name = "numpy" }, + { name = "numpydoc" }, + { name = "ophyd" }, + { name = "pcdsutils" }, + { name = "platformdirs" }, + { name = "pydm" }, + { name = "pyqt5" }, + { name = "pyqtgraph" }, + { name = "qdarkstyle" }, + { name = "qtawesome" }, + { name = "qtpy" }, + { name = "timechart" }, +] + +[package.optional-dependencies] +dev = [ + { name = "caproto" }, + { name = "happi" }, + { name = "line-profiler" }, + { name = "lxml-stubs" }, + { name = "pcdsdevices" }, + { name = "ruff" }, +] +doc = [ + { name = "docs-versions-menu" }, + { name = "happi" }, + { name = "ipython" }, + { name = "sphinx" }, + { name = "sphinx-rtd-theme" }, + { name = "sphinxcontrib-jquery" }, +] +test = [ + { name = "caproto" }, + { name = "happi" }, + { name = "line-profiler" }, + { name = "pcdsdevices" }, + { name = "pytest" }, + { name = "pytest-benchmark" }, + { name = "pytest-cov" }, + { name = "pytest-qt" }, + { name = "pytest-timeout" }, +] + +[package.metadata] +requires-dist = [ + { name = "caproto", marker = "extra == 'dev'" }, + { name = "caproto", marker = "extra == 'test'" }, + { name = "coloredlogs" }, + { name = "docs-versions-menu", marker = "extra == 'doc'" }, + { name = "entrypoints" }, + { name = "happi", marker = "extra == 'dev'" }, + { name = "happi", marker = "extra == 'doc'" }, + { name = "happi", marker = "extra == 'test'" }, + { name = "ipython", marker = "extra == 'doc'", specifier = ">=7.16" }, + { name = "line-profiler", marker = "extra == 'dev'" }, + { name = "line-profiler", marker = "extra == 'test'" }, + { name = "lxml" }, + { name = "lxml-stubs", marker = "extra == 'dev'" }, + { name = "numpy" }, + { name = "numpydoc" }, + { name = "ophyd" }, + { name = "pcdsdevices", marker = "extra == 'dev'", specifier = ">=8.4.0" }, + { name = "pcdsdevices", marker = "extra == 'test'", specifier = ">=8.4.0" }, + { name = "pcdsutils" }, + { name = "platformdirs" }, + { name = "pydm", specifier = ">=1.19.1" }, + { name = "pyqt5" }, + { name = "pyqtgraph" }, + { name = "pytest", marker = "extra == 'test'" }, + { name = "pytest-benchmark", marker = "extra == 'test'" }, + { name = "pytest-cov", marker = "extra == 'test'" }, + { name = "pytest-qt", marker = "extra == 'test'" }, + { name = "pytest-timeout", marker = "extra == 'test'" }, + { name = "qdarkstyle" }, + { name = "qtawesome" }, + { name = "qtpy" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.15.11" }, + { name = "sphinx", marker = "extra == 'doc'" }, + { name = "sphinx-rtd-theme", marker = "extra == 'doc'" }, + { name = "sphinxcontrib-jquery", marker = "extra == 'doc'" }, + { name = "timechart" }, +] +provides-extras = ["dev", "doc", "test"] + +[[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" }, +] + +[[package]] +name = "wcwidth" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, +] + +[[package]] +name = "xraydb" +version = "4.5.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "platformdirs" }, + { name = "scipy" }, + { name = "sqlalchemy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/01/267954e2e0342418edadf3dea7f85f0edd92a3b065e6ef5daeaae36d4b79/xraydb-4.5.8.tar.gz", hash = "sha256:4414474c10be6dd8f273062fe57c1342e7a9f4c8f8c2c86881acd2ce013182ff", size = 3851174, upload-time = "2025-07-18T19:58:33.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/8b/7ec325b4e9e78beefc2d025b01ee8a2fde771ef7c957c3bff99b9e1fbffa/xraydb-4.5.8-py3-none-any.whl", hash = "sha256:2215baafa6a03d00d0254a94525aafc6493c8c285e4ac4477fbd6271b25e6a51", size = 3858878, upload-time = "2025-07-18T19:58:31.528Z" }, +] + +[[package]] +name = "xraylib" +version = "4.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/24/fd/444dc2034012ea33427c5c69d841ec7e8228ddcc7749bd4ed208ef65c638/xraylib-4.2.1.tar.gz", hash = "sha256:6ec503b1a9d2d0dc56df98c7d5581a76ab487fef9abf3116d18b64752fd4e4ab", size = 13195127, upload-time = "2025-12-21T11:32:44.699Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/6d/0364d04d6a862b254050c7000f17c741f43e6ded442006e9f5089985e4e4/xraylib-4.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9015014bd0002afd0d3d724dd494b66ad85ae85b09cd83dc7b57e51a54f450c5", size = 15950973, upload-time = "2025-12-21T11:31:50.657Z" }, + { url = "https://files.pythonhosted.org/packages/25/76/9e858b1c601b142cda6dfe10c66d47a424593323f7e12c16b058c3a541d9/xraylib-4.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adca5ab9de73b76a0eb90827426e0f3ebe7f5fcd76c472529a559b5315aac33f", size = 16170814, upload-time = "2025-12-21T11:31:53.321Z" }, + { url = "https://files.pythonhosted.org/packages/84/11/484366cd26be0c2ae83b8ff4ae9e2a2889a1d2d7b2f6e3fdd3924999a6d0/xraylib-4.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e79c1ef06473d38beb6f7aca2332f32c286f7cc27c2b6135cbcfbbf1989e7f4a", size = 16284834, upload-time = "2025-12-21T11:31:56.177Z" }, + { url = "https://files.pythonhosted.org/packages/21/01/4921565db7d876d8410b2eeaeaa2644c6edb54bd519d7baba7d850123521/xraylib-4.2.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84000896ed3625538622df4ae5cf44c92ca631e5db1fa173ca2641e3ca77236d", size = 16330428, upload-time = "2025-12-21T11:31:58.9Z" }, + { url = "https://files.pythonhosted.org/packages/3f/67/e385be93b247b01d8a30df2edc29216263e2cde72e3210075b5520750c2e/xraylib-4.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:3abe9a3570a0c7037e17fd09b5922f78fcd0325d28676fe6a5e523e3934be758", size = 14898368, upload-time = "2025-12-21T11:32:01.539Z" }, + { url = "https://files.pythonhosted.org/packages/db/e5/acedf8658e6e091a690200b64eb66c6bca23b0b813d3fc69596c39724ee0/xraylib-4.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f8c16486445d44e4450fc5bf5a944ebfbe59da2d2c2494385e2de6515fa805a", size = 15950299, upload-time = "2025-12-21T11:32:04.64Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/ee8949baf36174f894effaa6f632d1fce7614f31335a893edbeaad8735f5/xraylib-4.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc0a8985acc807bf98734239badd80967449f9322f3f5efdf3a88405497efcba", size = 16169950, upload-time = "2025-12-21T11:32:07.562Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b2/508c94044fa3b8bfb96eecf0bff77b7fbdcd8fe85ba8b21e970c4ae03ad3/xraylib-4.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:226f5922afffba0d10552fc6977d5d3aecb4266cbe7e148be08f944b082c35e5", size = 16284241, upload-time = "2025-12-21T11:32:09.991Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f1/c693abf4d3434654020e8555f8561ab34030946a639ebd0c0871f790c3af/xraylib-4.2.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:350c73976ca216097ad7f2a06dd0137f492b33d31e944cab8147720a007405f3", size = 16329187, upload-time = "2025-12-21T11:32:12.894Z" }, + { url = "https://files.pythonhosted.org/packages/4c/19/1b5044cd8e779831158f5266a40e3ded80f5c5eb6e246a1a9b8f95887acb/xraylib-4.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a07ecc5bf78c8c65b82dd7f0adef343b7c9d27edba26a05719da567e573853d6", size = 14898210, upload-time = "2025-12-21T11:32:15.722Z" }, + { url = "https://files.pythonhosted.org/packages/75/44/c192888960dcbbaa69499583ce878990f8aa3cc4a15d3d3c90029180f540/xraylib-4.2.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:981fe1b9b1cbabb1673ea5a819f113eb1e54721c0d465e104fdc1feca3d6cf26", size = 15951488, upload-time = "2025-12-21T11:32:18.47Z" }, + { url = "https://files.pythonhosted.org/packages/aa/a4/be4fbeeccb0ee2fe93d81e013b0c7971f6ffdead1db60c08bf11f5cbaaee/xraylib-4.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fc2900eed0356912c3d16a83a91938e67af76d0d4254e8ee3778ba1a81a53400", size = 16171125, upload-time = "2025-12-21T11:32:21.676Z" }, + { url = "https://files.pythonhosted.org/packages/50/76/2e04a1aa60bc71649253b0d3507c4b123c52f2b1909f62b5e4933e0d62aa/xraylib-4.2.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3042e991fe443620d27bfc485b7572f440ef9b4bb2d3a37e55d2ab1ee31144a5", size = 16286751, upload-time = "2025-12-21T11:32:24.1Z" }, + { url = "https://files.pythonhosted.org/packages/69/a0/f89531cc898da313acbc4f24d967e3566b4b8edaf4ecc960c74072809577/xraylib-4.2.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2c34a3f84fb38ab31b98e88eeebd20a77692c21555e4d0d7e4579a1aff65de3", size = 16327482, upload-time = "2025-12-21T11:32:26.974Z" }, + { url = "https://files.pythonhosted.org/packages/e3/69/093211cccac0761fe615315ceeb4157da16e07719a8acb156feb68293f6d/xraylib-4.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:31883da0fbba7600973aff4ce49377798167d1ebb11c4fbf0190db9882cdcea6", size = 14919860, upload-time = "2025-12-21T11:32:29.71Z" }, + { url = "https://files.pythonhosted.org/packages/35/80/c043c0f3d85c248f7397826a79ddb54c4e9c06c0fc23dd65672bb33c303d/xraylib-4.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f0869d1468e03020ca79a4032c16772f7c604329cb0d9843b90b494998cda325", size = 15960513, upload-time = "2025-12-21T11:32:32.068Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e7/71a62321e3a957aa7c4a9797c4992a5811be6fe481377487a135ce7ef47a/xraylib-4.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0bc8a92e4d606cbc60ff805d09388903067ea2f12e8c96f91dff2c4ca87f8e2d", size = 16180035, upload-time = "2025-12-21T11:32:34.459Z" }, + { url = "https://files.pythonhosted.org/packages/f5/79/3c6966d3e4580da7b3a629f5d564f3a1a98231ef0f4966c5170996408e1e/xraylib-4.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:03c987598d2d062681a32f1b3b64001c6de40a392e2dd22d59fb8d94f3b6f544", size = 16292005, upload-time = "2025-12-21T11:32:36.944Z" }, + { url = "https://files.pythonhosted.org/packages/1d/40/887c6c2e94b5a92c5c7ba04c9f3ba218674e7751b75ffd4628b06d6c6053/xraylib-4.2.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f23afbe8fc9fb8382402365e8b487834e7b4c02bf174b1c8a3573fcf28ca3f25", size = 16330388, upload-time = "2025-12-21T11:32:39.203Z" }, + { url = "https://files.pythonhosted.org/packages/ce/66/678e391fd96686b82f2e93ae7749bacb2c118525c072c6925af1f0084ee2/xraylib-4.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f6c589d914f764f42a199ef9fe22e6331a4d7a6e241cb87c9d2d8e7f2f95634e", size = 14943752, upload-time = "2025-12-21T11:32:41.878Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/21/093488dfc7cc8964ded15ab726fad40f25fd3d788fd741cc1c5a17d78ee8/zipp-3.23.1.tar.gz", hash = "sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110", size = 25965, upload-time = "2026-04-13T23:21:46.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/8a/0861bec20485572fbddf3dfba2910e38fe249796cb73ecdeb74e07eeb8d3/zipp-3.23.1-py3-none-any.whl", hash = "sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc", size = 10378, upload-time = "2026-04-13T23:21:45.386Z" }, +]